什么是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写入文件系统
892

被折叠的 条评论
为什么被折叠?



