什么是Webpack
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
在下图中可以看出,Webpack 将左侧错综复杂的各自不同类型文件的模板依赖关系,包括 .js、.hbs、.cjs、.sass、.jpg、.png 等类型文件,打包成 .js、.css、.jpg、.png 4 种类型的静态资源。
简单来说,Webpack 就是一个静态资源打包工具,负责将项目中依赖的各个模块,打包成一个或多个文件。
它的主要功能包括:
1)模块打包
将项目中的所有模块(JavaScript、CSS、图片等)当作一个整体,通过依赖关系将它们打包成一个或多个静态资源文件
2)依赖管理
Webpack 可以分析模块之间的依赖关系,根据配置的入口文件找出所有依赖的模块,并将其整合到打包结果中
3)文件转换
Webpack 本身只能处理 JavaScript 模块,但通过加载器(Loader)的使用,可以将其他类型的文件(如CSS、LESS、图片等)转换为有效的模块,使其能够被打包到最终的结果中
4)代码拆分
Webpack 支持将代码拆分成多个模块,按需加载,实现按需加载和提升应用性能
5)插件系统
Webpack 提供了丰富的插件系统,可以通过插件实现各种功能的扩展,例如压缩代码、自动生成 HTML 文件等
基本概念
dependency graph(依赖图)
依赖图指的就是各个模块之间的依赖关系,例如模块 A 导入了模块 B,则在依赖图中,模块 A 会指向模块 B,如上文引入过的图:
构建依赖图的主要步骤如下:
1)解析入口文件
从配置文件中指定的入口文件(如index.js
)开始解析,入口文件是打包的起点
2)查找依赖
Webpack 会解析入口文件中的代码,查找所有通过import
、require
或其他方式引入的模块,例如以下代码,Webpack 会找到vue
和./styles.css
这两个依赖
import Vue from 'vue';
import './styles.css';
3)递归解析
对于每个找到的依赖,Webpack 会继续递归解析其内部的依赖。例如./styles.css
文件中可能又引入了其他 CSS 文件或图片资源,Webpack 会继续解析这些资源
4)构建图结构
在解析过程中,Webpack 会将每个文件作为图中的一个节点,并根据依赖关系建立边,最终形成一个完整的依赖图
entry(入口)
入口是指依赖关系图的开始,从入口开始寻找依赖,打包构建。Webpack 允许一个或多个入口配置,配置示例如下:
module.exports = {
entry: 'index.js',
};
output(输出)
输出则是用于配置 Webpack 构建打包的出口,如打包的位置、文件名等,配置示例如下:
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
loader(加载器)
Webpack 自带 JavaScript 和 JSON 文件的打包构建能力,无需格外配置。而其他类型的文件,如 CSS、TypeScript,则需要安装 loader 来进行处理,它让 Webpack 能够去处理其他类型的文件,并将它们转换为有效模块。配置示例如下:
module.exports = {
module: {
rules: [{ test: /.txt$/, use: 'raw-loader' }],
},
};
plugin(插件)
插件则是用于扩展 Webpack 的能力,常见的插件有:
- ProgressBarPlugin:编译进度条
- BundleAnalyzerPlugin:打包体积分析
- MiniCssExtractPlugin:提取 CSS 到独立 bundle 文件
mode(模式)
Webpack5 提供了模式选择,包括开发模式、生产模式、空模式,并对不同模式做了对应的内置优化,可通过配置模式让项目性能更优,配置示例如下:
module.exports = {
mode: 'development',
};
resolve(解析)
resolve 用于控制模块的解析规则,通过配置 resolve,Webpack 可以更高效地解析模块路径,找到正确的文件,并处理不同类型的模块。常见的配置项如下:
- alias:为模块路径设置别名,简化模块引入
- extensions:用于指定在解析模块时自动尝试的文件扩展名,在引入模块时可不带后缀
- modules:指定 Webpack 在解析模块时应该查找的目录,默认情况下会查找 node_modules 目录
- symlinks:控制是否解析符号链接,禁用可提升编译速度
配置示例如下:
module.exports = {
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.d.ts'],
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components')
}
modules: [path.resolve(__dirname, 'src'), 'node_modules'], // 先查找 src 目录,再查找 node_modules
symlinks: false,
}
}
optimization(优化)
optimization 用于自定义 Webpack 的内置优化配置,一般用于生产模式提升性能,常用配置项如下:
- minimize:是否需要压缩 bundle
- minimizer:配置压缩工具,如 TerserPlugin、OptimizeCSSAssetsPlugin
- splitChunks:拆分 bundle
- runtimeChunk:是否需要将所有生成 chunk 之间共享的运行时文件拆分出来
配置示例如下:
module.exports = {
optimization: {
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
// `...`,
new CssMinimizerPlugin(),
],
splitChunks: {
// include all types of chunks
chunks: 'all',
// 重复打包问题
cacheGroups:{
vendors:{ //node_modules里的代码
test: /[\/]node_modules[\/]/,
chunks: "all",
name: 'vendors', //chunks name
priority: 10, //优先级
enforce: true
}
}
},
},
}
核心流程
Webpack 核心功能:将各种类型的资源,包括图片、css、js 等,转译、组合、拼接、生成 JS 格式的 bundler 文件,这个过程核心完成了内容转换 + 资源合并两种功能,实现上包含三个阶段:初始化阶段、构建阶段、生成阶段,单次构建过程自上而下按顺序执行。
初始化阶段
1)初始化参数:从配置文件、 配置对象、Shell 参数中读取,与默认配置结合得出最终的参数
2)创建编译器对象:用上一步得到的参数创建 Compiler
对象
3)初始化编译环境:包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等
4)开始编译:执行compiler
对象的run
方法
5)确定入口:根据配置中的entry
找出所有的入口文件,调用compilition.addEntry
将入口文件转换为 dependence
对象
构建阶段
构建阶段从entry
开始递归解析资源与资源的依赖,在compilation
对象内逐步构建出module
集合以及module
之间的依赖关系,核心流程:
从上图看出,构建阶段从入口文件开始:
-
调用
handleModuleCreate
,根据文件类型构建module
子类 -
调用 loader-runner 仓库的
runLoaders
转译module
内容,通常是从各类资源类型转译为 JS 文本 -
调用 acorn 将 JS 文本解析为 AST
-
遍历 AST,触发各种钩子
- 监听
exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖 - 调用
module
对象的addDependency
将依赖对象加入到module
依赖列表中
- 监听
-
AST 遍历完毕后,调用
module.handleParseResult
处理模块依赖 -
对于
module
新增的依赖,调用handleModuleCreate
,控制流回到第一步 -
所有依赖都解析完毕后,构建阶段结束
这个过程中数据流:module => ast => dependences => module
,先转 AST 再从 AST 找依赖,这就要求loaders
处理完的最后结果必须是可以被 acorn 处理的标准 JavaScript 语法。比如说对于图片,需要从图像二进制转换成 base64 格式或 url 格式。
compilation
按这个流程递归处理,逐步解析出每个模块的内容以及module
依赖关系,后续就可以根据这些内容打包输出。
生成阶段
基础流程
构建阶段围绕module
展开,生成阶段则围绕chunks
展开。经过构建阶段之后,Webpack 得到足够的模块内容与模块关系信息,接下来开始生成最终资源了。代码层面就是开始执行compilation.seal
函数:
// 取自 webpack/lib/compiler.js
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
// ...
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
// ...
this.hooks.finishMake.callAsync(compilation, err => {
// ...
process.nextTick(() => {
compilation.finish(err => {
**compilation.seal**(err => {...});
});
});
});
});
});
}
seal
函数主要完成从 module
到 chunks
的转化,核心流程:
简单梳理一下:
- 构建本次编译的
ChunkGraph
对象 - 遍历
compilation.modules
集合,将module
按entry/动态引入
的规则分配给不同的Chunk
对象 compilation.modules
集合遍历完毕后,得到完整的chunks
集合对象,调用createXxxAssets
方法createXxxAssets
遍历module/chunk
,调用compilation.emitAssets
方法将assets
信息记录到compilation.assets
对象中- 触发
seal
回调,控制流回到compiler
对象
这一步的关键逻辑是将module
按规则组织成chunks
,Webpack 内置的 chunk 封装规则比较简单:
entry
及entry
触达到的模块,组合成一个chunk
- 使用动态引入语句引入的模块,各自组合成一个
chunk
chunk
是输出的基本单位,默认情况下这些chunks
与最终输出的资源一一对应,那按上面的规则大致上可以推导出一个entry
会对应打包出一个资源,而通过动态引入语句引入的模块,也对应会打包出相应的资源
SplitChunksPlugin
的作用
上面提到 Webpack 主流程里面是按entry / 动态引入
两种情况组织chunks
的,这必然会引发一些不必要的重复打包,而SplitChunksPlugin
能将多个模块中重复的代码提取到一个单独的chunk
中,这样可以避免在多个文件中重复加载相同的代码,从而减少最终打包文件的体积。
SplitChunksPlugin
的拆分过程分为以下几个步骤:
1)模块依赖分析:通过 Webpack 的模块依赖图,识别出被多个chunk
共同引用的模块
2)应用拆分规则:根据配置的规则(如minSize
、minChunks
、chunks
等),筛选出符合条件的模块
3)创建新的chunk
:将符合条件的模块移动到一个新的 chunk
中,并更新模块依赖关系
4)生成最终的打包文件:根据更新后的依赖关系,生成最终的打包文件
资源形态流转
上面已经把逻辑层面的构造主流程梳理完了,这里结合资源形态流转的角度重新考察整个过程:
compiler.make
阶段:
entry
文件以dependence
对象形式加入compilation
的依赖列表,dependence
对象记录有entry
的类型、路径等信息- 根据
dependence
调用对应的工厂函数创建module
对象,之后读入module
对应的文件内容,调用loader-runner
对内容做转化,转化结果若有其它依赖则继续读入依赖资源,重复此过程直到所有依赖均被转化为module
compilation.seal
阶段:
- 遍历
module
集合,根据entry
配置及引入资源的方式,将module
分配到不同的chunk
- 遍历
chunk
集合,调用compilation.emitAsset
方法标记chunk
的输出规则,即转化为assets
集合
compiler.emitAssets
阶段:
- 将
assets
写入文件系统