Webpack
返回 相关私有笔记 · 相关私有笔记
构建流程

- 初始化参数:从配置文件和 shell 语句读取参数并合并,获得最终的参数
- 开始编译:用初始化参数初始化
compiler对象,加载所有配置的 plugin,执行compiler对象的run方法开始编译流程- 确定入口:根据
entry找出入口文件- 编译模块:从入口文件开始,根据配置的
loader对模块进行转译,如果该模块还有依赖,则递归进行转译,直到所有模块都转译完成- 完成模块编译:经过
loader转译完所有模块后,获得每个模块之间的依赖关系图ModuleGraph- 输出资源:根据入口和模块之间的依赖关系,生成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表中
- 输出完成:根据输出项的配置,将文件内容写到系统中
上述流程简化之后,可以大致分为以下几个阶段:
- 初始化阶段:合并计算配置参数,创建
Compiler、Compilation等基础对象,并初始化 Plugin,并最终根据entry配置,找到所有入口模块 - 构建模块:从
entry开始,调用loader转译对应的模块,调用Acorn将代码转换为AST结构,遍历AST从中构建出完整的模块依赖关系图(递归操作) - 生成阶段:根据
entry配置,根据模块生成一个个 chunk 对象,之后转译 chunk 代码并封装为 asset,最后写出到文件系统
如果启用了
watch,则在单次构建完成之后不会退出 webpack 进程,而是持续监听文件变化,发生变化时回到构建模块阶段重新执行构建
从资源转换角度看构建流程
-
compiler.make阶段entry文件以dependence对象形式加入compilation的依赖列表 ,dependence对象记录了entry的相关信息- 根据
dependency创建 对应的module对象,之后读入module对应的文件内容, 调用loader-runner对内容做转化, 转化结果若有对其他依赖则继续读入依赖资源, 重复此过程直到所有的依赖均被转换为module
-
compilation.seal阶段- 遍历
module集合, 根据entry配置以及引入资源的方式, 将module分配到不同的Chunk Chunk之间最终形成ChunkGraph结构- 遍历
ChunkGraph调用compilation.emitAssets方法标记chunk的输出规则, 及转换为assets集合
- 遍历
-
compiler.emitAssets阶段- 将
assets写入文件系统
- 将
分包策略
在 webpack 打包过程中,经常出现 vendor.js, app.js 单个文件较大的情况,这偏偏又是网页最先加载的文件,这就会使得加载时间过长,从而使得白屏时间过长,影响用户体验。所以我们需要有合理的分包策略。
webpack 中常见的三种代码分割方式:
- 使用
entry配置手动分离代码 - 在模块中使用
import('./xxx')来加载模块代码 - 使用
splitChunks来去重和分离
文件监听
开启文件监听后,webpack 会轮询访问文件的最后修改时间。当发现文件的最后修改时间变化后,会先缓存变化后的文件,等到 aggregateTimeout 再统一执行
开启文件监听方式:可以在构建时带上 --watch 参数或者设置 watch:true,而 watchOptions 则可以对监听的细节进行定制
watch: true,
watchOptions: {
//不监听的文件或者文件夹忽略一些大型的不经常变化的文件可以提高构建速度
ignored: /node_modules/,
//监听到变化会等多少时间再执行
aggregateTimeout: 300,
//判断文件是否发生变化是通过不断轮询指定文件有没有变化实现的
poll: 1000
}热更新 (HMR)
如何开启热更新?⬇️
通过设置devServer: {hot: true}开启,开启后便可以在发生改变后局部刷新改变的部分
原理:
- 使用
webpack-dev-server(WDS)托管静态资源,同时以 runtime 的形式注入 HMR 客户端代码 - 浏览器加载后,与 WDS 建立 `websocket` 连接
- webpack 监听到文件变化之后,增量构建发生变更的模块,并通过
websocket发送 hash 事件 - 浏览器接受到
hash事件之后,请求manifest资源文件,确认增量变更范围 - 浏览器加载发生变更的增量模块
- webpack runtime 触发变更模块的
module.hot.accept回调,执行变更逻辑

常见问题
loader 和 plugin 的区别?
loader
loader 负责将文件内容转化为 webpack 可以处理的 js 代码
plugin
plugin 基于 tapable 事件流框架来监听 webpack 在构建 / 打包过程中的 hooks,通过自定义的逻辑和功能来改变输出结果
如何保证 loader 按照想要的顺序执行?
webpack 会按照 use 定义的顺序从前往后执行 Pitch Loader 从后往前执行 Normal Loader 我们可以将一些预处理的逻辑放在 Pitch 中
可以通过 enforce 来强制控制 Loader 的执行顺序 (pre 表示在所有正常的 loader 执行之前执行,post 则表示在之后执行)
loader 的执行有两个阶段:
- pitching 阶段:loader 上的 pitch 方法,按照
后置(post)、行内(inline)、普通(normal)、前置(pre)的顺序调用。更多详细信息,请查看 Pitching Loader- normal 阶段:loader 上的常规方法,按照
前置(pre)、普通(normal)、行内(inline)、后置(post)的顺序调用。模块源码的转换,发生在这个阶段
如何编写 loader?
即使没有实际地编写过一个 webpack loader,但是当面试官问你这个问题的时候,实际上是要考察你对 loader 这方面知识的掌握程度,所以,你可以讲出一个 loader 的编写要注意的点应该是这样的:
- loader 的主要职责就是将文件内容转换为 webpack 可以理解的 js 代码;并且 loader 支持链式调用,所以上一个 loader 的执行结果会作为下一个 loader 的入参
- 要在 loader 内部通过
return / this.callback返回转换后的结果,并且这个返回值必须是标准的 js 字符串或者 AST
- 要在 loader 内部通过
- loader 应该尽量保持单一功能原则
- webpack 默认缓存 loader 的执行结果,可以通过
this.cacheable来控制 - loader 接收三个参数
- source:对于第一个执行的 loader,这个为文件的内容,后续执行的 loader 则为前一个 loader 的执行结果
sourceMap(optional):- data (optional):其他需要在 loader 链上传递的数据
- loader 异常信息处理
- 一般尽量使用
this.emitError - 明确需要警示用户的错误,优先使用
this.emitError - 对于严重的编译错误,使用
callback
- 一般尽量使用
- loader 的
this指向的是loader-runner和loaderContext - loader 分为 pitch 和 normal 两个阶段
如何编写 plugin?
核心的点有两个:
compiler:全局的单例对象,代表了 webpack 从开启到关闭的整个生命周期,负责启动编译和监听文件compilation:每次构建的上下文对象(也就意味着每次热更新和重新编译都会重新成生一个),包含当次构建所需要的信息
那编写 plugin 时的思路和要注意的点如下:
- plugin 一定是一个函数或者包含
apply()的对象,这样才可以监听compiler对象 - 传递给插件的
compiler和compilation都是同一个引用 - 基于 tapable 来完成对 hooks 的复杂的订阅以及响应
- 监听一些具有特定意义的 hook 来影响构建
compiler.hooks.compilation:webpack 刚启动完并创建 `compilation` 对象后触发compiler.hooks.make:webpack 开始构建时触发compiler.hooks.done:webpack 完成编译时触发,此时可以通过stats对象得知编译过程中的各种信息
- 使用
schema-utils等开发工具 - 正确处理插件日志信息以及插件信息
- 使用
stats汇总插件的统计数据 - 使用
ProgressPlugin插件的reportProgress接口上报执行进度 - 通过
compilation.getLogger获取分级日志管理器 - 使用
compilation.errors/warning处理异常信息(eslint-webpack-plugin的做法)
- 使用
- 测试插件
- 通过分析 compilation.error/warn 数组来判断 webpack 是否运行成功
- 分析构建产物判断插件功能是否符合预期
什么是文件指纹?
文件指纹是指文件打包后的一连串后缀,通常有两个作用:
版本管理: 在发布版本时,通过文件指纹来区分修改的文件和未修改的文件。
使用缓存: 浏览器通过文件指纹是否改变来决定使用缓存文件还是请求新文件。
种类:
Hash:和整个项目的构建相关,只要项目有修改(compilation实例改变),Hash就会更新Contenthash:和文件的内容有关,只有内容发生改变时才会修改Chunkhash:和 webpack 构建的 chunk 有关,不同的 entry 会构建出不同的chunk (不同Chunkhash之间的变化互不影响)
如何使用:
- JS 文件:使用
Chunkhash - CSS 文件:使用
Contenthash - 图片等静态资源: 使用
hash
module.exports = {
entry: {
app: "./scr/app.js",
search: "./src/search.js",
},
output: {
filename: "[name][chunkhash:8].js",
path: __dirname + "/dist",
},
// 通过 MiniCssExtractPlugin 插件设置 css 文件指纹
plugins: [
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`,
}),
],
module: {
// 通过 rules 配置图片的文件指纹设置
// 设置file-loader的name,使用hash。
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [
{
loader: "file-loader",
options: {
name: "img/[name][hash:8].[ext]",
},
},
],
},
],
},
};什么是 source map?如何使用?
source map 是将编译打包后的代码映射回源码
可以通过 devtool 配置项来设置,或者通过
SourceMapDevToolPlugin实现更加精细粒度的控制
- 开发环境:
cheap-eval-source-map,这种source map速度最快,并且由于开发环境下没有代码压缩,所以不会影响断点调试 - 生产环境:
hidden-source-map,由于进行了代码压缩,所以并不会占用多大的体积
Tree shaking
webpack 使用 Tree shaking 的三个必要条件
- 使用 ESM 规范编写模块代码
- 配置
optimization.usedExports为true启动标记功能 - 启动代码优化功能 可以通过如下方法实现
- 配置
mode = production - 配置
optimization.minimize = true - 提供
optimization.minimizer数组
- 配置
对于使用了
babel-loader或者根据 loader 对代码进行转译的时候,注意应该关闭对于导入/导出语句的转译,因为这会影响到后续的 tree shaking比如应该将
babel-loader的babel-preset-env的modules配置为false
如何利用 Webpack 来优化项目性能
动态导入
reference:webpack 是如何实现动态导入的

