📚 官方文档:Webpack 官网 | Webpack 中文文档
🚀 实践项目:webpack-simple-demo - 本文档的完整示例项目
Webpack 主要是干什么的?
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)。简单来说,它就是把你的项目中的各种文件(TS、Less、JS、CSS、图片等)打包成一个或多个浏览器可以直接使用的文件。特别像搬家公司,把你的东西打包进各种盒子里。 英文中"bundle"意为"捆扎、打包",形象地描述了将多个松散文件整合为单一或少量文件的过程。 想象你打包东西,也是一样,把一堆东西打包成一个或多个包装。
核心功能(用简单的话理解)- 打包、依赖、转换、优化
1. 模块打包 📦
- 作用:把分散的多个文件合并成少数几个文件
- 比喻:就像把一堆散乱的积木组装成一个完整的模型
- 实际效果:原本 100 个 JS 文件 → 打包后变成 2-3 个文件
2. 依赖管理 🔗
- 作用:自动处理文件之间的依赖关系
- 比喻:就像一个智能管家,知道先加载哪个文件,后加载哪个文件
- 实际效果:你不用手动管理
<script>标签的顺序
3. 资源转换 🔄
- 作用:把各种格式的文件转换成浏览器能理解的格式
- 例子:
- Sass/SCSS → CSS
- TypeScript → JavaScript
- ES6+ → ES5(兼容老浏览器)
- 图片优化和压缩
4. 代码优化 ⚡
- 作用:压缩代码、删除无用代码、合并重复代码
- 效果:文件变小,加载更快
用生活例子理解:
想象你要搬家:
Webpack= 搬家公司- 你的文件 = 家里的物品
- 打包过程 = 整理装箱
- 最终结果 = 整齐的搬家箱子
搬家公司会:
- 把散乱的物品整理好(
模块打包) - 知道哪些物品要一起放(
依赖管理) - 把大件拆解成小件方便运输(
格式转换) - 用最少的箱子装最多的东西(
代码优化)
Webpack 核心概念详解
1. Entry(入口) 🚪
- 定义:告诉
Webpack从哪个文件开始打包 - 搬家比喻:就像搬家公司问"从哪个房间开始收拾?"
- 实际例子:
src/index.js就是入口,Webpack从这里开始分析依赖 - 重要性:入口决定了整个依赖图的起点
2. Output(输出) 📦
- 定义:告诉
Webpack把打包后的文件放在哪里 - 搬家比喻:就像告诉搬家公司"把箱子都放到新家的客厅"
- 实际例子:
dist/bundle.js就是输出文件 - 配置:可以设置文件名、路径、公共路径等
3. Loaders(加载器) 🔧
- 定义:告诉
Webpack如何处理不同类型的文件 - 搬家比喻:就像搬家公司有专门的工具处理不同物品
- 易碎品用泡沫包装(
CSS Loader) - 大件家具要拆解(
Babel Loader) - 衣服要折叠压缩(
Terser Loader)
- 易碎品用泡沫包装(
- 常见 Loaders:
babel-loader:处理JavaScriptcss-loader:处理CSSfile-loader:处理图片、字体等sass-loader:处理Sass/SCSS
4. Plugins(插件) 🛠️
- 定义:扩展
Webpack功能的工具 - 搬家比喻:就像搬家公司提供的增值服务
- 清洁服务(
HtmlWebpackPlugin) - 保险服务(压缩优化插件)
- 监控服务(开发服务器)
- 清洁服务(
- 常见 Plugins:
HtmlWebpackPlugin:自动生成HTMLMiniCssExtractPlugin:提取CSSCleanWebpackPlugin:清理输出目录
5. Mode(模式) ⚙️
- 定义:告诉
Webpack使用哪种模式(开发/生产) - 搬家比喻:就像告诉搬家公司"这是临时搬家还是永久搬家"
- 开发模式:快速打包,保留调试信息
- 生产模式:优化打包,压缩代码
6. Module(模块) 📄
- 定义:项目中的每个文件都是一个模块
- 搬家比喻:就像家里的每个房间、每件物品
- 特点:模块之间可以有依赖关系
- 处理:
Webpack会分析模块间的依赖关系
7. Chunk(代码块) 🧩
- 定义:打包后的代码块
- 搬家比喻:就像整理后的搬家箱子
- 类型:
- Entry Chunk:入口代码块
- Vendor Chunk:第三方库代码块
- Common Chunk:公共代码块
8. Dependency Graph(依赖图) 🕸️
- 定义:模块之间的依赖关系图
- 搬家比喻:就像搬家前的物品清单和摆放关系图
- 作用:
Webpack根据依赖图决定打包顺序 - 特点:有向无环图(
DAG)
搬家比喻完整版 🏠
想象你要搬家,Webpack 就是专业的搬家公司:
| Webpack 概念 | 搬家比喻 | 具体说明 |
|---|---|---|
| Entry | 从哪个房间开始收拾 | 告诉搬家公司从主卧开始 |
| Output | 箱子放到新家哪里 | 所有箱子都放到客厅 |
| Loaders | 专业工具处理不同物品 | 易碎品用泡沫,大件要拆解 |
| Plugins | 增值服务 | 清洁、保险、监控服务 |
| Mode | 搬家类型 | 临时搬家 vs 永久搬家 |
| Module | 家里的每个房间/物品 | 厨房、卧室、客厅等 |
| Chunk | 整理后的搬家箱子 | 按房间分类的箱子 |
| Dependency Graph | 物品摆放关系图 | 知道哪些物品要一起搬 |
构建项目 - 理解 webpack
💡 完整示例:本文档的所有代码示例都可以在 webpack-simple-demo 项目中找到
1mkdir webpack-simple-demo 2cd webpack-simple-demo 3pnpm init -y 4
package.json 增加脚本
1{ 2 "name": "webpack-simple-demo", 3 "version": "1.0.0", 4 "description": "", 5 "main": "src/index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1", 8 "build": "webpack", 9 "build:prod": "webpack --mode=production", 10 "dev": "webpack --mode=development" 11 }, 12 "keywords": ["webpack"], 13 "license": "MIT", 14 "devDependencies": { 15 "webpack": "^5.44.0", 16 "webpack-cli": "^4.7.2" 17 } 18} 19
webpack.config.js
1const path = require('path'); 2module.exports = { 3 entry: './src/index.js', 4 output: { 5 filename: 'bundle.js', 6 path: path.resolve(__dirname, 'dist'), 7 }, 8}; 9
src/ 目录结构
src/double.js
1function double(a) { 2 return a * 2; 3} 4 5module.exports = { 6 double, 7}; 8
src/sumDouble.js
1const { double } = require('./double'); 2function sumDouble(a, b) { 3 return double(a + b); 4} 5 6module.exports = { 7 sumDouble, 8}; 9
src/index.js
1const { sumDouble } = require('./sumDouble'); 2 3console.log(sumDouble(1, 2)); 4 5var indexExport = 'indexExport'; 6module.exports = { 7 indexExport, 8}; 9
运行构建
1pnpm install 2pnpm run build 3
那么会生成一个 dist/bundle.js 文件,这个文件是打包后的文件,包含了所有的依赖和模块。
分析 dist/bundle.js 文件
bundle.js 文件是一个自执行函数,包含了三个主要部分:
- 模块定义区域,存储了所有模块(有导出内容的)的代码,以键值对的形式存储。键是模块路径,值是函数,函数接收三个参数,分别是模块对象,导出对象,
require函数,函数内部是模块的代码。 - 模块缓存系统,存储了所有模块的缓存结果,以键值对的形式存储。键是模块路径,值是对象,对象的结构是
{ exports: {} },exports是模块的导出对象。 - 入口模块执行,执行入口模块的代码,会调用
require函数,require函数会调用模块缓存系统,如果模块缓存系统中存在,则直接返回缓存结果,否则会调用模块定义区域,执行模块的代码,设置模块的导出对象,然后缓存结果,最后返回模块的导出对象。
这里注意,src 中的模块里 require 语句会替换为 __webpack_require__ 函数。__webpack_require__ 函数是 Webpack 自己实现的 require 函数,用来替代 Node.js 的 require。它负责加载和管理模块。功能是:检查缓存、创建模块、执行模块、返回导出对象。
1(() => { 2 // 1.模块定义区域 3 var __webpack_modules__ = { 4 './src/double.js': (module) => { 5 function double(a) { 6 return a * 2; 7 } 8 module.exports = { 9 double, 10 }; 11 }, 12 './src/index.js': ( 13 module, 14 __unused_webpack_exports, 15 __webpack_require__ 16 ) => { 17 // require 语句被替换为 __webpack_require__ 函数 18 const { sumDouble } = __webpack_require__('./src/sumDouble.js'); 19 console.log(sumDouble(1, 2)); 20 var indexExport = 'indexExport'; 21 module.exports = { 22 indexExport, 23 }; 24 }, 25 './src/sumDouble.js': ( 26 module, 27 __unused_webpack_exports, 28 __webpack_require__ 29 ) => { 30 const { double } = __webpack_require__('./src/double.js'); 31 function sumDouble(a, b) { 32 return double(a + b); 33 } 34 module.exports = { 35 sumDouble, 36 }; 37 }, 38 }; 39 // 2.模块缓存系统 40 var __webpack_module_cache__ = {}; 41 42 function __webpack_require__(moduleId) { 43 // 1.检查缓存 44 var cachedModule = __webpack_module_cache__[moduleId]; 45 // 2.如果缓存中存在,则直接返回缓存模块的导出对象 46 if (cachedModule !== undefined) { 47 return cachedModule.exports; 48 } 49 // 3.如果缓存中不存在,则创建新的模块对象 50 var module = (__webpack_module_cache__[moduleId] = { 51 exports: {}, 52 }); 53 // 4.执行模块的代码 54 __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 55 // 5.返回模块的导出对象 56 return module.exports; 57 } 58 59 // 3.入口模块执行 60 var __webpack_exports__ = __webpack_require__('./src/index.js'); 61})(); 62
代码执行逻辑详细分析 🔍
🎯 整体执行流程
这段代码的执行可以分为以下几个关键阶段:
第1阶段:初始化阶段
1(() => { 2 // 1.模块定义区域 3 var __webpack_modules__ = { ... }; 4 // 2.模块缓存系统 5 var __webpack_module_cache__ = {}; 6
作用:创建模块定义对象和缓存系统,为后续模块加载做准备。
第 2 阶段:入口模块加载
1var __webpack_exports__ = __webpack_require__('./src/index.js'); 2
作用:从入口模块开始执行,触发整个依赖链的加载。
🔄 详细执行步骤分析
步骤 1:调用入口模块
1__webpack_require__('./src/index.js'); 2
执行过程:
- 检查缓存:
__webpack_module_cache__['./src/index.js']不存在 - 创建模块:
module = { exports: {} } - 执行模块:调用
__webpack_modules__['./src/index.js'](module, module.exports, __webpack_require__)
步骤 2:index.js 模块执行
1// index.js 模块代码 2const { sumDouble } = __webpack_require__('./src/sumDouble.js'); 3console.log(sumDouble(1, 2)); 4var indexExport = 'indexExport'; 5module.exports = { indexExport }; 6
执行过程:
- 遇到 require:
__webpack_require__('./src/sumDouble.js')被调用 - 暂停执行:
index.js暂停,等待sumDouble.js加载完成
步骤 3:sumDouble.js 模块加载
1// sumDouble.js 模块代码 2const { double } = __webpack_require__('./src/double.js'); 3function sumDouble(a, b) { 4 return double(a + b); 5} 6module.exports = { sumDouble }; 7
执行过程:
- 检查缓存:
__webpack_module_cache__['./src/sumDouble.js']不存在 - 创建模块:
module = { exports: {} } - 遇到 require:
__webpack_require__('./src/double.js')被调用 - 暂停执行:
sumDouble.js暂停,等待double.js加载完成
步骤 4:double.js 模块加载
1// double.js 模块代码 2function double(a) { 3 return a * 2; 4} 5module.exports = { double }; 6
执行过程:
- 检查缓存:
__webpack_module_cache__['./src/double.js']不存在 - 创建模块:
module = { exports: {} } - 执行模块:定义
double函数 - 设置导出:
module.exports = { double } - 返回结果:返回
{ double }给sumDouble.js
步骤 5:sumDouble.js 继续执行
1// 现在 double 函数已经可用 2function sumDouble(a, b) { 3 return double(a + b); // double(1 + 2) = double(3) = 6 4} 5module.exports = { sumDouble }; 6
执行过程:
- 继续执行:
double函数已经加载完成 - 定义函数:
sumDouble函数被定义 - 设置导出:
module.exports = { sumDouble } - 返回结果:返回
{ sumDouble }给index.js
步骤 6:index.js 继续执行
1// 现在 sumDouble 函数已经可用 2console.log(sumDouble(1, 2)); // 输出:6 3var indexExport = 'indexExport'; 4module.exports = { indexExport }; 5
执行过程:
- 继续执行:
sumDouble函数已经加载完成 - 调用函数:
sumDouble(1, 2)执行,返回 6 - 输出结果:
console.log(6)输出 6 - 设置导出:
module.exports = { indexExport } - 返回结果:返回
{ indexExport }给入口
🏠 搬家比喻完整版
想象这是一个三层楼的搬家过程:
| 阶段 | 搬家比喻 | 代码执行 | 状态 |
|---|---|---|---|
| 初始化 | 准备搬家工具 | 创建模块定义和缓存 | 准备就绪 |
| 开始搬家 | 从 3 楼开始 | 调用 __webpack_require__('./src/index.js') | 开始执行 |
| 3 楼需要 2 楼 | 3 楼需要客厅的东西 | require('./src/sumDouble.js') | 暂停 3 楼 |
| 2 楼需要 1 楼 | 客厅需要厨房的东西 | require('./src/double.js') | 暂停 2 楼 |
| 1 楼执行 | 厨房先收拾 | double.js 执行完成 | 1 楼完成 |
| 2 楼继续 | 客厅拿到厨房的东西 | sumDouble.js 执行完成 | 2 楼完成 |
| 3 楼继续 | 3 楼拿到客厅的东西 | index.js 执行完成 | 3 楼完成 |
| 搬家完成 | 所有楼层都收拾好 | 输出结果:6 | 全部完成 |
🔍 关键执行特点
- 同步执行
- 模块加载是同步的,必须等待依赖模块加载完成
- 不会出现异步加载的情况
- 依赖链式加载
1index.js → sumDouble.js → double.js 2
- 从最底层开始,逐层向上返回
- 缓存机制
- 每个模块只执行一次
- 后续调用直接返回缓存结果
- 作用域隔离
- 每个模块都有独立的作用域
- 通过
module.exports和require通信
💡 执行结果
最终执行结果:
- 控制台输出:
6 - 模块导出:
{ indexExport: 'indexExport' } - 缓存状态:三个模块都被缓存
这段代码展示了 Webpack 模块系统的核心机制:
- ✅ 模块化:每个模块独立执行
- ✅ 依赖管理:自动处理模块间的依赖关系
- ✅ 缓存优化:避免重复加载
- ✅ 同步执行:确保依赖关系正确
搬家比喻:就像三层楼的搬家,从顶层开始,逐层向下收集需要的东西,最后完成整个搬家过程!🏠
输出的文件名配置
真实的项目中,output 的输出文件名配置通常是动态的,根据入口文件的名称和哈希值来生成。 而哈希值有三种类型:
hash:基于整个项目的构建内容生成全局唯一哈希值,同一次构建过程中所有文件的hash值相同,适用于开发环境或需要强制更新所有资源的场景chunkhash:根据入口文件(chunk)及其依赖模块的内容生成独立哈希值,不同入口文件的chunkhash互不影响,用于入口JS文件命名,例如[name].[chunkhash].jscontenthash:基于文件自身内容生成哈希值,仅当文件内容变化时才会更新,适用于抽离的CSS文件或静态资源,避免因关联文件修改导致不必要的缓存失效,例如[name].[contenthash].css
1entry: { 2 page1: './src/page1.js', 3 page2: './src/page2.js', 4}, 5output: { 6 filename: '[name].[chunkhash:8].bundle.js', 7 path: path.resolve(__dirname, 'dist'), 8} 9
src 建立四个文件,2 个入口文件,2 个依赖文件:
1// src/page1.js 2var page1 = require('./page1-a'); 3console.log('page1'); 4console.log(page1); 5 6// src/page2.js 7var page2 = require('./page2-a'); 8console.log('page2'); 9console.log(page2); 10 11// src/page1-a.js 12module.exports = 'page1-a'; 13 14// src/page2-a.js 15module.exports = 'page2-a'; 16
执行 build 之后:
1// dist/page1.18656d4d.bundle.js 2// dist/page2.18656d4d.bundle.js 3
如果只有 page1.js 进行修改,那么 page1.bundle.js 的哈希值会变化,而 page2.bundle.js 的哈希值不会变化。
loader 的配置 - 文件类型转换
📖 官方文档:Webpack Loaders | Loader API
Webpack 中的 Loader 是用于将非 JavaScript 模块(如 CSS、图片、TypeScript 等)转换为 Webpack 能处理的模块的工具。它们像"翻译员"一样,通过链式调用对文件进行多步转换。
webpack-simple-demo 项目中,我们使用了 ts-loader 来将 ts 文件转换为 js 文件。
webpack.config.js 中,配置了 extensions 和 module.rules。extensions 配置了文件的扩展名,module.rules 配置了 loader 的规则。
rules 中,我们配置了 ts-loader 来将 ts 文件转换为 js 文件。test 属性是正则表达式,用于匹配文件。use 属性是 loader 的名称。exclude 属性是排除的文件。include 属性是包含的文件。include 和 exclude 是互斥的,只能配置一个。
先配置 package.json:
1"devDependencies": { 2 "typescript": "^5.0.0", 3 "ts-loader": "^9.4.0", 4 "@types/node": "^20.0.0", 5} 6
然后配置 webpack.config.js:
1// 模块解析 2 resolve: { 3 extensions: ['.ts', '.js'], 4 }, 5 6 // 模块规则 7 module: { 8 rules: [ 9 { 10 test: /\.ts$/, 11 use: 'ts-loader', 12 exclude: /node_modules/, 13 }, 14 ], 15 }, 16
src 的文件结构如下:
1// src/page1.ts 2import page1A from './page1-a'; 3console.log(page1A); 4 5// src/page1-a.ts 6export default 'page1-a'; 7 8// src/page2.ts 9import page2A from './page2-a'; 10console.log(page2A); 11 12// src/page2-a.ts 13export default 'page2-a'; 14
先执行 pnpm install,再执行 npm run build 之后:
1// dist/page1.95962465.bundle.js 2// dist/page2.18656d4d.bundle.js 3// dist/page1-a.d.ts 4// dist/page2-a.d.ts 5// dist/page1.d.ts 6// dist/page2.d.ts 7
这里每个 ts 文件都生成了一个 d.ts 文件,这个文件是类型定义文件,用来定义模块的类型。
如果一个文件需要多个 loader 解析,比如 less 文件,那么需要配置多个 loader,且 loader 的执行顺序是从右到左,也就是数组的最后一个 loader 先执行:
1{ 2 test: /\.less$/, 3 use: ['style-loader', 'css-loader', 'less-loader'], 4} 5
这里我们配置了三个 loader,分别是 style-loader、css-loader 和 less-loader:
less-loader用来将Less语法转换为普通CSS,例如:@color: red;→color: red;css-loader处理转换后的css文件,解析@import语句,处理url(./image.png)等资源路径,若启用modules,会生成类名哈希(如.class_1234)style-loader将最终CSS代码通过JavaScript动态插入DOM:document.head.innerHTML += '<style>body { color: red; }</style>';
plugin 的配置
📖 官方文档:Webpack Plugins | Plugin API
Webpack 中的 plugin(插件)是用于扩展 Webpack 功能的模块,通过在构建流程中注入钩子来实现资源处理、优化等自定义任务。与 loader(专注于文件转换)不同,plugin 作用于 Webpack 的整个编译生命周期,能够执行更广泛的任务,如打包优化、资源管理、环境变量注入等。
webpack-simple-demo 项目中,我们使用了 HtmlWebpackPlugin 来举例说明 plugin 的配置。HtmlWebpackPlugin 是 Webpack 生态中的核心插件,主要用于自动生成 HTML 文件并管理打包资源的注入。
先配置 package.json:
1"devDependencies": { 2 "html-webpack-plugin": "^5.5.3", 3} 4
然后配置 webpack.config.js:
1const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 3 plugins: [ 4 // 为 page1 生成 HTML 5 new HtmlWebpackPlugin({ 6 template: './src/templates/page1.html', // 模板文件 7 filename: 'page1.html', // 输出文件名 8 chunks: ['page1'], // 需要注入到当前HTML的JS模块 9 title: 'Page1', // 标题 10 meta: { // 元数据 11 viewport: 'width=device-width, initial-scale=1', 12 description: 'Page1 页面' 13 }, 14 inject: 'body', // 资源注入位置 15 minify: { // 生产环境压缩 16 removeComments: true, 17 collapseWhitespace: true 18 } 19 }), 20 // 为 page2 生成 HTML 21 new HtmlWebpackPlugin({ 22 template: './src/templates/page2.html', 23 filename: 'page2.html', 24 chunks: ['page2'], 25 title: 'Page2', 26 meta: { 27 viewport: 'width=device-width, initial-scale=1', 28 description: 'Page2 页面' 29 }, 30 inject: 'body', 31 minify: { 32 removeComments: true, 33 collapseWhitespace: true 34 } 35 }) 36 ], 37
然后在 src 里 templates 里建立两个 html 文件:
1// src/templates/page1.html 2<!DOCTYPE html> 3<html lang="zh-CN"> 4 <head> 5 <meta charset="UTF-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title><%= htmlWebpackPlugin.options.title %></title> 8 <meta 9 name="description" 10 content="<%= htmlWebpackPlugin.options.meta.description %>" 11 /> 12 </head> 13 <body> 14 <div class="container"> 15 <h1>Page1</h1> 16 17 <div class="nav"> 18 <a href="page1.html">Page1</a> 19 <a href="page2.html">Page2</a> 20 </div> 21 </div> 22 </body> 23</html> 24 25// src/templates/page2.html 26<!DOCTYPE html> 27<html lang="zh-CN"> 28 <head> 29 <meta charset="UTF-8" /> 30 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 31 <title><%= htmlWebpackPlugin.options.title %></title> 32 <meta 33 name="description" 34 content="<%= htmlWebpackPlugin.options.meta.description %>" 35 /> 36 </head> 37 <body> 38 <div class="container"> 39 <h1>Page2</h1> 40 41 <div class="nav"> 42 <a href="page1.html">Page1</a> 43 <a href="page2.html">Page2</a> 44 </div> 45 </div> 46 </body> 47</html> 48
先执行 pnpm install,再执行 npm run build 之后:
1// dist/page1.html 里面自动注入<script defer src="page1.c4e91d29.bundle.js"></script> 2// dist/page2.html 里面自动注入<script defer src="page2.c4e91cb1.bundle.js"></script> 3
这里生成了两个 HTML 文件,分别是 page1.html 和 page2.html。可以拖动浏览器查看效果。
开发服务器
📖 官方文档:Webpack Dev Server | Dev Server API
Webpack 的开发服务器是用于在开发过程中提供实时预览和调试功能的工具。它允许开发者在浏览器中实时查看代码变化,并提供自动刷新和热更新功能,从而加快开发速度和提升开发体验。
webpack-simple-demo 项目中,我们使用了 webpack-dev-server 来举例说明开发服务器的配置。
先配置 package.json:
1"scripts": { 2 "serve": "webpack serve --config webpack.config.js" 3}, 4"devDependencies": { 5 "webpack-dev-server": "^4.15.1", 6} 7
然后配置 webpack.config.js:
1module.exports = { 2 // 开发服务器配置 3 devServer: { 4 static: { 5 directory: path.join(__dirname, 'dist'), 6 }, 7 compress: true, 8 port: 3000, 9 hot: true, 10 open: true, 11 historyApiFallback: { 12 rewrites: [ 13 { from: '/', to: '/page1.html' }, 14 { from: /^\/page1/, to: '/page1.html' }, 15 { from: /^\/page2/, to: '/page2.html' }, 16 ], 17 }, 18 }, 19}; 20
然后执行 npm run serve 之后,浏览器会自动打开 http://localhost:3000 就能看到页面了。
配置区别
一般开发环境配置和生产环境配置的配置是不同的,比如开发环境配置会包含开发服务器配置,而生产环境配置会包含代码压缩配置。
webpack-simple-demo 项目中,我们使用了 webpack-merge 来举例说明配置合并的配置。
先配置 package.json:
1"devDependencies": { 2 "webpack-merge": "^5.8.0", 3} 4
然后配置 webpack.config.base.js:
1const path = require('path'); 2const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 4module.exports = { 5 // 入口文件 6 entry: { 7 page1: './src/page1.ts', 8 page2: './src/page2.ts', 9 }, 10 11 // 模块解析 12 resolve: { 13 extensions: ['.ts', '.js'], 14 }, 15 16 // 模块规则 17 module: { 18 rules: [ 19 { 20 test: /\.ts$/, 21 use: 'ts-loader', 22 exclude: /node_modules/, 23 }, 24 ], 25 }, 26 27 // 插件配置 28 plugins: [ 29 // 为 page1 生成 HTML 30 new HtmlWebpackPlugin({ 31 template: './src/templates/page1.html', 32 filename: 'page1.html', 33 chunks: ['page1'], 34 title: 'Page1 - TypeScript Webpack Demo', 35 meta: { 36 viewport: 'width=device-width, initial-scale=1', 37 description: 'Page1 页面 - TypeScript Webpack 示例', 38 }, 39 }), 40 // 为 page2 生成 HTML 41 new HtmlWebpackPlugin({ 42 template: './src/templates/page2.html', 43 filename: 'page2.html', 44 chunks: ['page2'], 45 title: 'Page2 - TypeScript Webpack Demo', 46 meta: { 47 viewport: 'width=device-width, initial-scale=1', 48 description: 'Page2 页面 - TypeScript Webpack 示例', 49 }, 50 }), 51 ], 52}; 53
然后配置 webpack.config.dev.js:
1const { merge } = require('webpack-merge'); 2const baseConfig = require('./webpack.config.base'); 3 4// @ts-ignore 5module.exports = merge(baseConfig, { 6 // 模式 7 mode: 'development', 8 9 // 开发工具 10 devtool: 'eval-cheap-module-source-map', 11 12 // 输出配置(开发环境) 13 output: { 14 filename: '[name].bundle.js', 15 path: require('path').resolve(__dirname, 'dist'), 16 clean: true, 17 }, 18 19 // 开发服务器配置 20 devServer: { 21 static: { 22 directory: require('path').join(__dirname, 'dist'), 23 }, 24 compress: true, 25 port: 3000, 26 hot: true, 27 open: true, 28 historyApiFallback: { 29 rewrites: [ 30 { from: '/', to: '/page1.html' }, 31 { from: /^\/page1/, to: '/page1.html' }, 32 { from: /^\/page2/, to: '/page2.html' }, 33 ], 34 }, 35 }, 36 37 // 开发环境优化 38 optimization: { 39 splitChunks: { 40 chunks: 'all', 41 cacheGroups: { 42 vendor: { 43 test: /[\\/]node_modules[\\/]/, 44 name: 'vendor', 45 chunks: 'all', 46 priority: 10, 47 }, 48 common: { 49 name: 'common', 50 minChunks: 2, 51 chunks: 'all', 52 priority: 5, 53 reuseExistingChunk: true, 54 }, 55 }, 56 }, 57 }, 58 plugins: [], 59}); 60
然后配置 webpack.config.prod.js:
1const { merge } = require('webpack-merge'); 2const TerserPlugin = require('terser-webpack-plugin'); 3const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 4const baseConfig = require('./webpack.config.base'); 5// @ts-ignore 6module.exports = merge(baseConfig, { 7 // 模式 8 mode: 'production', 9 10 // 开发工具 11 devtool: 'source-map', 12 13 // 输出配置(生产环境) 14 output: { 15 filename: '[name].[contenthash:8].bundle.js', 16 path: require('path').resolve(__dirname, 'dist'), 17 clean: true, 18 }, 19 20 // 生产环境优化 21 optimization: { 22 minimize: true, 23 minimizer: [ 24 new TerserPlugin({ 25 terserOptions: { 26 compress: { 27 drop_console: true, 28 drop_debugger: true, 29 pure_funcs: ['console.log', 'console.info', 'console.debug'], 30 }, 31 mangle: { 32 safari10: true, 33 }, 34 }, 35 extractComments: false, 36 }), 37 new CssMinimizerPlugin(), 38 ], 39 splitChunks: { 40 chunks: 'all', 41 minSize: 20000, 42 maxSize: 244000, 43 cacheGroups: { 44 vendor: { 45 test: /[\\/]node_modules[\\/]/, 46 name: 'vendor', 47 chunks: 'all', 48 priority: 10, 49 enforce: true, 50 }, 51 common: { 52 name: 'common', 53 minChunks: 2, 54 chunks: 'all', 55 priority: 5, 56 reuseExistingChunk: true, 57 enforce: true, 58 }, 59 }, 60 }, 61 }, 62 63 // 性能配置 64 performance: { 65 hints: 'warning', 66 maxEntrypointSize: 512000, 67 maxAssetSize: 512000, 68 }, 69}); 70
开发环境的主要特性:
- 模式:使用
development - 开发工具:使用
eval-cheap-module-source-map - 输出配置:使用
[name].bundle.js - 热更新:支持代码修改后自动刷新
- 开发服务器:内置开发服务器
- 路由支持:支持单页应用路由
- 自动打开:启动后自动打开浏览器
这样构建出来的文件:文件不压缩,便于调试;包含源码映射;快速构建;文件大小较大。
生产环境的主要特性:
- 模式:使用
production - 开发工具:使用
source-map - 输出配置:使用
[name].[contenthash:8].bundle.js - 代码压缩:使用
TerserPlugin - 文件哈希:使用
contenthash - 性能优化:使用
splitChunks - 源码映射:使用
source-map - 控制台清理:移除
console.log等调试代码
这样构建出来的文件:代码压缩,文件更小;移除调试代码;文件哈希命名;性能优化。
开发一个自己的 loader - md 转 html
📖 官方文档:Writing a Loader | Loader Examples
💡 实践示例:查看 webpack-simple-demo 中的
md2html-loader.js实现
loader 本质是一个导出函数的 Node.js 模块,接收源文件内容并返回 js 代码字符串或 Buffer 类型数据。 基本模板如下:
1module.exports = function (source) { 2 // source里最后含有module.exports=xx 3 return source; 4}; 5
现在想实现,能通过 import xx from './xx.md' 的方式引入 md 文件,并转换为 html 代码,然后插入到 html 文件中。
首先写一个 md2html-loader.js:
1// 简单的 Markdown 解析器(基础功能) 2function simpleMarkdownParser(text) { 3 return ( 4 text 5 // 标题 6 .replace(/^### (.*$)/gim, '<h3>$1</h3>') 7 .replace(/^## (.*$)/gim, '<h2>$1</h2>') 8 .replace(/^# (.*$)/gim, '<h1>$1</h1>') 9 // 粗体 10 .replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>') 11 // 斜体 12 .replace(/\*(.*)\*/gim, '<em>$1</em>') 13 // 代码块 14 .replace(/``[`([\s\S]*?)`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.s.md)``/gim, '<pre><code>$1</code></pre>') 15 // 行内代码 16 .replace(/`([^`]*)`/gim, '<code>$1</code>') 17 // 链接 18 .replace(/\[([^\]]*)\]\(([^)]*)\)/gim, '<a href="$2">$1</a>') 19 // 换行 20 .replace(/\n/gim, '<br>') 21 ); 22} 23 24module.exports = function (source) { 25 const options = this.getOptions() || {}; 26 console.log('获取use的options配置', options); 27 // 解析 Markdown 为 HTML 28 const html = simpleMarkdownParser(source); 29 30 // 开发环境下输出转换结果 31 if (process.env.NODE_ENV === 'development') { 32 console.log('📝 Markdown 转换结果:'); 33 console.log('原文:', source); 34 console.log('HTML:', html); 35 } 36 37 // 返回 JavaScript 模块 38 return [`module.exports = ${JSON.stringify(html)}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.html.md); 39}; 40
然后配置 webpack.config.js:
1module.exports = { 2 module: { 3 rules: [ 4 { 5 test: /\.md$/, 6 use: { 7 loader: path.resolve(__dirname, 'md2html-loader'), 8 options: { 9 breaks: true, // 支持换行符 10 headerIds: true, // 生成标题 ID 11 mangle: false, // 不混淆 HTML 属性 12 }, 13 }, 14 exclude: /node_modules/, 15 }, 16 ], 17 }, 18}; 19
然后再 src 里创建一个 page1-a.md 文件:
1# page1-a 2 3我是 page1-a 文件 4
再在 page1.js 文件中引入 page1-a.md 文件:
1// page1 入口文件 2import page1A from './page1-a'; 3import page1Md from './page1-a.md'; 4 5function renderMd() { 6 const div = document.createElement('div'); 7 div.innerHTML = page1Md; 8 document.body.appendChild(div); 9} 10 11renderMd(); 12 13console.log(page1A); 14
然后执行 npm run serve 之后,就能在浏览器看到 md 文件的内容了。
开发 plugin 的前置知识
📖 官方文档:Writing a Plugin | Plugin API
💡 实践示例:查看 webpack-simple-demo 中的
file-list-plugin.js实现
plugin 本质是一个导出函数的 Node.js 模块,接收 Compiler 对象,然后通过 Compiler 对象的钩子函数,在构建过程中执行自定义任务。
基本模板如下:
1class MyPlugin { 2 constructor(options) { 3 this.options = options; 4 } 5 apply(compiler) { 6 const hooks = compiler.hooks; 7 // compile编译开始时触发 8 hooks.compile.tap('MyPlugin', (compilation, callback) => {}); 9 // emit生成资源到输出目录前 10 hooks.emit.tap('MyPlugin', (compilation, callback) => { 11 console.log('MyPlugin'); 12 }); 13 14 // done编译完成后触发 15 hooks.done.tap('MyPlugin', (compilation, callback) => {}); 16 } 17} 18 19module.exports = MyPlugin; 20
使用的时候:
1const MyPlugin = require('./my-plugin'); 2module.exports = { 3 plugins: [new MyPlugin({ text: '1' })], 4}; 5
compiler 和 compilation
compiler 和 compilation 是 webpack 的两大核心对象,它们在插件开发中扮演着重要角色。
compiler
compiler:代表 Webpack 的完整运行环境,从启动到关闭全程存在,仅实例化一次,包含全局配置(如 options、loaders、plugins)和生命周期钩子(如 run、done),插件常通过 compiler.hooks 监听构建事件(如修改配置、触发新编译)
compiler 的核心属性:
options:包含webpack配置(如mode、entry、output)loaders:包含所有loader配置plugins:包含所有plugin配置hooks:包含所有钩子函数,插件常通过compiler.hooks监听构建事件,常用的钩子函数有:compile、emit、done,详见:webpack.js.org/api/compile…
compiler 的核心方法:
run:启动编译watch:监听文件变化compile:触发编译make:创建compilationemit:触发资源输出done:编译完成
compilation
compilation:代表单次编译过程,每次文件变动或重新构建时创建新实例,包含模块依赖图(modules)、代码分块(chunks)和输出资源(assets)等动态数据,插件通过 compilation 操作具体模块(如 emitAsset 添加文件)或优化编译结果
compilation 的核心属性:
modules:包含所有模块依赖图chunks:包含所有代码分块assets:包含所有输出资源
compilation 的核心方法:
addEntry:添加入口addChunk:添加代码分块addAsset:添加输出资源emitAsset:触发资源输出done:编译完成
联系
当文件修改触发重新构建时:
Compiler持续存在,调用compile()方法- 创建新的
Compilation实例处理模块解析和资源生成 - 通过
compilation.assets修改输出内容后,由Compiler完成最终写入
这种设计实现了配置与编译状态的隔离,确保 watch 模式下仅需重建 Compilation 而非整个环境。
开发一个自己的 plugin
💡 实践示例:查看 webpack-simple-demo 中的
file-list-plugin.js完整实现
现在想实现,输出一个 md 文件,里面的内容是打包的文件数量和文件的 list。
首先写一个 file-list-plugin.js:
1class FileListPlugin { 2 constructor(options) { 3 this.options = options; 4 } 5 apply(compiler) { 6 const hooks = compiler.hooks; 7 // emit生成资源到输出目录前 8 hooks.emit.tap('FileListPlugin', (compilation, callback) => { 9 const fileNames = Object.keys(compilation.assets); 10 const content = fileNames.map((filename) => `- ${filename}`).join('\n'); 11 compilation.assets[this.options.outputFile] = { 12 source: function() { 13 return `文件数量是 ${fileNames.length},\n文件列表是:\n${content}`; 14 }, 15 size: function() { 16 return Buffer.byteLength(this.source(), 'utf8'); 17 } 18 }; 19 callback && callback(); 20 }); 21 22 23 } 24} 25 26module.exports = FileListPlugin; 27
然后配置 webpack.config.js:
1module.exports = { 2 plugins: [new FileListPlugin({ outputFile: 'assets.md' })], 3}; 4
然后执行 npm run build:dev 之后,就能在 dist 目录下看到 assets.md 文件了。
📚 相关资源
官方文档
- Webpack 官网 - 官方英文文档
- Webpack 中文文档 - 官方中文文档
- Webpack 概念 - 核心概念详解
- Webpack 配置 - 配置选项说明
核心概念文档
- Entry 和 Output - 入口和输出配置
- Loaders - 加载器概念
- Plugins - 插件系统
- Mode - 开发/生产模式
开发工具
- Webpack Dev Server - 开发服务器
- Source Maps - 源码映射
- Hot Module Replacement - 热模块替换
高级主题
- Code Splitting - 代码分割
- Tree Shaking - 摇树优化
- Caching - 缓存策略
- Performance - 性能优化
实践项目
- webpack-simple-demo - 本文档的完整示例项目
- 包含所有代码示例
- 自定义 Loader 和 Plugin 实现
- 完整的 Webpack 配置
- TypeScript 支持
- 开发和生产环境配置
社区资源
- Awesome Webpack - Webpack 资源集合
- Webpack Examples - 官方示例
- Webpack 中文社区 - 中文社区资源
🎯 总结
通过本文档,你学会了:
- Webpack 基础概念 - 理解了模块打包的核心思想
- 配置文件编写 - 掌握了基础、开发、生产环境的配置
- Loader 开发 - 学会了如何编写自定义加载器
- Plugin 开发 - 掌握了插件系统的使用方法
- 实际项目应用 - 通过 webpack-simple-demo 项目实践
继续深入学习 Webpack,建议:
Happy Coding! 🚀
《用搬家公司的例子来入门webpack》 是转载文章,点击查看原文。