Webpack 原理与插件开发
打包原理
Webpack 的运行流程是一个串行的过程,从启动到结束依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终参数
- 开始编译:用上一步得到的参数初始化
Compiler对象,加载所有配置的插件,执行对象的run方法开始编译 - 确定入口:根据配置中的
entry找出所有入口文件 - 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行转换,再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过处理
- 完成模块编译:在第 4 步使用 Loader 转换完所有模块后,得到每个模块被转换后的最终内容以及依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
- 输出完成:根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,Webpack 会在特定时间点广播出特定的事件,插件在监听到感兴趣的事件后执行特定的逻辑,并且可以调用 Webpack 提供的 API 改变运行结果。
开发一个 Loader
Loader 支持以数组形式配置多个,Webpack 会按顺序链式调用。前一个 Loader 返回的内容会作为下一个的入参。返回值必须是标准 JS 代码字符串。
js
// my-loader.js
module.exports = function (source) {
const content = doSomeThing2JsString(source)
// 如果 loader 配置了 options,this.query 指向 options
const options = this.query
// this.callback 参数:
// error: Error | null
// content: String | Buffer,编译后需要导出的内容
// sourceMap: 生成的 source map
// ast: 本次生成的 AST,后续 loader 可直接使用
this.callback(null, content)
// or return content
}// my-loader.js
module.exports = function (source) {
const content = doSomeThing2JsString(source)
// 如果 loader 配置了 options,this.query 指向 options
const options = this.query
// this.callback 参数:
// error: Error | null
// content: String | Buffer,编译后需要导出的内容
// sourceMap: 生成的 source map
// ast: 本次生成的 AST,后续 loader 可直接使用
this.callback(null, content)
// or return content
}Loader 函数中的 this 上下文由 webpack 的 loader-runner 提供。
开发一个 Plugin
Plugin 需要实现一个 apply 方法:
js
// file-webpack-plugin.js
class FileWebpackPlugin {
constructor(options) {
this.options = options
}
// compiler: webpack 实例
apply(compiler) {
// 定义插件执行时机(异步)
compiler.hooks.emit.tapAsync('FileWebpackPlugin', (compilation, callback) => {
const packFilesLen = Object.keys(compilation.assets).length
let content = `本次构建结果统计:\n\r生成文件数量:${packFilesLen}个;\n\r`
for (let filename in compilation.assets) {
content += `文件名称:${filename},文件大小:${compilation.assets[filename].size()};\n\r`
}
compilation.assets['file.txt'] = {
source: () => content,
size: () => 1024,
}
callback()
})
}
}
module.exports = FileWebpackPlugin// file-webpack-plugin.js
class FileWebpackPlugin {
constructor(options) {
this.options = options
}
// compiler: webpack 实例
apply(compiler) {
// 定义插件执行时机(异步)
compiler.hooks.emit.tapAsync('FileWebpackPlugin', (compilation, callback) => {
const packFilesLen = Object.keys(compilation.assets).length
let content = `本次构建结果统计:\n\r生成文件数量:${packFilesLen}个;\n\r`
for (let filename in compilation.assets) {
content += `文件名称:${filename},文件大小:${compilation.assets[filename].size()};\n\r`
}
compilation.assets['file.txt'] = {
source: () => content,
size: () => 1024,
}
callback()
})
}
}
module.exports = FileWebpackPluginjs
// webpack.config.js
plugins: [new FileWebpackPlugin({ author: 'HBF' })]// webpack.config.js
plugins: [new FileWebpackPlugin({ author: 'HBF' })]Compiler vs Compilation
| 对象 | 说明 |
|---|---|
| Compiler | 包含 Webpack 环境所有的配置信息(options、loaders、plugins),Webpack 启动时实例化,全局唯一,可理解为 Webpack 实例 |
| Compilation | 包含当前模块资源、编译生成资源、变化的文件。开发模式下每次文件变化会创建新的 Compilation |
Compiler 代表了整个 Webpack 从启动到关闭的生命周期,Compilation 只代表一次新的编译。
Compiler 事件钩子

Compilation 事件钩子

常见 Loader
| Loader | 说明 |
|---|---|
file-loader | 把文件输出到文件夹,代码中通过相对 URL 引用 |
url-loader | 类似 file-loader,小文件以 base64 方式注入代码 |
source-map-loader | 加载额外的 Source Map 文件,方便断点调试 |
image-loader | 加载并压缩图片文件 |
babel-loader | 把 ES6 转换成 ES5 |
css-loader | 加载 CSS,支持模块化、压缩、文件导入 |
style-loader | 把 CSS 注入到 JavaScript 中,通过 DOM 加载 |
eslint-loader | 通过 ESLint 检查 JS 代码(已弃用,改用 eslint-webpack-plugin) |
常见 Plugin
| Plugin | 说明 |
|---|---|
ProvidePlugin | 自动加载模块,代替 require 和 import(webpack 内置) |
html-webpack-plugin | 根据模板自动生成 HTML,自动引用 CSS 和 JS |
DefinePlugin | 编译时配置全局变量(webpack 内置) |
HotModuleReplacementPlugin | 热更新(webpack 内置) |
mini-css-extract-plugin | 将 JS 中的样式单独抽离成 CSS 文件 |
css-minimizer-webpack-plugin | CSS 压缩去重 |
webpack-bundle-analyzer | Bundle 文件分析工具(可交互 treemap) |
compression-webpack-plugin | Gzip 压缩 JS 和 CSS |
happypack | 多进程模型加速代码构建 |
clean-webpack-plugin | 清理上次打包未使用的文件 |
speed-measure-webpack-plugin | 查看每个 Loader 和 Plugin 执行耗时 |
Cao Note