📦 webpack 中那些最易混淆的 5 个知识点

今天我主要分享的是一些 webpack 中的易混淆知识点,也是面试的常见内容。我把这些分散在文档和教程里的内容总结起来,目前看是全网独一份,大家可以加个收藏,方便以后检索和学习。
前两天为了优化公司的代码打包项目,恶补了很多 webpack4 的知识。要是放在几年前让我学习 webpack 我肯定是拒绝的,之前看过 webpack 的旧文档,比我们内部项目的文档还要简陋。
但是最近看了一下 webpack4 的文档,发现 webpack官网的 指南 写的还不错,跟着这份指南学会 webpack4 基础配置完全不是问题,想系统学习 webpack 的朋友可以看一下。
友情提示:本文章不是入门教程,不会费大量笔墨去描写 webpack 的基础配置,请读者配合教程源代码食用。
1.webpack 中,module,chunk 和 bundle 的区别是什么?
说实话我刚开始看 webpack 文档的时候,对这 3 个名词云里雾里的,感觉他们都在说打包文件,但是一会儿 chunk 一会儿 bundle 的,逐渐就迷失在细节里了,所以我们要跳出来,从宏观的角度来看这几个名词。
webpack 官网对 chunk 和 bundle 做出了解释,说实话太抽象了,我这里举个例子,给大家形象化的解释一下。
首先我们在 src 目录下写我们的业务代码,引入 index.js、utils.js、common.js 和 index.css 这 4 个文件,目录结构如下:
src/
├── index.css
├── index.html # 这个 是 HTML 模板代码
├── index.js
├── common.js
└── utils.js
index.css 写一点儿简单的样式:
body {
background-color: red;
}
utils.js 文件写个求平方的工具函数:
export function square(x) {
return x * x;
}
common.js 文件写个 log 工具函数:
module.exports = {
log: (msg) => {
console.log('hello ', msg)
}
}
index.js 文件做一些简单的修改,引入 css 文件和 common.js:
import './index.css';
const { log } = require('./common');
log('webpack');
webpack 的配置如下:
{
entry: {
index: "../src/index.js",
utils: '../src/utils.js',
},
output: {
filename: "[name].bundle.js", // 输出 index.js 和 utils.js
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 创建一个 link 标签
'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖
],
},
]
}
plugins: [
// 用 MiniCssExtractPlugin 抽离出 css 文件,以 link 标签的形式引入样式文件
new MiniCssExtractPlugin({
filename: 'index.bundle.css' // 输出的 css 文件名为 index.css
}),
]
}
我们运行一下 webpack,看一下打包的结果:

我们可以看出,index.css 和 common.js 在 index.js 中被引入,打包生成的 index.bundle.css 和 index.bundle.js 都属于 chunk 0,utils.js 因为是独立打包的,它生成的 utils.bundle.js 属于 chunk 1。
感觉还有些绕?我做了一张图,你肯定一看就懂:

看这个图就很明白了:
- 对于一份同逻辑的代码,当我们手写下一个一个的文件,它们无论是 ESM 还是 commonJS 或是 AMD,他们都是 module ;
- 当我们写的 module 源文件传到 webpack 进行打包时,webpack 会根据文件引用关系生成 chunk 文件,webpack 会对这个 chunk 文件进行一些操作;
- webpack 处理好 chunk 文件后,最后会输出 bundle 文件,这个 bundle 文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。
一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js;但也有例外,比如说上图中,我就用 MiniCssExtractPlugin 从 chunks 0 中抽离出了 index.bundle.css 文件。
1.1 一句话总结:
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
2.filename 和 chunkFilename 的区别
2.1 filename
filename 是一个很常见的配置,就是对应于 entry 里面的输入文件,经过webpack 打包后输出文件的文件名。比如说经过下面的配置,生成出来的文件名为 index.min.js。
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
}
}

2.2 chunkFilename
chunkFilename 指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。一般来说,这个 chunk 文件指的就是要懒加载的代码。
比如说我们业务代码中写了一份懒加载 lodash 的代码:
// 文件:index.js
// 创建一个 button
let btn = document.createElement("button");
btn.innerHTML = "click me";
document.body.appendChild(btn);
// 异步加载代码
async function getAsyncComponent() {
var element = document.createElement('div');
const { default: _ } = await import('lodash');
element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
return element;
}
// 点击 button 时,懒加载 lodash,在网页上显示 Hello! dynamic imports async
btn.addEventListener('click', () => {
getAsyncComponent().then(component => {
document.body.appendChild(component);
})
})
我们的 webpack 不做任何配置,还是原来的配置代码:
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
}
}
这时候的打包结果如下:

这个 1.min.js 就是异步加载的 chunk 文件。文档里这么解释:
output.chunkFilename默认使用[id].js或从output.filename中推断出的值([name]会被预先替换为[id]或[id].)
文档写的太抽象,我们不如结合上面的例子来看:
output.filename 的输出文件名是 [name].min.js,[name] 根据 entry 的配置推断为 index,所以输出为 index.min.js;
由于 output.chunkFilename 没有显示指定,就会把 [name] 替换为 chunk 文件的 id 号,这里文件的 id 号是 1,所以文件名就是 1.min.js。
如果我们显式配置 chunkFilename,就会按配置的名字生成文件:
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
chunkFilename: 'bundle.js', // bundle.js
}
}

2.3 一句话总结:
filename 指列在 entry 中,打包后输出的文件的名称。
chunkFilename 指未列在 entry 中,却又需要被打包出来的文件的名称。
3.webpackPrefetch、webpackPreload 和 webpackChunkName 到底是干什么的?
这几个名词其实都是 webpack 魔法注释(magic comments)里的,文档中说了 6 个配置,配置都可以组合起来用。我们说说最常用的三个配置。
3.1 webpackChunkName
前面举了个异步加载 lodash 的例子,我们最后把 output.chunkFilename 写死成 bundle.js。在我们的业务代码中,不可能只异步加载一个文件,所以写死肯定是不行的,但是写成 [name].bundle.js 时,打包的文件又是意义不明、辨识度不高的 chunk id。
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
chunkFilename: '[name].bundle.js', // 1.bundle.js,chunk id 为 1,辨识度不高
}
}

这时候 webpackChunkName 就可以派上用场了。我们可以在 import 文件时,在 import 里以注释的形式为 chunk 文件取别名:
async function getAsyncComponent() {
var element = document.createElement('div');
// 在 import 的括号里 加注释 /* webpackChunkName: "lodash" */ ,为引入的文件取别名
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
return element;
}
这时候打包生成的文件是这样的:

现在问题来了,lodash 是我们取的名字,按道理来说应该生成 lodash.bundle.js 啊,前面的 vendors~ 是什么玩意?
其实 webpack 懒加载是用内置的一个插件 SplitChunksPlugin 实现的,这个插件里面有些默认配置项,比如说 automaticNameDelimiter,默认的分割符就是 ~,所以最后的文件名才会出现这个符号,这块儿内容我就不引申了,感兴趣的同学可以自己研究一下。
3.2 webpackPrefetch 和 webpackPreload
这两个配置一个叫预拉取(Preload),一个叫预加载(Prefetch),两者有些细微的不同,我们先说说 webpackPreload。
在上面的懒加载代码里,我们是点击按钮时,才会触发异步加载 lodash 的动作,这时候会动态的生成一个 script 标签,加载到 head 头里:

如果我们 import 的时候添加 webpackPrefetch:
...
const { default: _ } = await import(/* webpackChunkName: "lodash" */ /* webpackPrefetch: true */ 'lodash');
...
就会以 <link rel="prefetch" as="script"> 的形式预拉取 lodash 代码:

这个异步加载的代码不需要手动点击 button 触发,webpack 会在父 chunk 完成加载后,闲时加载 lodash 文件。
webpackPreload 是预加载当前导航下可能需要资源,他和 webpackPrefetch 的主要区别是:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻
3.3 一句话总结:
webpackChunkName 是为预加载的文件取别名,webpackPreload 会在浏览器闲置下载文件,webpackPreload 会在父 chunk 加载时并行下载文件。
4.hash、chunkhash、contenthash 有什么不同?
首先来个背景介绍,哈希一般是结合 CDN 缓存来使用的。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的 HTML 引用的 URL 地址也会改变,触发 CDN 服务器从源服务器上拉取对应数据,进而更新本地缓存。
4.1 hash
hash 计算是跟整个项目的构建相关,我们做一个简单的 demo。
沿用案例 1 的 demo 代码,文件目录如下:
src/
