webpack 笔记
有哪些打包工具?
远古时代
- browserify
- Grunt
传统
- Gulp
- Parcel
- Webpack ✔
- Rollup
现代
- Vite
- Esbuild
- Rspack
- ……
第一章:走进 webpack
一、介绍
1. 是什么?
webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。
- 现代的 modern:因为现代前端开发面临各种各样的问题,才催生了 webpack 的出现和发展。
- 静态的 static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器)。
- 模块化 module:webpack 默认支持各种模块化开发,ES Module、CommonJS、AMD 等。
- 打包 bundler:webpack 可以将帮助我们进行打包,所以它是一个打包工具。

中文文档:
世界观:webpack 是基于模块化的打包(构建)工具,它把一切视为模块。
它通过一个开发时态的入口模块为起点,分析出所有的依赖关系,然后经过一系列的过程(压缩、合并 ……),最终生成运行时态的文件。
2. 安装
# 全局安装
npm install webpack webpack-cli -g
# 局部安装 (推荐)
npm install webpack webpack-cli -D3. 快速入门
在项目根路径下直接执行 webpack 命令。
发现生成了一个 dist 文件夹,里面存放了一个 main.js 的文件,就是我们打包之后的文件。这个文件中的代码被压缩和丑化了。
另外还发现代码中依然存在 ES6 的语法,比如箭头函数、 const 等,这是因为默认情况下 webpack 并不清楚打包后的文件是否需要转成 ES5 之前的语法,后续我们需要通过 babel 来进行转换和设置。
但是有一个问题,webpack 是如何确定我们的入口的呢?
事实上,当我们运行 webpack 时, webpack 会查找当前目录 src 文件夹下的 index.js 作为入口。所以,如果当前项目中没有存在 src 下的 index.js 文件,那么会报错。当然,我们也可以通过配置来指定入口和出口。
npx webpack --entry ./src/main.js --output-path ./build --mode=development一般会使用 npm script 来简化命令。
4. 模块兼容性
由于 webpack 同时支持 CommonJS 和 ES6 module,因此需要理解它们互操作时 webpack 是如何处理的。
1)同模块化标准
如果导出和导入使用的是同一种模块化标准,打包后的效果和之前学习的模块化没有任何差异。
2)不同模块化标准
不同的模块化标准,webpack 按照如下的方式处理。
ES6 导出,用 cjs 导入:ESM 导出的不管什么东西,使用 cjs 导入都是对象。
例如:就算 ESM 只有默认导出,用 cjs 导入也是对象,只不过属性只有
{default: xxx}。
cjs 导出,ES6 导入:cjs 导出是什么,ESM 默认和整体导入也是什么。
例如:cjs 导出是对象,ESM 默认和整体导入也是对象;cjs 导出是函数,ESM 默认和整体导入也是函数。

二、原理探究
1. 编译结果分析
console.log("index module")
var a = require("./a")
a.abc()
console.log(a)console.log("module a")
module.exports = "a"// 合并两个模块
// ./src/a.js
// ./src/index.js
(function (modules) {
var moduleExports = {}; // 用于缓存模块的导出结果
// require函数相当于是运行一个模块,得到模块导出结果
function __webpack_require(moduleId) { // moduleId就是模块的路径
if (moduleExports[moduleId]) {
// 检查是否有缓存
return moduleExports[moduleId];
}
var func = modules[moduleId]; // 得到该模块对应的函数
var module = {
exports: {}
}
func(module, module.exports, __webpack_require); // 运行模块
var result = module.exports; // 得到模块导出的结果
moduleExports[moduleId] = result; // 缓存起来
return result;
}
// 执行入口模块
return __webpack_require("./src/index.js"); // require函数相当于是运行一个模块,得到模块导出结果
})({ // 该对象保存了所有的模块,以及模块对应的代码
"./src/a.js": function (module, exports) {
eval("console.log(\"module a\")\nmodule.exports = \"a\";\n //# sourceURL=webpack:///./src/a.js")
},
"./src/index.js": function (module, exports, __webpack_require) {
eval("console.log(\"index module\")\nvar a = __webpack_require(\"./src/a.js\")\na.abc();\nconsole.log(a)\n //# sourceURL=webpack:///./src/index.js")
}
});2. webpack 编译过程
webpack 的作用是将源代码编译(构建、打包)成最终代码。
整个过程大致分为三个步骤:
- 初始化
- 编译
- 输出

1)初始化
此阶段,webpack 会将 CLI 参数、配置文件、默认配置进行融合,形成一个最终的配置对象。
对配置的处理过程是依托一个第三方库 yargs 完成的。
此阶段相对比较简单,主要是为接下来的编译阶段做必要的准备。
目前,可以简单的理解为,初始化阶段主要用于产生一个最终的配置。
2)编译
[1] 创建 chunk
chunk 是 webpack 在内部构建过程中的一个概念,译为块,它表示通过某个入口找到的所有依赖的统称。
根据入口模块(默认为 ./src/index.js)创建一个 chunk。

每个 chunk 都有至少两个属性:
- name:默认为 main。
- id:唯一编号,开发环境和 name 相同,生产环境是一个数字,从 0 开始。
[2] 构建所有依赖模块

AST 在线测试工具:https://astexplorer.net/
简图

[3] 产生 chunk assets
在第二步完成后,chunk 中会产生一个模块列表,列表中包含了模块 id 和 模块转换后的代码。
接下来,webpack 会根据配置为 chunk 生成一个资源列表,即 chunk assets,资源列表可以理解为是生成到最终文件的文件名和文件内容。

chunk hash 是根据所有 chunk assets 的内容生成的一个 hash 字符串。
hash:一种算法,具体有很多分类,特点是将一个任意长度的字符串转换为一个固定长度的字符串,而且可以保证原始内容不变,产生的 hash 字符串就不变。
简图

[4] 合并 chunk assets
将多个 chunk 的 assets 合并到一起,并产生一个总的 hash。

3)输出
此步骤非常简单,webpack 将利用 node 中的 fs 模块(文件处理模块),根据编译产生的总的 assets,生成相应的文件。

总过程


涉及术语
- module:模块,分割的代码单元,webpack 中的模块可以是任何内容的文件,不仅限于 JS。
- chunk:webpack 内部构建模块的块,一个 chunk 中包含多个模块,这些模块是从入口模块通过依赖分析得来的。
- bundle:chunk 构建好模块后会生成 chunk 的资源清单,清单中的每一项就是一个 bundle,可以认为 bundle 就是最终生成的文件。
- hash:最终的资源清单所有内容联合生成的 hash 值。
- chunkhash:chunk 生成的资源清单内容联合生成的 hash 值。
- chunkname:chunk 的名称,如果没有配置则使用 main。
- id:通常指 chunk 的唯一编号,如果在开发环境下构建,和 chunkname 相同;如果是生产环境下构建,则使用一个从 0 开始的数字进行编号。

3. loader 原理
1)实现原理
webpack 做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。更多的功能需要借助 webpack loaders 和 webpack plugins 完成。
webpack loader:loader 本质上是一个函数,它的作用是将某个源码字符串转换成另一个源码字符串返回。

loader 函数将在模块解析的过程中被调用,以得到最终的源码。
chunk 中解析模块的更详细流程

处理 loaders 流程

loader 执行顺序是从下到上,从右到左。
loader 配置
module.exports = {
module: { // 针对模块的配置,目前版本只有两个配置,rules、noParse
rules: [ // 模块匹配规则,可以存在多个规则
{ // 每个规则是一个对象
test: /\.js$/, // 匹配的模块正则
use: [ // 匹配到后应用的规则模块
{ // 其中一个规则
loader: "模块路径", // loader模块的路径,该字符串会被放置到require中
options: { // 向对应loader传递的额外参数, 也支持: 模块路径?param
}
}
]
}
]
}
}module.exports = {
module: { // 针对模块的配置,目前版本只有两个配置,rules、noParse
rules: [ // 模块匹配规则,可以存在多个规则
{ // 每个规则是一个对象
test: /\.js$/, // 匹配的模块正则
use: ["模块路径2", "模块路径1"] // loader模块的路径,该字符串会被放置到require中
}
]
}
}2)编写 loader
[1] 处理样式
1️⃣ 编写 loader
module.exports = function (sourceCode) {
var code = `
var style = document.createElement("style");
style.innerHTML = \`${sourceCode}\`;
document.head.appendChild(style);
module.exports = \`${sourceCode}\`
`;
return code;
}2️⃣ 使用 loader
module.exports = {
mode: "development",
devtool: "source-map",
module: {
rules: [{
test: /\.css$/,
use: ["./loaders/style-loader"]
}]
}
}3️⃣ 加入到依赖图
var content = require("./assets/webpack/index.css")
console.log(content); // css的源码字符串4️⃣ 编写需要处理的资源
body {
background: #333;
color: #fff;
}[2] 处理图片
1️⃣ 编写处理图片的 loader
var loaderUtil = require("loader-utils")
function loader(buffer) { // 给的是buffer
console.log("文件数据大小:(字节)", buffer.byteLength);
var { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
if (buffer.byteLength >= limit) {
var content = getFilePath.call(this, buffer, filename);
} else {
var content = getBase64(buffer)
}
return `module.exports = \`${content}\``;
}
loader.raw = true; // 该loader要处理的是原始数据
module.exports = loader;
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
function getFilePath(buffer, name) {
var filename = loaderUtil.interpolateName(this, name, { // interpolateName 用于生成文件名
content: buffer
});
this.emitFile(filename, buffer);
return filename;
}2️⃣ 配置使用 loader
module.exports = {
mode: "development",
devtool: "source-map",
module: {
rules: [
{
test: /\.(png)|(jpg)|(gif)$/,
use: [{
loader: "./loaders/img-loader.js",
options: {
limit: 3000, // 3000字节以上使用图片,3000字节以内使用base64
filename: "img-[contenthash:5].[ext]"
}
}]
}
]
}
}3️⃣ 编写代码
var src = require("./assets/webpack/webpack.png")
console.log(src);
var img = document.createElement("img")
img.src = src;
document.body.appendChild(img);4. plugin 原理
1)是什么
loader 的功能定位是转换代码,而一些其他的操作难以使用 loader 完成。比如:
- 当 webpack 生成文件时,顺便多生成一个说明描述文件。
- 当 webpack 编译启动时,控制台输出一句话表示 webpack 启动了。
- 当 xxxx 时,xxxx。
这种类似的功能需要把功能嵌入到 webpack 的编译流程中,而这种事情的实现是依托于 plugin 的。

2)原理
plugin 的本质是一个带有 apply 方法的对象。
var plugin = {
apply: function(compiler){
}
}通常,习惯上,我们会将该对象写成构造函数的模式。
class MyPlugin {
apply(compiler){
}
}
var plugin = new MyPlugin();要将插件应用到 webpack,需要把插件对象配置到 webpack 的 plugins 数组中,如下:
module.exports = {
plugins:[
new MyPlugin()
]
}apply 函数会在初始化阶段,创建好 Compiler 对象后运行。
compiler 对象是在初始化阶段构建的,整个 webpack 打包期间只有一个 compiler 对象,后续完成打包工作的是 compiler 对象内部创建的 compilation。
apply 方法会在创建好 compiler 对象后调用,并向方法传入一个 compiler 对象。

compiler 对象提供了大量的钩子函数(hooks,可以理解为事件),plugin 的开发者可以注册这些钩子函数,参与 webpack 编译和生成。可以在 apply 方法中使用下面的代码注册钩子函数:
class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name, function(compilation){
// 事件处理函数
})
}
}事件名称
即要监听的事件名,即钩子名,所有的钩子:https://www.webpackjs.com/api/compiler-hooks
事件类型
这一部分使用的是 Tapable API,这个小型的库是一个专门用于钩子函数监听的库。
它提供了一些事件类型:
- tap:注册一个同步的钩子函数,函数运行完毕则表示事件处理结束。
- tapAsync:注册一个基于回调的异步的钩子函数,函数通过调用一个回调表示事件处理结束。
- tapPromise:注册一个基于 Promise 的异步的钩子函数,函数通过返回的 Promise 进入已决状态表示事件处理结束。
处理函数
处理函数有一个事件参数 compilation。
3)实战
需求:编译完成后,添加文件列表。
1️⃣ 编写插件
module.exports = class FileListPlugin {
constructor(filename = "filelist.txt"){
this.filename = filename;
}
apply(compiler) {
// emit 是生成资源到 output 目录之前
compiler.hooks.emit.tap("FileListPlugin", complation => {
var fileList = [];
for (const key in complation.assets) {
var content = `【${key}】大小:${complation.assets[key].size()/1000}KB`;
fileList.push(content);
}
var str = fileList.join("\n\n");
complation.assets[this.filename] = {
source() {
return str // 生成的文件内容
},
size() {
return str.length; // 生成的文件大小
}
}
})
}
}2️⃣ 使用插件
var FileListPlugin = require("./plugins/FileListPlugin")
module.exports = {
mode: "development",
devtool: "source-map",
plugins: [
new FileListPlugin("文件列表.md")
]
}三、区分环境
1. 知识
有些时候,我们需要针对生产环境和开发环境分别书写 webpack 配置。
为了更好的适应这种要求,webpack 允许配置不仅可以是一个对象,还可以是一个函数。
module.exports = env => {
return {
// 配置内容
}
}在开始构建时,webpack 如果发现配置是一个函数,会调用该函数,将函数返回的对象作为配置内容,因此,开发者可以根据不同的环境返回不同的对象。
在调用 webpack 函数时,webpack 会向函数传入一个参数 env,该参数的值来自于 webpack 命令中给 env 指定的值,例如:
npx webpack --env abc # env: "abc"
npx webpack --env.abc # env: {abc:true}
npx webpack --env.abc=1 # env: {abc:1}
npx webpack --env.abc=1 --env.bcd=2 # env: {abc:1, bcd:2}这样一来,我们就可以在命令中指定环境,在代码中进行判断,根据环境返回不同的配置结果。
2. 实战
1)先写好配置文件
module.exports = {
entry: "./src/index.js",
output: {
filename: "scripts/[name]-[hash:5].js"
}
}module.exports = {
mode: "development",
devtool: "source-map"
}module.exports = {
mode: "production",
devtool: "none"
}2)合并到主配置中
var baseConfig = require("./webpack.base")
var devConfig = require("./webpack.dev")
var proConfig = require("./webpack.pro")
module.exports = function (env) {
if (env && env.prod) {
return {
...baseConfig,
...proConfig
}
} else {
return {
...baseConfig,
...devConfig
}
}
}第二章:webpack 配置文件
一、5 大核心概念
1)entry(入口)
指示 webpack 从哪个文件开始打包。
2)output(输出)
指示 webpack 打包完的文件输出到哪里去,如何命名等。
3)loader(加载器)
webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,webpack 才能解析。
4)plugins(插件)
扩展 webpack 的功能。
5)mode(模式)
主要有两种模式:
开发模式:development
生产模式:production
二、准备配置文件
在通常情况下,webpack 需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的。
因此,可以在根目录下创建一个 webpack.config.js 文件,来作为 webpack 的配置文件。webpack 默认会查找项目根目录下的 webpack.config.js 文件作为配置文件。以下是一些常见的配置选项:
- context:设置 webpack 的基本目录,用于从配置中解析入口起点 (entry point) 和加载器 (loader) 。
- mode:设置构建模式,可以是 development、production 或 none。不同的模式会启用不同的内置优化。
- target:设置编译环境,例如 web、node 等。
- entry:指定 webpack 的入口点。这通常是你的应用程序的主文件,例如 main.js。
- output:指定 webpack 输出的文件名和路径。它是一个对象,包含 path 和 filename 属性。
- module:用于配置加载器(loader)。加载器可以将所有类型的文件转换为 webpack 可以处理的模块。
- plugins:用于配置插件。插件可以执行各种各样的任务,如清理构建目录、压缩代码、生成 HTML 文件等。
- resolve:用于配置模块解析的方式。例如,可以设置文件扩展名的解析顺序,别名等。
- devServer:配置 webpack-dev-server,一个提供了热模块替换等功能的开发服务器。
- devtool:选择一种 source map 格式以增强调试过程。
- optimization:用于配置优化选项,例如代码拆分、压缩等。
基本的配置文件如下:
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口文件
output: { // 输出
path: path.resolve(__dirname, './build'), // 输出路径
filename: 'bundle.js', // 输出文件名
// filename: "static/js/main.js", // 将 js 文件输出到 ./build/static/js 目录中
clean: true // 自动将上次打包目录资源清空
},
module: { // 模块,定义了对模块的处理逻辑
rules: [
{
test: /\.css$/, // 对所有 .css 文件使用 style-loader 和 css-loader
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
// 使用插件
],
mode: 'development' // 'production' 用于生产模式
};如果配置文件不在项目的根目录,或者有多个配置文件,或者设置的名字不是 webpack.config.js,那么就可以使用 --config 选项来指定配置文件的位置与名字,例如:
webpack --config ./path/to/your/wk.config.js上面命令太长了,可以使用 package.json 中的 scripts 简化命令。用 npm run xxx 来代替上面的命令。一般都是 npm run build。

三、配置项解释
1. context
context 是 webpack 配置中的一个属性,它用于设置 webpack 的基本目录。这个基本目录是 webpack 用来解析配置中的入口起点(entry point)和加载器(loader)的路径的。
例如,如果项目结构如下:
/my-project
|-- /src
| |-- index.js
|-- /node_modules
|-- webpack.config.jswebpack.config.js 文件可能如下:
module.exports = {
context: __dirname, // 当前目录
entry: './src/index.js',
// ...
};context 被设置为 __dirname,这是一个 Node.js 全局变量,表示当前文件的目录。因此,entry 的路径 ./src/index.js 将从当前目录(即 webpack.config.js 所在的目录)开始解析。
如果你没有设置 context,那么 webpack 会默认使用当前工作目录(即你运行 webpack 命令的目录)作为基本目录。
2. 模式(Mode)
只需在配置对象(webpack.config.js)中提供 mode 选项:
module.exports = {
mode: 'development',
};支持以下字符串值:
| 选项 | 描述 |
|---|---|
| development | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development。为模块和 chunk 启用有效的名。 |
| production | 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称。 自动应用 TerserPlugin …… 插件。 |
| none | 不使用任何默认优化选项。 |
如果没有设置,webpack 会给 mode 的默认值设置为 production。
webpack 给每种模式都预设了一些配置,所以在使用 webpack 时,最好都写上 mode。


小贴士:在 node 中,模块化代码,比如
require("./"),表示当前 js 文件所在的目录;在路径处理中,"./"表示 node 运行目录。
1)快速入门
var path = require("path")
module.exports = {
mode: "production",
entry: {
main: "./src/index.js", // 属性名: chunk的名称、属性值: 入口模块(启动模块)
a: ["./src/a.js", "./src/index.js"] // 启动模块有两个
},
output: {
path: path.resolve(__dirname, "target"), // 必须配置一个绝对路径, 表示资源放置的文件夹, 默认是dist
filename: "[id].[chunkhash:5].js" // 配置的合并的js文件的规则
},
devtool: "source-map"
}
// -------------------------------------------------------------------------------------------------
/*
开发中常见, 默认等价于:
entry: {
main: "./src/index.js",
},
*/
entry: "./src/index.js"2)具体配置
webpack 多入口配置主要通过修改 entry 字段实现。以下是几种常见方式:
1. 对象语法 (最常用)
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};2. 数组语法 (合并多个文件到一个入口)
module.exports = {
entry: {
app: ['./src/polyfills.js', './src/app.js'],
vendor: ['react', 'react-dom']
}
};3. 动态入口 (函数形式)
module.exports = {
entry: () => ({
app: './src/app.js',
admin: './src/admin.js'
})
};解释:
[name]占位符对应 entry 的键名。
3)Entry Dependencies (入口依赖)
假如多个入口文件都有相同的依赖,如 axios,打包后的多个入口文件中都有 axios 代码,怎么优化?
主要配置选项
dependOn 声明依赖
module.exports = {
entry: {
app: {
import: './src/app.js',
dependOn: 'shared' // app 依赖于 shared 入口
},
shared: ['react', 'react-dom', 'lodash']
}
};复杂场景示例
module.exports = {
entry: {
// 主应用
app: {
import: './src/app.js',
dependOn: ['react-vendors', 'shared-utils'],
filename: 'app.[contenthash].js'
},
// 管理后台
admin: {
import: './src/admin.js',
dependOn: ['react-vendors', 'shared-utils'],
filename: 'admin.[contenthash].js'
},
// React相关依赖
'react-vendors': {
import: ['react', 'react-dom'],
runtime: 'runtime-react'
},
// 工具库
'shared-utils': {
import: ['lodash', 'axios'],
runtime: 'runtime-utils'
}
},
output: {
path: path.resolve(__dirname, 'dist'),
clean: true
}
};4)出口
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js', // 打包后生成文件的名字
chunkFilename: '[name].[contenthash].chunk.js' // 对非入口的打包文件进行命名,也就是被 Webpack 额外切割出来的 chunk 或动态依赖
}
};如何配置动态依赖的 chunk 名字?
// 当用户点击按钮时,动态加载 math.js
button.onclick = () => {
import(/* webpackChunkName: "mathTools" */ './math.js').then(math => {
console.log(math.add(1, 2));
});
};最终打包生成的文件名:mathTools.e3b0c442.chunk.js
4. resolve
resolve 可以帮助 webpack 从每个 require/import 语句中,找到需要引入到合适的模块代码。webpack 使用 enhanced resolve (Node.js 模块) 来解析文件路径。
webpack 能解析三种文件路径
绝对路径:由于已经获得文件的绝对路径,因此不需要再做进一步解析。
相对路径:在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
模块路径:在 resolve.modules 中指定的所有目录检索模块。默认值是 ['node_modules '],所以默认会从 node_modules 中查找文件。
确定文件还是文件夹
如果是一个文件:
- 如果文件具有扩展名,则直接打包文件。否则,将使用 resolve.extensions 选项作为文件扩展名解析。
如果是一个文件夹:
- 会在文件夹中根据 resolve.mainFiles 配置选项中指定的文件顺序查找。resolve.mainFiles 的默认值是 ['index']。再根据 resolve.extensions 来解析扩展名。
1)extensions
extensions:这个配置项是一个数组,用于指定 webpack 在解析模块时应该自动添加哪些文件扩展名。例如,如果设置 extensions 为 ['.js', '.jsx'],那么在导入文件时,可以省略 js 和 jsx 扩展名。如下:
module.exports = {
// ...
resolve: {
extensions: ['.js', '.jsx']
}
};就可以像这样导入模块:
import MyComponent from './MyComponent'; // 实际上是 './MyComponent.jsx'2)mainFiles
resolve.mainFiles 是一个数组,它用于描述当模块的路径指向一个目录时,webpack 应该尝试哪些文件名来解析模块。换句话说,当你尝试导入一个目录(而不是具体的文件)时,webpack 会自动尝试这个目录下的哪些文件作为模块的入口。
module.exports = {
// ...
resolve: {
mainFiles: ['index', 'default']
}
};在这个例子中,如果你尝试导入一个目录,比如 import Foo from './Foo',webpack 会首先尝试解析 ./Foo/index.js,如果这个文件不存在,它会尝试解析 ./Foo/default.js。
注意:resolve.mainFiles 的默认值是 ['index'],所以如果你没有提供这个配置项,webpack 会默认尝试解析 index.js。
3)alias 别名
在 webpack 中,可以使用 resolve.alias 选项来创建别名,来更方便地导入某些模块。例如,想要避免在代码中使用相对路径。
const path = require('path');
module.exports = {
// ...
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/'),
// '@': path.resolve(__dirname, 'src'), // 作用跟上面一样
},
},
// ...
};上面的例子中,创建了一个别名 '@',它指向了 'src/' 目录。这意味着,在代码中可以使用 '@/' 来替代 'src/'。例如,可以这样导入一个模块:
import MyComponent from '@/components/MyComponent';这将导入 'src/components/MyComponent.js' 文件(假设已经配置了文件扩展名的解析顺序)。
注意:path.resolve(__dirname, 'src/') 会生成一个绝对路径,这是必要的,因为 webpack 需要绝对路径来准确地解析文件。
4)modules
告诉 webpack 在解析模块时应搜索哪些目录。
module.exports = {
// ...
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules']
}
};当 webpack 在解析模块时,首先会在项目根目录下的 src 文件夹中查找,如果没有找到,再在 node_modules 文件夹中查找。
5. output
1)library
library: "abc"这样一来,打包后的结果中,会将自执行函数的执行结果暴露给 abc。
2)libraryTarget
libraryTarget: "var"该配置可以更加精细的控制如何暴露入口包的导出结果。
其他可用的值有:
- var:默认值,暴露给一个普通变量。
- window:暴露给 window 对象的一个属性。
- this:暴露给 this 的一个属性。
- global:暴露给 global 的一个属性。
- commonjs:暴露给 exports 的一个属性。
- 其他:https://www.webpackjs.com/configuration/output/#output-librarytarget
6. target
target:"web" // 默认值设置打包结果最终要运行的环境,常用值有:
- web:打包后的代码运行在 web 环境中。
- node:打包后的代码运行在 node 环境中。
- 其他:https://www.webpackjs.com/configuration/target/
7. module.noParse
noParse: /jquery/不解析正则表达式匹配的模块,通常用它来忽略那些大型的单模块库,以提高构建性能。
8. externals
externals: {
jquery: "$",
lodash: "_"
}从最终的 bundle 中排除掉配置的源码,例如,入口模块代码:
// index.js
require("jquery")
require("lodash")生成的 bundle 是:
(function(){
...
})({
"./src/index.js": function(module, exports, __webpack_require__){
__webpack_require__("jquery")
__webpack_require__("lodash")
},
"jquery": function(module, exports){
// jquery的大量源码
},
"lodash": function(module, exports){
// lodash的大量源码
},
})但有了上面的配置后,则变成了
(function(){
...
})({
"./src/index.js": function(module, exports, __webpack_require__){
__webpack_require__("jquery")
__webpack_require__("lodash")
},
"jquery": function(module, exports){
module.exports = $;
},
"lodash": function(module, exports){
module.exports = _;
},
})这比较适用于一些第三方库来自于外部 CDN 的情况,这样一来,即可以在页面中使用 CDN,又让 bundle 的体积变得更小,还不影响源码的编写。
9. stats
stats 控制的是构建过程中控制台的输出内容。
10. 热更新
1)webpack-dev-server
[1] 使用
webpack-dev-server (webpack 官方出品) 是一个用于开发环境的简单的 HTTP 服务器,它提供了实时重载(live reloading)功能。这意味着当修改项目文件时,webpack 将重新编译代码,然后 webpack-dev-server 将自动刷新浏览器显示新的结果。
首先,需要安装 webpack-dev-server。使用 npm 来安装:
npm install --save-dev webpack-dev-server然后,在 webpack.config.js 文件中,添加一个 devServer 配置项(可选):
module.exports = {
// ...
devServer: {
host: "localhost", // 启动服务器域名
compress: true,
port: 9000,
open: true,
index: "index.html",
proxy: {
// 代理规则
'/api': {
target: 'https://api.yxts.com',
changeOrigin: true, //更改请求头中的host和origin
},
},
stats: {
modules: false,
colors: true,
},
},
// ...
};解释:
- compress:启用 gzip 压缩。
- port:指定了要监听请求的端口号。
- open:是否打开浏览器。默认值是 false,设置为 true 会打开浏览器。也可以设置为类似于 Google Chrome 等值。
- proxy:配置代理,常用于跨域访问。
- stats:配置控制台输出内容。
然后,在 package.json 文件的 scripts 部分添加一个脚本来启动 webpack-dev-server:
"scripts": {
"start": "webpack-dev-server --open",
// 或者下面的命令
// "serve": "webpack serve"
// ...
}现在,就可以运行 npm start(或 yarn start)来启动服务器,然后在浏览器中打开 http://localhost:9000 来查看项目。
注意运行指令发生了变化
并且当你使用开发服务器时,所有代码都会在内存中编译打包,并不会输出到 dist 目录下。
开发时我们只关心代码能运行,有效果即可,至于代码被编译成什么样子,我们并不需要知道。
更多的配置选项和详细的使用方法,可以参考 webpack-dev-server 的官方文档。
[2] 原理
当执行 webpack-dev-server 命令后,它做了以下操作:
- 内部执行 webpack 命令,传递命令参数。
- 开启 watch。
- 注册 hooks:类似于plugin,webpack-dev-server 会向 webpack 中注册一些钩子函数,主要功能如下:
- 将资源列表(aseets)保存起来
- 禁止 webpack 输出文件
- 用 express 开启一个服务器,监听某个端口,当请求到达后,根据请求的路径,给予相应的资源内容。
2)模块热替换
模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,如果模块发生替换、添加或删除,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
要使用 HMR,需要做以下几步:
1)安装 webpack-dev-server
如果还没有安装 webpack-dev-server,需要首先安装它:
npm install --save-dev webpack-dev-server
# or
yarn add webpack-dev-server --dev2)更新 webpack 配置(可选)
在 webpack.config.js 文件中,需要添加 devServer 配置,并设置 hot 为 true:
const webpack = require('webpack');
module.exports = {
// ...
devServer: {
hot: true,
// ...
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
// ...其他插件
],
// ...
};这里的 new webpack.HotModuleReplacementPlugin() 是启用 HMR 的插件(Webpack 新版默认开启了 HMR,可以省略此步骤)。
3)在应用中接受更新的模块
现在,需要在应用中接受更新的模块。使用 module.hot.accept 方法来实现这一点。
例如,如果有一个模块 print.js,可以在入口文件 index.js 中这样做:
import printMe from './print.js';
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
printMe();
})
}这样,当 print.js 模块更新时,它会被重新获取,并且 printMe 函数将被重新执行。
这就是使用 HMR 的基本步骤。更多的信息和详细的使用方法,可以参考 webpack 的官方文档。
四、管理配置文件
webpack-merge 是一个用于合并多个 webpack 配置对象的工具。使用 webpack-merge 可以更方便地管理和组织 webpack 配置,特别是当有多个环境(例如开发环境和生产环境)需要不同配置时。
1)安装 webpack-merge。
npm install --save-dev webpack-merge2)在项目根路径下有一个 config 目录。使用 webpack-merge 管理下面的文件。
webpack.common.conf.js
webpack.dev.conf.js
webpack.prod.conf.js3)在 webpack.common.js 内容。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
// 其他通用配置 ...
};4)在 webpack.dev.conf.js 文件中使用 webpack-merge。
const { merge } = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
mode: 'development',
entry: './src/main.js',
devServer: {
compress: true,
port: 9000,
open: true,
},
// 其他开发环境特定的配置...
};
module.exports = merge(commonConfig, devConfig);需要注意,webpack-merge 在合并配置对象时,如果遇到同名的属性,它将使用后面的配置覆盖前面的配置。这意味着,如果在 devConfig 中定义了和 commonConfig 中相同的属性,那么 devConfig 中的属性将会覆盖 commonConfig 中的属性。
5)在 npm scripts 中,使用合并后的配置来启动 webpack:
"scripts": {
"start": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
}为什么 entry 的值不用修改路径还没问题?
在 webpack 配置中,context 选项用于设置 webpack 运行环境的基本目录,这个目录是一个绝对路径。entry 选项则是相对于这个 context 路径的。如果没有明确设置 context,默认是 Node.js 进程当前的工作目录(即 process.cwd())。
例如,webpack 配置文件位于项目根目录的 /config/webpack.config.js 下:
module.exports = {
context: path.resolve(__dirname, 'app'),
entry: './src/main.js',
// ... 其他配置
};那么,entry 实际上指向的就是 /config/app/src/main.js。
第三章:加载器
webpack 本身只能理解 JavaScript 和 JSON 文件。加载器使 webpack 能够处理其他类型的文件,并将它们转换为有效的模块,可以添加到依赖图中。
一、样式资源
1. webpack 处理 CSS
要拆分 css,就必须把 css 当成像 js 那样的模块;要把 css 当成模块,就必须有一个构建工具(webpack),它具备合并代码的能力。
而 webpack 本身只能读取 css 文件的内容、将其当作 JS 代码进行分析,因此,会导致错误。
于是,就必须有一个 loader,能够将 css 代码转换为 js 代码。
1)css-loader 原理
css-loader 的作用,就是将 css 代码转换为 js 代码。
它的处理原理极其简单:将 css 代码作为字符串导出。
例如:
.red {
color: "#f40";
}经过 css-loader 转换后变成 js 代码:
module.exports = `
.red {
color:"#f40";
}
`;上面的 js 代码是经过我简化后的,不代表真实的 css-loader 的转换后代码,css-loader 转换后的代码会有些复杂,同时会导出更多的信息,但核心思想不变。
再例如:
.red {
color: "#f40";
background: url("./bg.png")
}经过 css-loader 转换后变成 js 代码:
var import1 = require("./bg.png");
module.exports = `
.red {
color: "#f40";
background: url("${import1}")
}
`;这样一来,经过 webpack 的后续处理,会把依赖 ./bg.png 添加到模块列表,然后再将代码转换为
var import1 = __webpack_require__("./src/bg.png");
module.exports = `
.red {
color: "#f40";
background: url("${import1}")
}
`;再例如:
@import "./reset.css";
.red {
color: "#f40";
background: url("./bg.png")
}会转换为:
var import1 = require("./reset.css");
var import2 = require("./bg.png");
module.exports = `
${import1}
.red{
color: "#f40";
background: url("${import2}")
}
`;总结,css-loader 干了什么:
- 将 css 文件的内容作为字符串导出。
- 将 css 中的其他依赖作为 require 导入,以便 webpack 分析依赖。
2)style-loader 原理
由于 css-loader 仅提供了将 css 转换为字符串导出的能力,剩余的事情要交给其他 loader 或 plugin 来处理。
style-loader 可以将 css-loader 转换后的代码进一步处理,将 css-loader 导出的字符串加入到页面的 style 元素中。
例如:
.red {
color:"#f40";
}经过css-loader转换后变成js代码:
module.exports = `
.red {
color: "#f40";
}
`;经过 style-loader 转换后变成:
module.exports = `
.red {
color: "#f40";
}
`;
var style = module.exports;
var styleElem = document.createElement("style");
styleElem.innerHTML = style;
document.head.appendChild(styleElem);
module.exports = {}以上代码均为简化后的代码,并不代表真实的代码,style-loader 有能力避免同一个样式的重复导入。
3)使用
首先,需要在项目中安装 css-loader。可以使用 npm 来安装:
npm install --save-dev css-loader然后,在 webpack.config.js 文件中,需要添加一个规则来告诉 webpack 如何使用 css-loader 处理 CSS 文件。在配置文件中,找到 module.rules 数组,并添加一个新的对象到这个数组中:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.css$/i,
use: [
{loader: 'css-loader'}
],
},
// ...
],
},
// ...
};发现 HTML 页面没有 css 样式。什么原因?css-loader 只来处理 CSS 文件,还需要使用 style-loader 将 CSS 插入到 HTML 中。
注意,style-loader 是另一个需要安装的 loader,它的作用是将 CSS 添加到 DOM 中,使其生效。可以用以下命令安装:
# 安装 style-loader
npm install --save-dev style-loader配置使用 style-loader。
// 标准写法
{
test: /\.css$/i,
use: [
// 执行顺序: 从下到上, 从右到左
{loader: 'style-loader'},
{loader: 'css-loader'}
],
},
// 简写方式
{
test: /\.css$/i,
// 简写一:如果只有一个加载器
loader: 'css-loader'
// 简写二
use: ['style-loader', 'css-loader']
},这样,当在 JS 文件中 import 一个 CSS 文件时,webpack 就会使用 css-loader 和 style-loader 来处理这个文件。
例如,如果有一个 CSS 文件 styles.css,可以在 JS 文件中这样导入它:
import './styles.css';然后,当使用 webpack 打包项目时,styles.css 就会被正确地处理并添加到输出文件(./dist/index.js)中。
2. 预处理器
1)less-loader
less-loader 是 webpack 的一个加载器,它可以将 LESS 转换为 CSS。
首先,需要在项目中安装 less-loader 和 less。可以使用 npm 来安装。在终端中运行以下命令:
npm install --save-dev less-loader less然后,在 webpack.config.js 文件中,需要添加一个规则来告诉 webpack 如何使用 less-loader 处理 LESS 文件。在配置文件中,找到 module.rules 数组,并添加一个新的对象到这个数组中:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.less$/i,
use: [
'style-loader',
'css-loader',
'less-loader'
],
},
// ...
],
},
// ...
};使用了 less-loader 来处理 LESS 文件,css-loader 来处理转换后的 CSS 为 js,以及 style-loader 将 CSS 插入到 HTML 中。
例如,如果有一个 LESS 文件 styles.less,可以在 JavaScript 文件中这样导入它:
import './styles.less';然后,当使用 webpack 打包你的项目时,styles.less 就会被正确地处理并添加到你的输出文件中。
2)sass-loader
1)下载包
npm install --save-dev sass-loader sasssass-loader:负责将 Sass 文件编译成 css 文件。
sass:sass-loader 依赖 sass 进行编译。
2)配置
module.exports = {
//...
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader', // 将 JS 字符串生成为 style 节点
'css-loader', // 将 CSS 转化成 CommonJS 模块
'sass-loader' // 将 Sass 编译成 CSS
]
}
]
}
};3)使用 sass 编写 css
4)在 src/index.js 中引入。
import "./sass/index.sass";
import "./sass/index.scss";3)stylus-loader
1)下载包
npm i stylus-loader -Dstylus-loader:负责将 Styl 文件编译成 CSS 文件。
2)配置
module.exports = {
//...
module: {
rules: [
{
test: /\.styl(us)?$/,
use: [
'style-loader', // 将 JS 字符串生成为 style 节点
'css-loader', // 将 CSS 转化成 CommonJS 模块
'stylus-loader' // 将 Stylus 编译成 CSS
]
}
]
}
};3)使用 styl 编写 css
4)在 src/index.js 中引入。
3. css module
1)css-loader 中的
css-loader 自带 css module 功能。
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/, use: ["style-loader", {
loader: "css-loader",
options: {
modules: {
localIdentName: "[local]-[hash:5]" // 配置生成的 css 类名
}
modules: true // 开启 css module
}
}]
// test: /\.css$/, use:["style-loader", "css-loader?modules"]
}
]
},
// ...
};2)webpack 中的
待写。
4. CSS 兼容性处理
PostCSS 是一个通过 JavaScript 来转换样式的工具。这个工具可以帮助我们进行一些 CSS 的转换和适配,比如自动添加浏览器前缀、CSS 样式的重置。但是实现这些功能,需要借助于 PostCSS 对应的插件。
PostCSS 本身不是一个加载器,而是一个用 JavaScript 工具和插件转换 CSS 代码的工具。然而,它可以通过 webpack 的加载器(如 postcss-loader)进行集成,使得在 webpack 构建过程中可以使用 PostCSS 来处理 CSS 文件。
如何使用 PostCSS 呢?主要就是两个步骤:
第一步:查找 PostCSS 在构建工具中的扩展,比如 webpack 中的 postcss-loader。
第二步:选择添加你需要的 PostCSS 相关的插件。
1)使用例子
首先,需要在项目中安装 PostCSS 和它的插件。可以使用 npm 来安装。在终端中运行以下命令:
npm install --save-dev postcss postcss-loader autoprefixer这里以 autoprefixer 插件作为例子,它会自动添加 CSS 属性的浏览器前缀。
接下来,需要在 webpack.config.js 文件中配置 postcss-loader。在配置文件中,找到 module.rules 数组,并添加一个新的对象到这个数组中:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.css$/i,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
[
'autoprefixer',
{
// Options
},
],
],
},
},
},
],
},
// ...
],
},
// ...
};在这个规则中,添加了一个新的加载器 postcss-loader,并在 options 中配置了 autoprefixer 插件。
现在,当在 JavaScript 文件中 import 一个 CSS 文件时,webpack 就会使用 postcss-loader、css-loader 和 style-loader 来处理这个文件。并且 postcss-loader 会使用 autoprefixer 插件自动添加 CSS 属性的浏览器前缀。
例如,如果有一个 CSS 文件 styles.css,在 JavaScript 文件中这样导入它:
import './styles.css';然后,当使用 webpack 打包你的项目时,styles.css 就会被正确地处理并添加到你的输出文件中。
问题:webpack.config.js 好臃肿,这还只是一个 autoprefixer 插件的配置。
创建一个 PostCSS 配置文件 postcss.config.js 在项目的根目录下(Loader 将会自动搜索配置文件):
module.exports = {
plugins: [
require('autoprefixer')
]
}2)postcss-preset-env
事实上,在配置 postcss-loader 时,我们配置插件并不需要使用 autoprefixer。可以使用另外一个插件:postcss-preset-env。
postcss-preset-env 也是一个 postcss 的插件。它可以帮助我们将一些现代的 CSS 特性,转成大多数浏览器认识的 CSS,并且会根据目标浏览器或者运行时环境添加所需的 autoprefixer(相当于已经内置了 autoprefixer)。
[1] postcss.config.js
使用 npm 安装它:
npm install --save-dev postcss-preset-env然后,可以在 postcss.config.js 文件中配置它:
module.exports = {
plugins: [
// 标准写法
require('postcss-preset-env')({
// Options
}),
// 简写
'postcss-preset-env'
],
};在这个配置中,可以根据需要提供选项。例如可以指定你的目标浏览器:
module.exports = {
plugins: [
require('postcss-preset-env')({
browsers: 'last 2 versions',
}),
],
};在这个配置中,postcss-preset-env 只需要支持每个浏览器的最后两个版本。
最后,在构建工具(如 webpack)中配置 PostCSS。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
};在这个配置中,所有的 CSS 文件都会被 postcss-loader 处理,postcss-loader 会使用你在 postcss.config.js 中配置的插件。
[2] webpack.config.js
如果不想有 postcss.config.js 文件,可以在 webpack.config.js 配置。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
clean: true,
},
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: [
MiniCssExtractPlugin.loader, // 替代 style-loader
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
],
},
// ......上面代码不好,比如还有 less、sass 等,每次都要写一次这样的代码,怎么办?
// 获取处理样式的Loaders
const getStyleLoaders = (preProcessor) => {
return [
MiniCssExtractPlugin.loader,
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"postcss-preset-env", // 能解决大多数样式兼容性问题
],
},
},
},
preProcessor,
].filter(Boolean);
};
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "../dist"), // 生产模式需要输出
filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
clean: true,
},
module: {
rules: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: getStyleLoaders(),
},
{
test: /\.less$/,
use: getStyleLoaders("less-loader"),
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders("sass-loader"),
},
{
test: /\.styl$/,
use: getStyleLoaders("stylus-loader"),
},
},
};然后,控制兼容性。
可以在 package.json 文件中添加 browserslist 来控制样式的兼容性做到什么程度。
{
// 其他省略
"browserslist": ["ie >= 8"]
}想要知道更多的 browserslist 配置,查看 browserslist 文档
以上为了测试兼容性所以设置兼容浏览器 ie8 以上。
实际开发中我们一般不考虑旧版本浏览器了,所以我们可以这样设置:
{
// 其他省略
"browserslist": ["last 2 version", "> 1%", "not dead"]
}解释:
"last 2 versions"表示要支持所有浏览器的最后两个版本。"> 1%"表示要支持全球使用率超过 1% 的所有浏览器。"not dead"表示不想支持已经官方声明不再维护的死掉的浏览器(IE 6, 7, 8)。
二、处理图片资源
webpack5 引入了一种新的模块类型,称为 Asset Modules,用来替代之前的 file-loader、url-loader 和 raw-loader。Asset Modules 是一种模块类型,允许使用资源文件(如字体、图像等)而无需通过额外的 loader。
Asset Modules 有四种类型:asset(替代 url-loader)、asset/resource(替代 file-loader)、asset/inline(替代 url-loader)和 asset/source(替代 raw-loader)。
asset:导出一个 data URI(即 base64 编码字符串)或发送一个单独的文件到输出目录。
当文件大小小于配置的限制时,会选择 data URI。
其中,module.rules.parser.dataUrlCondition 用于限定文件大小阈值,对标 url-loader 的 limit 属性。
asset/resource:发送一个单独的文件到输出目录,并在导出处返回文件的 URL。
默认情况下,
asset/resource生成的文件会以[hash][ext][query]方式重命名,可以通过 output.assetModuleFilename 属性控制。asset/inline:导出一个资源的 data URI(base64 编码)。
asset/source:导出资源的源代码(把文件内容导入到某个变量)。
可以在 module.rules 中配置这些类型。例如,可以使用 asset 模块类型来处理图像文件:
module.exports = {
// ...
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8kb
},
},
},
],
},
// ...
};在这个配置中,当一个 png、jpg、jpeg 或 gif 文件小于 8kb 时,webpack 会将它作为 data URI 导出,否则,webpack 会将它作为单独的文件发送到输出目录,并在导出处返回文件的 URL。
修改打包后的图片名与路径
在 webpack5 中,可以使用 assetModuleFilename 选项来自定义 Asset Modules 的输出文件名。这个选项可以在 output 配置中设置。例如,如果希望所有的图像文件都被放入 images 目录,并且文件名包含原始文件名和内容哈希,可以这样配置:
module.exports = {
// ...
output: {
// ...
assetModuleFilename: 'images/[name][hash][ext]',
},
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset/resource',
},
],
},
// ...
};在这个配置中,[name] 是原始文件名,[ext] 是文件扩展名,[hash] 是基于文件内容的哈希。
注意,这个配置仅适用于 asset/resource 类型的模块。对于 asset 或 asset/inline 类型的模块,文件名的配置是不相关的,因为这些模块类型会生成 data URI,而不是实际的文件。
另外,也可以在每个 loader 规则中单独设置 assetModuleFilename,这样就可以对不同类型的资源采用不同的命名策略。例如:
module.exports = {
// ...
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
type: 'asset/resource',
generator: {
// 将图片文件命名
// [hash:8]: hash值取8位
// [ext]: 使用之前的文件扩展名
filename: 'images/[name][hash][ext]',
},
},
{
test: /\.svg$/i,
type: 'asset/resource',
generator: {
filename: 'icons/[name][hash:8][ext]',
},
},
],
},
// ...
};在这个配置中,图像文件会被放入 images 目录,而 svg 文件会被放入 icons 目录。
路径问题
在使用 file-loader 或 url-loader 时,可能会遇到一个非常有趣的问题。
比如,通过 webpack 打包的目录结构如下:
dist
|—— img
|—— a.png # file-loader生成的文件
|—— scripts
|—— main.js # export default "img/a.png"
|—— html
|—— index.html # <script src="../scripts/main.js" ></script>这种问题发生的根本原因:模块中的路径来自于某个 loader 或 plugin,当产生路径时,loader 或 plugin 只有相对于 dist 目录的路径,并不知道该路径将在哪个资源中使用,从而无法确定最终正确的路径。
面对这种情况,需要依靠 webpack 的配置 publicPath 解决。
三、处理字体图标资源
一句话:字体图标也是静态资源。
1)项目添加字体文件与 CSS 文件。
2)src/index.js 中引入上一步的 CSS 文件。
import "./css/iconfont.css";3)配置
{
test: /\.(ttf|woff2?)$/,
type: "asset/resource",
generator: {
filename: "static/media/[hash:8][ext][query]",
},
},4)使用
处理视频与音频等资源,也是这样处理的。在
test: /\.(ttf|woff2?)$/,代码后面继续添加test: /\.(ttf|woff2?|map4|map3|avi)$/,。
四、Babel
1. 是什么
Babel 是一个广泛使用的 JavaScript 编译器,它可以将标准的、新版的 JavaScript(ES6, ES7, ES8 等)转换为旧版的 JavaScript(ES5 或更早),以确保你的代码可以在旧版本的浏览器或环境中运行。
2. 配置文件
方法一:新建文件,位于项目根目录 babel.config.*
- babel.config.js
- babel.config.json
方法二:新建文件,位于项目根目录 .babelrc.*
- .babelrc
- .babelrc.js
- .babelrc.json
方法三:不需要创建文件,在原有文件 package.json 基础上添加 babel 键。
Babel 会查找和自动读取它们,所以以上配置文件只需要存在一个即可。
3. 具体配置
以 babel.config.js 配置文件为例:
module.exports = {
// 预设
presets: [],
};presets 预设
简单理解:就是一组 Babel 插件,扩展 Babel 功能。
@babel/preset-env:一个智能预设,允许您使用最新的 JavaScript。@babel/preset-react:一个用来编译 React jsx 语法的预设。@babel/preset-typescript:一个用来编译 TypeScript 语法的预设。
4. 使用
1)babel-loader
babel-loader 是 webpack 的一个加载器,用于在 webpack 构建过程中将 Babel 和 webpack 集成在一起。通过使用 babel-loader,webpack 可以在打包 JavaScript 文件之前,先通过 Babel 进行转译。
首先,需要安装 babel-loader 和 Babel 的核心库。可以使用 npm(或 yarn)来进行安装:
npm install --save-dev @babel/core babel-loader然后,想让箭头函数转换为普通函数,const 转成 var。怎么办?
npm install @babel/plugin-transform-arrow-functions -D
npm install @babel/plugin-transform-block-scoping -D接着,需要在 webpack 配置文件(通常是 webpack.config.js)中添加一个规则,以使用 babel-loader 处理 JavaScript 文件。
module.exports = {
module: {
rules: [
{
test: /\.js$/, // 使用正则来匹配 js 文件
exclude: /node_modules/, // 排除 node_modules 目录
use: {
loader: 'babel-loader', // 指定使用的 loader
options: {
plugins: [
[
"@babel/plugin-transform-arrow-functions",
// 将 ES6 的块级作用域转换为 ES5 代码
"@babel/plugin-transform-block-scoping"
],
],
},
}
}
]
}
};也可以把上面代码抽取到 babel.config.js 文件里。
module.exports = {
plugins: [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping"
]
};2)@babel/preset-env
@babel/preset-env 是一个 Babel 预设,它可以根据你的目标环境自动确定你需要的 Babel 插件和 polyfills(降级 JS)。这意味着你不再需要手动指定转换 ES2015(或更高版本)语法的插件,@babel/preset-env 会根据你的配置自动处理这些事情。
要使用 @babel/preset-env,需要首先安装它。可以使用 npm 或 yarn 来安装。在项目目录中打开终端,然后运行以下命令:
使用 npm:
npm install --save-dev @babel/preset-env或者,使用 yarn:
yarn add --dev @babel/preset-env安装完成后,可以在 webpack.config.js 文件中配置它,如下:
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 排除node_modules代码不编译
loader: "babel-loader",
options: {
presets: [
'@babel/preset-env',
// other presets...
],
}
},
],
},
// ...
};还可以在 Babel 配置文件 babel.config.js 中指定 @babel/preset-env。
可以将 @babel/preset-env 添加到 presets 数组中,如下所示:
module.exports = {
presets: [
'@babel/preset-env',
// other presets...
],
// plugins...
};默认情况下,@babel/preset-env 会转换所有 ES2015-ES2020 语法到 ES5。然而,可以通过配置目标环境或者浏览器来定制化 @babel/preset-env 的行为。例如,可以指定目标浏览器的版本,这样 Babel 只会转换那些不被这些浏览器版本支持的语法。
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
"chrome": "58",
"ie": "11"
}
}
],
// other presets...
],
// plugins...
};在这个例子中,@babel/preset-env 会只转换那些 Chrome 58 和 IE 11 不支持的语法。
第四章:插件
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化、资源管理、注入环境变量。
一、第三方插件
1. CleanWebpackPlugin
clean-webpack-plugin 是一个用来清理 webpack 的构建目录的插件。在每次构建前,这个插件都会清理构建目录,以确保只有使用的文件被包含在构建目录中。
首先,需要安装这个插件。可以使用 npm 或 yarn 来安装:
npm install --save-dev clean-webpack-plugin然后,在 webpack 配置文件中,需要引入并使用这个插件:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
// ...
plugins: [
new CleanWebpackPlugin(),
// ...其他插件
],
// ...
};在这个配置中,CleanWebpackPlugin 在每次构建前都会清理 webpack 的输出目录(默认配置是 output.path 指定的目录)。
更多的配置选项和详细的使用方法,可以参考 clean-webpack-plugin 的官方文档。
目前 webpack 5 中不在需要这个插件了,因为已经有这个功能了。只需在 webpack 配置文件中加入下面的内容。
module.exports = {
// ...
output: {
clean: true, // 在生成文件之前清空 output 目录
},
};2. HtmlWebpackPlugin
HtmlWebpackPlugin 是一个用于生成 HTML 文件的 webpack 插件,它可以接收一个 HTML 文件作为模板,然后将 webpack 打包后生成的 js、css 文件自动注入到这个 HTML 文件中。
首先,需要安装这个插件。
npm install --save-dev html-webpack-plugin然后,在 webpack 配置文件中,需要引入并使用这个插件:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ......
plugins: [
new HtmlWebpackPlugin({
title: "学习 webpack",
// 以 public/index.html 为模板创建文件
// 新的html文件有两个特点: 1.内容和源文件一致 2.自动引入打包生成的js等资源
template: path.resolve(__dirname, "public/index.html"),
}),
// ...其他插件
],
// ......
};在这个配置中,HtmlWebpackPlugin 会使用 public/index.html 作为模板,生成一个新的 HTML 文件,并将 webpack 打包后的 js、css 文件自动注入到这个 HTML 文件中。
HtmlWebpackPlugin 还有许多其他的配置选项,例如:
filename:指定生成的 HTML 文件的名称。
chunks:默认 HTML 会引入 webpack 打包后的所有资源,可以指定具体的 chunk 名字来引入打包后的特定资源。
inject:决定 script 标签应该放在 head 还是 body 中。
minify:压缩生成的 HTML 文件。
hash:为所有包含的 js 和 css 文件添加唯一的 webpack 编译哈希。
当设置
hash: true,生成的 HTML 文件中引用 js 和 css 文件的链接会附带一个查询参数,例如main.js?d587bbd6e38337f5accd。这个查询参数就是 webpack 编译哈希。
例如:
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
inject: 'body',
minify: true,
hash: true,
})更多的配置选项和详细的使用方法,可以参考 html-webpack-plugin 的官方文档。
3. CopyWebpackPlugin
作用:复制静态资源。
首先,需要安装这个插件。可以使用 npm 或 yarn 来安装:
npm install copy-webpack-plugin --save-dev
# or
yarn add -D copy-webpack-plugin然后,在 webpack 配置文件中,需要引入并使用这个插件:
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
// 复制 public 目录下的所有内容到输出目录的根目录
{ from: "public", to: "." },
// { from: "./public", to: "./" } // 跟上面写法等价
],
}),
],
};选项解释:
patterns:一个数组,包含要复制的源文件和目标文件的信息。每个数组元素都是一个对象,其中包含以下属性:
from:要复制文件的 glob 或路径。glob 遵循 fast-glob 模式语法。
to:目标文件路径。可以是字符串或函数。
toType:目标文件类型。可以是
'file'或'dir'。默认值是'file'。globOptions
flatten:是否将源文件复制到目标文件的子目录中。默认值是 false。
transform:一个函数,用于在复制文件之前对源文件进行转换。
options:一个对象,包含一些全局选项,如 concurrency(并发复制的文件数)和 overwrite(是否覆盖已存在的文件)。
filter:过滤。
4. 处理 CSS
1)提取 CSS 成单独文件
CSS 文件目前被打包到 js 文件中,当 js 文件加载时,会创建一个 style 标签来生成样式。
这样对于网站来说,会出现闪屏现象,用户体验不好。
我们应该是单独的 CSS 文件,通过 link 标签加载性能才好。
(1) 下载包
npm i mini-css-extract-plugin -D(2) 配置
webpack.prod.js
大致思路:① 引入插件;② 把 style-loader 换为 MiniCssExtractPlugin.loader;③ 注入插件。
// 1.引入插件
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
// ......
module: {
rules: [
{
test: /\.css$/,
// 2.把 style-loader 换为 MiniCssExtractPlugin.loader
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
},
plugins: [
// 3.提取css成单独文件
new MiniCssExtractPlugin({
// 定义输出文件名和目录
filename: "static/css/main.css",
}),
],
mode: "production",
};二、内置插件
1. DefinePlugin
DefinePlugin 是一个 webpack 内置的插件,它允许在编译时创建全局常量,这在需要区分开发和生产环境的不同行为时非常有用。
首先,需要在 webpack 配置文件中引入这个插件:
const webpack = require('webpack');
module.exports = {
// ......
plugins: [
new webpack.DefinePlugin({
// 'yxts': 'production.woaini', // 如果在代码中 console.log(yxts) 这样使用, 编译后的代码是 console.log(production.woaini), 会出现语法错误
'yxts': `'production.woaini'`, // 编译后的代码 console.log('production.woaini')
'VERSION': JSON.stringify('5fa3b9'),
'BROWSER_SUPPORTS_HTML5': true,
'TWO': '1+1', // 如果在代码中 console.log(TWO) 这样使用, 编译后的代码是 console.log(1+1)
// 'TWO': "'1+1'", // 如果在代码中 console.log(TWO) 这样使用, 编译后的代码是 console.log('1+1')
}),
// ...其他插件
],
// ......
};在这个配置中,DefinePlugin 创建了多个全局常量:VERSION、BROWSER_SUPPORTS_HTML5 和 TWO ……。它们在编译时被替换为对应的值。
注意,由于这些值是在编译时直接插入到源代码中的。所以如果值是字符串,需要使用 JSON.stringify 来确保它们被正确地插入到代码中。
更多的使用方法和详细的信息,可以参考 webpack 的官方文档。
2. BannerPlugin
它可以为每个 chunk 生成的文件头部添加一行注释,一般用于添加作者、公司、版权等信息。
new webpack.BannerPlugin({
banner: `
hash: [hash]
name: [name]
chunkhash: [chunkhash]
author: 雨下田上
corporation: 家里蹲
`
})3. ProvidePlugin
自动加载模块,而不必到处 import 或 require。
new webpack.ProvidePlugin({
$: 'jquery',
_: 'lodash'
})然后在任意源码中:
$('#item'); // <= 起作用
_.drop([1, 2, 3], 2); // <= 起作用第五章:性能优化
一、概述

从两个方面入手,分别是打包速度与产物结果。
二、构建性能
针对开发阶段。
1. 减少模块解析
1)什么叫做模块解析?

模块解析包括:抽象语法树分析、依赖分析、模块语法替换。
2)不做模块解析会怎样?

如果某个模块不做解析,该模块经过 loader 处理后的代码就是最终代码。如果不对某个模块进行解析,可以缩短构建时间。
进一步,如果没有 loader 对该模块进行处理,该模块的源码就是最终打包结果的代码。
3)哪些模块不需要解析?
模块中无其他依赖:一些已经打包好的第三方库,比如 jquery。
4)如何让某个模块不要解析?
配置 module.noParse,它是一个正则,被正则匹配到的模块不会解析。
module.exports = {
mode: "development",
devtool: "source-map",
module: {
noParse: /jquery/
}
}2. 优化 loader 性能
1)进一步限制 loader 的应用范围
思路:对于某些库,不使用 loader。
例如:babel-loader 可以转换 ES6 或更高版本的语法,可是有些库本身就是用 ES5 语法书写的,不需要转换,使用 babel-loader 反而会浪费构建时间。lodash 就是这样的一个库。
小贴士:lodash 是在 ES5 之前出现的库,使用的是 ES3 语法。
通过 module.rule.exclude 或 module.rule.include,排除或仅包含需要应用 loader 的场景。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /lodash/,
use: "babel-loader"
}
]
}
}如果暴力一点,甚至可以排除掉 node_modules 目录中的模块,或仅转换 src 目录下的模块。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// 或
// include: /src/,
use: "babel-loader"
}
]
}
}注意:这种做法是对 loader 的范围进行进一步的限制,和 noParse 不冲突。
2)缓存 loader 的结果
我们可以基于一种假设:如果某个文件内容不变,经过相同的 loader 解析后,解析后的结果也不变。
于是,可以将 loader 的解析结果保存下来,让后续的解析直接使用保存的结果。
cache-loader 可以实现这样的功能。
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['cache-loader', ...loaders]
},
],
},
};有趣的是,cache-loader 放到最前面,却能够决定后续的 loader 是否运行。为什么?
实际上,loader 的运行过程中,还包含一个过程,即 pitch。

cache-loader 还可以实现各自自定义的配置,具体方式见文档。
3)为 loader 的运行开启多线程
thread-loader 会开启一个线程池,线程池中包含适量的线程。
它会把后续的 loader 放到线程池的线程中运行,以提高构建效率。
由于后续的 loader 会放到新的线程中,所以,后续的 loader 不能:
- 使用 webpack api 生成文件
- 无法使用自定义的 plugin api
- 无法访问 webpack 配置
在实际的开发中,可以进行测试,来决定
thread-loader放到什么位置。
特别注意:开启和管理线程需要消耗时间,在小型项目中使用 thread-loader 反而会增加构建时间。
3. 热替换 HMR
热替换并不能降低构建时间(可能还会稍微增加),但可以降低代码改动到效果呈现的时间。
当使用 webpack-dev-server 时,考虑代码改动到效果呈现的过程。

而使用了热替换后,流程发生了变化

1)使用和原理
更改配置
module.exports = {
devServer:{
hot:true // 开启HMR
},
plugins:[
// 可选
new webpack.HotModuleReplacementPlugin()
]
}更改代码
// index.js
if(module.hot){ // 是否开启了热更新
module.hot.accept() // 接受热更新
}首先,这段代码会参与最终运行!
当开启了热更新后,webpack-dev-server 会向打包结果中注入 module.hot 属性。
默认情况下,webpack-dev-server 不管是否开启了热更新,当重新打包后,都会调用 location.reload 刷新页面。
但如果运行了 module.hot.accept(),将改变这一行为。
module.hot.accept() 的作用是让 webpack-dev-server 通过 socket 管道,把服务器更新的内容发送到浏览器。

然后,将结果交给插件 HotModuleReplacementPlugin。插件 HotModuleReplacementPlugin 会将结果覆盖原始代码,然后让代码重新执行。
所以,热替换发生在代码运行期。
2)样式热替换
对于样式也是可以使用热替换的,但需要使用 style-loader。
因为热替换发生时,HotModuleReplacementPlugin 只会简单的重新运行模块代码。
因此 style-loader 的代码一运行,就会重新设置 style 元素中的样式。
而 mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间并会也无法改动文件,因此它对于热替换是无效的。
三、传输性能
1. 代码压缩
1)压缩 JS
[1] 是什么
Terser 是最为流行的 JavaScript 的解释 (Parser)、Mangler (绞肉机) / Compressor (压缩机) 的工具集。
早期会使用 uglify-js 来压缩、丑化我们的 JavaScript 代码,但是目前已经不再维护,并且不支持 ES6+ 的语法。Terser 是从 uglify-es fork 过来的,并且保留它原来的大部分 API 以及适配 uglify-es 和 uglify-js@3 等。
Terser 可以帮助我们压缩、丑化我们的 JS 代码,让我们的 bundle 变得更小。
[2] 安装
因为 Terser 是一个独立的工具,所以它可以单独安装与使用:
# 全局安装
npm install terser -g
# 局部安装
npm install terser -D[3] 使用
之后可以在命令行中使用 Terser。
# 语法
terser [input files] [options]
# 举例说明
terser js/foo.js -o foo.min.js -c -mCompress 的 options (-c 选项)
- arrows:class 或者 object 中的函数,转换成箭头函数。
- arguments:将函数中使用 arguments[index] 转成对应的形参名称。
- dead_code:移除不可达的代码 (tree shaking) 。
Mangle 的 options (-m 选项)
- toplevel:默认值是 false,顶层作用域中的变量名称,进行丑化 (转换) 。
- keep_classnames:默认值是 false,是否保持依赖的类名称。
- keep_fnames:默认值是 false,是否保持原来的函数名称。
npx terser ./src/abc.js -o abc.min.js -c arrows,arguments=true,dead_code -m toplevel=true,keep_classnames=true,keep_fnames=true更多的查看文档
[4] 在 webpack 中的使用
真实开发中,不需要手动的通过 terser 来处理我们的代码,可以直接通过 webpack 来处理:
webpack5.0 后默认使用 Terser 作为 JavaScript 代码压缩器,简单用法只需通过 optimization.minimize 配置项开启压缩功能即可:
module.exports = {
//...
optimization: {
minimize: true
}
};提示:使用 mode = 'production' 启动生产模式构建时,默认也会开启 Terser 压缩。如果对默认的配置不满意,可以自己来创建 TerserPlugin 的实例,来覆盖相关的默认配置。
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
// 是否要启用压缩,默认情况下,生产环境会自动开启
minimize: true,
minimizer: [ // 压缩时使用的插件,可以有多个
new TerserPlugin({
terserOptions: {
compress: {
reduce_vars: true,
pure_funcs: ["console.log"],
},
// ...
},
})
],
},
};首先,我们需要打开 minimize,让其对我们的代码进行压缩。其次,可以在 minimizer 创建一个 TerserPlugin。
terser-webpack-plugin 是一个颇为复杂的 Webpack 插件,提供下述配置项:
test:只有命中该配置的产物路径才会执行压缩,功能与 module.rules.test 相似;include:在该范围内的产物才会执行压缩,功能与 module.rules.include 相似;exclude:与include相反,不在该范围内的产物才会执行压缩,功能与 module.rules.exclude 相似;parallel:是否启动并行压缩,默认值为true,此时会按os.cpus().length - 1启动若干进程并发执行;minify:用于配置压缩器,支持传入自定义压缩函数,也支持swc/esbuild/uglifyjs等值,下面我们再展开讲解;terserOptions:传入minify—— “压缩器”函数的配置参数;extractComments:是否将代码中的备注抽取为单独文件,可配合特殊备注如@license使用。
terser-webpack-plugin 插件并不只是 Terser 的简单包装,它更像是一个代码压缩功能骨架,底层还支持使用 SWC、UglifyJS、ESBuild 作为压缩器,使用时只需要通过 minify 参数切换即可,例如:
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
// `terserOptions` 将被传递到 `swc` (`@swc/core`) 工具
// 具体配置参数可参考:https://swc.rs/docs/config-js-minify
terserOptions: {},
}),
],
},
};Terser 支持的 terserOptions 参数常见有:
- compress:设置压缩相关的选项。
- mangle:设置丑化相关的选项,可以直接设置为 true。
- toplevel:顶层变量是否进行转换。
- keep_classnames:保留类的名称。
- keep_fnames:保留函数的名称。
2)压缩 CSS
一般使用 css-minimizer-webpack-plugin 插件。这个插件底层调用 cssnano 工具来优化、压缩 CSS (也可以单独使用)。
1)安装
npm install css-minimizer-webpack-plugin -D2)配置
webpack.prod.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
//...
module: {
rules: [
{
test: /.css$/,
// 注意,这里用的是 `MiniCssExtractPlugin.loader` 而不是 `style-loader`
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
optimization: {
minimize: true,
minimizer: [
// Webpack5 之后,约定使用 `'...'` 字面量保留默认 `minimizer` 配置
"...",
new CssMinimizerPlugin({
paraller: true
}),
],
},
// 需要使用 `mini-css-extract-plugin` 将 CSS 代码抽取为单独文件
// 才能命中 `css-minimizer-webpack-plugin` 默认的 `test` 规则
plugins: [new MiniCssExtractPlugin()],
};与 terser-webpack-plugin 类似,css-minimizer-webpack-plugin 也支持 test、include、exclude、minify、minimizerOptions 配置,其中 minify 支持:
CssMinimizerPlugin.cssnanoMinify:默认值,使用 cssnano 压缩代码,不需要额外安装依赖;CssMinimizerPlugin.cssoMinify:使用 csso 压缩代码,需要手动安装依赖yarn add -D csso;CssMinimizerPlugin.cleanCssMinify:使用 clean-css 压缩代码,需要手动安装依赖yarn add -D clean-css;CssMinimizerPlugin.esbuildMinify:使用 ESBuild 压缩代码,需要手动安装依赖yarn add -D esbuild;CssMinimizerPlugin.parcelCssMinify:使用 parcel-css 压缩代码,需要手动安装依赖yarn add -D@parcel/css。
提示:同样的,
minimizerOptions也是直接透传给具体minify,具体配置选项可参考 官方文档。
3)压缩 HTML
html-minifier-terser 是一个基于 JavaScript 实现的、高度可配置的 HTML 压缩器。
可以借助 html-minimizer-webpack-plugin 插件接入 html-minifier-terser 压缩器,步骤:
(1) 安装依赖:
yarn add -D html-minimizer-webpack-plugin(2) 修改 Webpack 配置,如:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
module.exports = {
// ...
optimization: {
minimize: true,
minimizer: [
// Webpack5 之后,约定使用 `'...'` 字面量保留默认 `minimizer` 配置
"...",
new HtmlMinimizerPlugin({
minimizerOptions: {
// 折叠 Boolean 型属性
collapseBooleanAttributes: true,
// 使用精简 `doctype` 定义
useShortDoctype: true,
// ...
},
}),
],
},
plugins: [
// 简单起见,这里我们使用 `html-webpack-plugin` 自动生成 HTML 演示文件
new HtmlWebpackPlugin({
templateContent: `<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta charset="UTF-8" />
<title>webpack App</title>
</head>
<body>
<input readonly="readonly"/>
<!-- comments -->
<script src="index_bundle.js"></script>
</body>
</html>`,
}),
],
};与 terser-webpack-plugin 类似,html-minimizer-webpack-plugin 也支持 include、test、minimizerOptions 等一系列配置。
2. Tree Shaking
Tree Shaking 是移除无用代码。
webpack2 开始就支持了 Tree Shaking。
只要是生产环境,Tree Shaking 自动开启。
在编译原理中,把这项技术叫做 DCE (dead code elimination) 。但是 DCE 和 tree shaking 有些许不同,按照 Tobias 的说法,tree shaking 主要应用于于模块(module)之间,用于帮助进行 DCE(webpack 的 DEC 通过 uglify 完成),rollup 的作者也曾经提到, tree shaking 是打包的过程中抽取有用的部分,别的部分像树叶一样落下,所以叫 tree shaking。
1)原理
webpack 会从入口模块出发寻找依赖关系。
当解析一个模块时,webpack 会根据 ES6 的模块导入语句来判断,该模块依赖了另一个模块的哪个导出。
webpack 之所以选择 ES6 的模块导入语句,是因为 ES6 模块有以下特点:
- 导入导出语句只能是顶层语句。
- import 的模块名只能是字符串常量。
- import 绑定的变量是不可变的。
这些特征都非常有利于分析出稳定的依赖。
在具体分析依赖时,webpack 坚持的原则是:保证代码正常运行,然后再尽量 Tree Shaking。
所以,如果你依赖的是一个导出的对象,由于 JS 语言的动态特性,以及 webpack 还不够智能,为了保证代码正常运行,它不会移除对象中的任何信息。
因此,我们在编写代码的时候,尽量:
- 使用
export xxx导出,而不使用export default {xxx}导出。 - 使用
import {xxx} from "xxx"导入,而不使用import xxx from "xxx"导入。
依赖分析完毕后,webpack 会根据每个模块每个导出是否被使用,标记其他导出为 dead code,然后交给代码压缩工具处理。
代码压缩工具最终移除掉那些 dead code 代码。
2)使用第三方库
某些第三方库可能使用的是 commonjs 的方式导出,比如 lodash。
又或者没有提供普通的 ES6 方式导出。
对于这些库,Tree Shaking 是无法发挥作用的。
因此要寻找这些库的 ES6 版本,好在很多流行但没有使用的 ES6 的第三方库,都发布了它的 ES6 版本,比如 lodash-es。
3)作用域分析
Tree Shaking 本身并没有完善的作用域分析,可能导致在一些 dead code 函数中的依赖仍然会被视为依赖。
插件 webpack-deep-scope-plugin 提供了作用域分析,可解决这些问题。
const DeepScope = require("webpack-deep-scope-plugin").default;
module.exports = {
mode: "production",
plugins: [
new DeepScope(),
]
};
webpack-deep-scope-plugin插件诞生自 Issue。目前 webpack@5 可以不使用此插件就能解决这个问题了。
4)副作用问题
webpack 在 Tree Shaking 的使用,有一个原则:一定要保证代码正确运行。
在满足该原则的基础上,再来决定如何 Tree Shaking。
因此,当 webpack 无法确定某个模块是否有副作用时,它往往将其视为有副作用。
因此,某些情况可能并不是我们所想要的。
// common.js
var n = Math.random();
// index.js
import "./common.js"虽然我们根本没使用 common.js 的导出,但 webpack 担心 common.js 有副作用,如果去掉会影响某些功能。
如果要解决该问题,就需要标记该文件是没有副作用的。
在 package.json 中加入 sideEffects。
{
"sideEffects": false
}有两种配置方式:
- false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些 css 文件的导入。
- 数组:设置哪些文件拥有副作用,例如:
["!src/common.js"],表示只要不是src/common.js的文件,都有副作用。
这种方式我们一般不处理,通常是一些第三方库在它们自己的
package.json中标注。
5)css tree shaking
webpack 无法对 css 完成 tree shaking,因为 css 跟 es6 没有半毛钱关系。
因此对 css 的 tree shaking 需要其他插件完成。
例如:purgecss-webpack-plugin。
const MiniCss = require("mini-css-extract-plugin");
const Purgecss = require("purgecss-webpack-plugin");
const path = require("path");
const globAll = require("glob-all");
const srcAbs = path.resolve(__dirname, "src"); // 得到src的绝对路径
const htmlPath = path.resolve(__dirname, "public/index.html");
const paths = globAll.sync([`${srcAbs}**/*.js`, htmlPath]);
module.exports = {
mode: "production",
module: {
rules: [{ test: /\.css$/, use: [MiniCss.loader, "css-loader"] }]
},
plugins: [
new MiniCss(),
new Purgecss({
paths
})
]
};注意:
purgecss-webpack-plugin对css module无能为力。
3. 懒加载 / 动态导入
1)用法
Webpack 中实现代码分割和懒加载的核心技术,使用 ES6 的 import() 语法在运行时按需加载模块。
基本语法
// 返回 Promise
import('./module.js').then(module => {
module.default();
});
// 或使用 async/await
async function loadModule() {
const module = await import('./module.js');
module.default();
}条件加载
// 根据条件加载不同模块
async function loadEditor(type) {
if (type === 'rich') {
const { RichEditor } = await import('./editors/RichEditor');
return RichEditor;
} else {
const { PlainEditor } = await import('./editors/PlainEditor');
return PlainEditor;
}
}
// 按需加载 polyfill
if (!window.IntersectionObserver) {
await import('intersection-observer');
}2)打包后的文件名
因为动态导入通常是一定会打包成独立的文件的,所以并不会再 cacheGroups 中进行配置。那么它的命名我们通常会在 output 中,通过 chunkFilename 属性来命名。
output: {
filename: "[name].bundle.js", // 入口文件打包后的命名格式
chunkFilename: "chunk_[id]_[name].js" // 非入口chunk (如动态导入) 的命名格式
},Magic Comments (魔法注释)
webpack 提供特殊注释来控制动态导入行为。
// webpackChunkName: 指定chunk名称
import(
/* webpackChunkName: "my-chunk-name" */
'./module.js'
);3)实际应用场景
大型库按需加载
// 只在需要时加载图表库
async function showChart() {
const echarts = await import('echarts');
const chart = echarts.init(document.getElementById('chart'));
chart.setOption({...});
}多语言切换
async function changeLanguage(lang) {
const messages = await import(`./i18n/${lang}.js`);
i18n.setLocaleMessage(lang, messages.default);
}4)缺点
比如使用 import() 引入 lodash,因为是动态依赖,所以无法 Tree Shaking。
const btn = document.querySelector("button");
btn.onclick = async function() {
// 动态加载 import, 是 ES6 的草案
const { chunk } = await import(/* webpackChunkName:"lodash" */ "lodash-es");
const result = chunk([3, 5, 6, 7, 87], 2);
console.log(result);
};<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>import() 函数</title>
</head>
<body>
<button>点击</button>
</body>
</html>上面代码无法使用摇树,怎么办?
const btn = document.querySelector("button");
btn.onclick = async function() {
const { chunk } = await import("./util");
const result = chunk([3, 5, 6, 7, 87], 2);
console.log(result);
};export { chunk } from "lodash-es";4. 预压缩
使用 compression-webpack-plugin 插件对打包结果进行预压缩,可以移除服务器的压缩时间。
四、bundle analyzer
Webpack-bundle-analyzer 分析打包后产物中模块体积大小与占比。
1)安装
npm install --save-dev webpack-bundle-analyzer2)使用插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}第六章:分包
此部分也属于传输性能章节。只是分包这块重要且难,所以单独提出来了。
一、手动分包
1. 总体思路
1)先单独的打包公共模块

公共模块会被打包成为动态链接库 (dll Dynamic Link Library) ,并生成资源清单。
2)根据入口模块进行正常打包
打包时,如果发现模块中使用了资源清单中描述的模块,则不会形成下面的代码结构。
// 源码,入口文件index.js
import $ from "jquery"
import _ from "lodash"
_.isArray($(".red"));由于资源清单中包含 jquery 和 lodash 两个模块,因此打包结果的大致格式是:
(function(modules){
//...
})({
// index.js文件的打包结果并没有变化
"./src/index.js":
function(module, exports, __webpack_require__){
var $ = __webpack_require__("./node_modules/jquery/index.js")
var _ = __webpack_require__("./node_modules/lodash/index.js")
_.isArray($(".red"));
},
// 由于资源清单中存在,jquery的代码并不会出现在这里
"./node_modules/jquery/index.js":
function(module, exports, __webpack_require__){
module.exports = jquery;
},
// 由于资源清单中存在,lodash的代码并不会出现在这里
"./node_modules/lodash/index.js":
function(module, exports, __webpack_require__){
module.exports = lodash;
}
})2. 步骤
1)打包公共模块
打包公共模块是一个独立的打包过程。
1)单独打包公共模块,暴露变量名
// webpack.dll.config.js
module.exports = {
mode: "production",
entry: {
jquery: ["jquery"],
lodash: ["lodash"]
},
output: {
filename: "dll/[name].js",
library: "[name]"
}
};2)利用 DllPlugin 生成资源清单
// webpack.dll.config.js
module.exports = {
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, "dll", "[name].manifest.json"), // 资源清单的保存位置
name: "[name]" // 资源清单中,暴露的变量名
})
]
};运行后,即可完成公共模块打包。
2)使用公共模块
1)在页面中手动引入公共模块
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>2)重新设置 clean-webpack-plugin
如果使用了插件 clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置
new CleanWebpackPlugin({
// 要清除的文件或目录
// 排除掉dll目录本身和它里面的文件
cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
})目录和文件的匹配规则使用的是 globbing patterns
3)使用 DllReferencePlugin 控制打包结果
module.exports = {
plugins:[
new webpack.DllReferencePlugin({
manifest: require("./dll/jquery.manifest.json")
}),
new webpack.DllReferencePlugin({
manifest: require("./dll/lodash.manifest.json")
})
]
}3. 总结
手动打包的过程
- 开启
output.library暴露公共模块 - 用 DllPlugin 创建资源清单
- 用 DllReferencePlugin 使用资源清单
手动打包的注意事项
- 资源清单不参与运行,可以不放到打包目录中
- 记得手动引入公共 JS
- 不要对小型的公共 JS 库使用
优点
- 极大提升自身模块的打包速度
- 极大的缩小了自身文件体积
- 有利于浏览器缓存第三方库的公共代码
缺点
- 使用非常繁琐
- 如果第三方库中包含重复代码,则效果不太理想
二、自动分包
1. 基本原理
不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制。
因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要。要控制自动分包,关键是要配置一个合理的分包策略。
有了分包策略之后,不需要额外安装任何插件,webpack 会自动的按照策略进行分包。
实际上,webpack 在内部是使用 SplitChunksPlugin 进行分包的。
过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善,到了 webpack4 之后,已被SplitChunksPlugin取代。

从分包流程中至少可以看出以下几点:
- 分包策略至关重要,它决定了如何分包。
- 分包时,webpack 开启了一个新的 chunk,对分离的模块进行打包。
- 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新 chunk 的产物。
2. 分包策略的配置
webpack 提供了 Optimization 配置项,用于配置一些优化信息。其中 splitChunks 是分包策略的配置。
module.exports = {
optimization: {
splitChunks: {
// 分包策略
}
}
}事实上,分包策略有其默认的配置,我们只需要轻微的改动,即可应对大部分分包场景。
1)设置分包范围
该配置项用于配置需要应用分包策略的 chunk。
我们知道,分包是从已有的 chunk 中分离出新的 chunk,那么哪些 chunk 需要分离呢。
chunks 有三个取值,分别是:
- all:对于所有的 chunk 都要应用分包策略。
- async:【默认】仅针对异步 chunk 应用分包策略。
- initial:仅针对 entry chunk 应用分包策略。
2)根据 Module 使用频率分包
minChunks:一个模块被多少个 chunk 使用时,才会进行分包,默认值 1。
3)限制分包数量
- maxInitialRequest:用于设置 Initial Chunk 最大并行请求数;
- maxAsyncRequests:用于设置 Async Chunk 最大并行请求数。
4)限制分包体积
这一规则相关的配置项有:
minSize: 超过这个尺寸的 Chunk 才会正式被分包,默认值 30000 字节。maxSize: 超过这个尺寸的 Chunk 会尝试进一步拆分出更小的 Chunk;maxAsyncSize: 与maxSize功能类似,但只对异步引入的模块生效;maxInitialSize: 与maxSize类似,但只对entry配置的入口模块生效;enforceSizeThreshold: 超过这个尺寸的 Chunk 会被强制分包,忽略上述其它 Size 限制。
那么,结合前面介绍的两种规则,SplitChunksPlugin 的主体流程如下:
SplitChunksPlugin尝试将命中minChunks规则的 Module 统一抽到一个额外的 Chunk 对象;- 判断该 Chunk 是否满足
maxInitialRequests阈值,若满足则进行下一步; - 判断该 Chunk 资源的体积是否大于上述配置项
minSize声明的下限阈值;- 如果体积小于
minSize则取消这次分包,对应的 Module 依然会被合并入原来的 Chunk - 如果 Chunk 体积大于
minSize则判断是否超过maxSize、maxAsyncSize、maxInitialSize声明的上限阈值,如果超过则尝试将该 Chunk 继续分割成更小的部分
- 如果体积小于
maxSize
该配置可以控制包的最大字节数。
如果某个包(包括分出来的包)超过了该值,则 webpack 会尽可能的将其分离成多个包。
但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积。
另外,该配置看上去很美妙,实际意义其实不大。
因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存。
虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化。
如果要进一步减少公共模块的体积,只能是压缩和
tree shaking。
5)其他配置
- automaticNameDelimiter:新 chunk 名称的分隔符,默认值
~。
3. 缓存组
之前配置的分包策略是全局的,而实际上,分包策略是基于缓存组的。
每个缓存组提供一套独有的策略,webpack 按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包。
cacheGroups 常见属性
- test 属性:匹配符合规则的包。
- name 属性:chunk 名字。
- filename 属性:写到硬盘上的实际文件名称格式。可以使用 placeholder,如
filename: '[id]_[hash:6]_vender.js':[id]是 webpack 为这个 chunk 分配的内部数字 ID。[hash:6]是本次打包生成的哈希值的首 6 位。_vender.js是你写死的字符串。
缓存组默认配置
默认情况下,webpack 提供了两个缓存组:
module.exports = {
optimization:{
splitChunks: {
// 全局配置
// chunks: all,
// 缓存组配置
cacheGroups: {
// 属性名是缓存组名称,会影响到分包的chunk名
// 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
vendors: {
test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
},
default: {
minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
priority: -20, // 优先级
reuseExistingChunk: true // 重用已经被分离出去的chunk
}
}
}
}
}公共样式的抽离
很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了。
但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离。
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
styles: {
test: /\.css$/, // 匹配样式模块
minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
minChunks: 2 // 覆盖默认的最小chunk引用数
}
}
}
},
module: {
rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["index"]
}),
new MiniCssExtractPlugin({
filename: "[name].[hash:5].css",
// chunkFilename是配置来自于分割chunk的文件名
chunkFilename: "common.[hash:5].css"
})
]
}4. 配合多页应用
虽然现在单页应用是主流,但免不了还是会遇到多页应用。
由于在多页应用中需要为每个 html 页面指定需要的 chunk,这就造成了问题。
entry: {
page1: "./src/page1",
page2: "./src/page2"
},
// --------------------------------------------------------------
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["page1~other", "vendors~page1~other", "page1"]
})我们必须手动的指定被分离出去的 chunk 名称,这不是一种好办法。
幸好 html-webpack-plugin 的新版本中解决了这一问题。
npm i -D html-webpack-plugin@next做出以下配置即可:
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["page1"]
})它会自动的找到被 index 分离出去的 chunk,并完成引用。
目前这个版本仍处于测试解决,还未正式发布
5. 原理
自动分包的原理其实并不复杂,主要经过以下步骤:
- 检查每个 chunk 编译的结果,也就是模块记录。
- 根据分包策略,找到那些满足策略的模块。
- 根据分包策略,生成新的 chunk 来打包这些模块。
- 把打包出去的模块从原始包中移除,并修正原始包代码。
在代码层面,有以下变动:
- 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块的代码。
- 原始包的代码中,使用数组中的公共代码。
三、其他
1. 解决注释的单独提取
默认情况下,webpack 在进行分包时,有对包中的注释进行单独提取。
原因是另一个插件默认配置的原因:
optimization: {
splitChunks: {
// ...
},
minimize: true,
minimizer: [
new TerserPlugin({
extractComments: false // 不提取注释到单独的文件 (默认会将注释提取到 .LICENSE.txt 文件)
})
]
}第七章:其他
一、Sourcemap
1. 是什么
代码报错后可以知道原始代码哪个位置出错了。
2. 原理
1)Sourcemap 映射结构
{
"version": 3,
"file": "main.js",
"lineCount": 3,
"sources": [
"webpack:///./src/index.js"
],
"names": ["name", "console", "log"],
"mappings": ";;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E",
"sourcesContent": [
"const name = 'tecvan';\n\nconsole.log(name)"
],
"sourceRoot": ""
}字段含义解释:
| 字段 | 含义 |
|---|---|
| version | Source map 的版本,目前为 3 |
| file | 转换后的文件名 |
| sources | 转换前的文件名。该项是一个数组,表示可能存在多个文件合并 |
| sourceRoot | 转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空 |
| sourcesContent | 字符串数组,原始代码的内容 |
| names | 转换前的所有变量名和属性名 |
| mappings | 记录位置信息的字符串 |
使用时,浏览器会按照 mappings 记录的数值关系,将产物代码映射回 sourcesContent 数组所记录的原始代码文件、行、列位置,这里面最复杂难懂的点就在于 mappings 字段的规则。
2)mappings
[1] 理论
mappings 属性含义
记录着两个文件关联关系的关键信息。
| 对应 | 含义 |
|---|---|
| 第一层是行对应 | 以分号 (😉 表示,每个分号对应转换后源码的一行。所以,第一个分号前的内容,就对应源码的第一行,以此类推。 |
| 第二层是位置对应 | 以逗号 (,) 表示片段映射,对应该行中每一个代码片段到源码的映射。所以,第一个逗号前的内容,就对应该行源码的第一个位置,以此类推。 |
| 第三层是位置转换 | 以 VLQ 编码表示,代表该位置对应的转换前的源码位置。 |
位置对应的原理
每个位置使用五位,表示五个字段。
| 位置 | 含义 |
|---|---|
| 第一位 | 表示这个位置在(转换后的代码的)的第几列 |
| 第二位 | 表示这个位置属于 sources 属性中的哪一个文件 |
| 第三位 | 表示这个位置属于转换前代码的第几行 |
| 第四位 | 表示这个位置属于转换前代码的第几列 |
| 第五位 | 表示这个位置属于 names 属性中的哪一个变量 |
首先,所有的值都是以 0 作为基数的。其次,第五位不是必需的,如果该位置没有对应 names 属性中的变量,可以省略第五位,再次,每一位都采用 VLQ 编码表示;由于 VLQ 编码是变长的,所以每一位可以由多个字符构成。
如果某个位置是 AAAAA,由于 A 在 VLQ 编码中表示 0,因此这个位置的五个位实际上都是 0。它的意思是,该位置在转换后代码的第 0 列,对应 sources 属性中第 0 个文件,属于转换前代码的第 0 行第 0 列,对应 names 属性中的第 0 个变量。
[2] 实战
举个例子,对于下面的代码:
编译前 | 编译后 |
| |
当 devtool = 'source-map' 时,webpack 生成的 mappings 字段为:
;;;;;AAAA,IAAMA,IAAI,GAAG,QAAb;AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E字段内容包含三层结构:
以
;分割的行映射,每一个;对应编译产物每一行到源码的映射,上例经过分割后:javascript[ // 产物第 1-5 行内容为 Webpack 生成的 runtime,不需要记录映射关系 '', '', '', '', '', // 产物第 6 行的映射信息 'AAAA,IAAMA,IAAI,GAAG,QAAb', // 产物第 7 行的映射信息 'AAEAC,OAAO,CAACC,GAAR,CAAYF,IAAZ,E' ]以
,分割的片段映射,每一个,对应该行中每一个代码片段到源码的映射,上例经过分割后:javascript[ // 产物第 1-5 行内容为 Webpack 生成的 runtime,不需要记录映射关系 '', '', '', '', '', // 产物第 6 行的映射信息 [ // 片段 `var` 到 `const` 的映射 'AAAA', // 片段 `name` 到 `name` 的映射 'IAAMA', // 等等 'IAAI', 'GAAG', 'QAAb'], // 产物第 7 行的映射信息 ['AAEAC', 'OAAO', 'CAACC', 'GAAR', 'CAAYF', 'IAAZ', 'E'] ]第三层逻辑为片段映射到源码的具体位置,以上例
IAAMA为例:- 第一位
I代表该代码片段在产物中列数; - 第二位
A代表源码文件的索引,即该片段对标到sources数组的元素下标; - 第三位
A代表片段在源码文件的行数; - 第四位
M代表片段在源码文件的列数; - 第五位
A代表该片段对应的名称索引,即该片段对标到names数组的元素下标。
- 第一位
上述第1、2层逻辑比较简单,唯一需要注意的是片段之间是一种相对偏移关系,例如对于上例第六行映射值:AAAA,IAAMA,IAAI,GAAG,QAAb,每一个片段的第一位 —— 即片段列数为 A,I,I,G,Q,分别代表:
A:第A列;I:第A + I列;I:第A + I + I列;G:第A + I + I + G列;Q:第A + I + I + G + Q列。
这种相对偏移能减少 Sourcemap 产物的体积,提升整体性能。注意,第三层逻辑中的片段位置映射则用到了一种比较高效数值编码算法 —— VLQ(Variable-length Quantity)。
3)VLQ 编码
是什么
VLQ 是一种将整数数值转换为 Base64 的编码算法,它先将任意大的整数转换为一系列六位字节码,再按 Base64 规则转换为一串可见字符。VLQ 使用六位比特存储一个编码分组,例如:
数字 7 的二进制为 111,经过 VLQ 编码后,结果为 001110,其中:
- 第一位为连续标志位,标识后续分组是否为同一数字;1 为后面那一组是我的,0 为后面那一组不是我的。
- 第六位表示该数字的正负符号;0 为正整数,1 为负整数。
- 中间第 2-5 为实际数值。
这样一个六位编码分组,就可以按照 Base64 的映射规则转换为 ABC 等可见字符,例如上述数字 7 编码结果 001110,等于十进制的 14,按 Base64 字码表可映射为字母 O。
实战
按行、片段规则分割后,得出如下片段:
[
// 产物第 1-5 行内容为 Webpack 生成的 runtime,不需要记录映射关系
'', '', '', '', '',
// 产物第 6 行的映射信息
['AAAA', 'IAAMA', 'IAAI', 'GAAG', 'QAAb'],
// 产物第 7 行的映射信息
['AAEAC', 'OAAO', 'CAACC', 'GAAR', 'CAAYF', 'IAAZ', 'E']
]以第 6 行 ['AAAA', 'IAAMA', 'IAAI', 'GAAG', 'QAAb'] 为例:
AAAA解码结果为[000000, 000000, 000000, 000000],即产物第 6 行第 0 列映射到sources[0]文件的第 0 行,第 0 列,实际对应var到const的位置映射;IAAMA解码结果为[001000, 000000, 000000, 001100, 000000],即产物第 6 行第 4 列映射到sources[0]文件的第 0 行,第 6 列,实际对应产物name到源码name的位置映射。
其它片段以此类推,Webpack 生成 .map 文件时,只需要在 webpack-sources 中,按照这个编码规则计算好编译前后的代码映射关系即可。
3. 浏览器中的 Sourcemap
浏览器如何处理 Sourcemap


sourceURL 与 sourceMappingURL 特殊注释
用于指导浏览器的开发者工具(DevTools)如何处理和显示源代码。
sourceURL:给这段生成的代码“起个名字”。
- 作用:主要用于通过
eval()动态执行的代码。正常情况下,通过eval()运行的代码在浏览器开发者面板(Sources 选项卡)中会显示为一串随机的虚拟名称(比如 VM1234),很难分辨这是哪个模块的代码。 - 效果:加上
//# sourceURL=webpack:///某某模块路径后,浏览器就会把你指定的名字当作这段代码的文件名。这样在调试时,你就能清楚地看到这是哪一个模块。
sourceMappingURL:告诉浏览器“Source Map (源码映射) 数据在哪里”。
- 作用:当浏览器运行压缩或编译后的代码(如 ES6 转 ES5,或者打包后的 bundle.js)时,如果开发者工具打开了,它会寻找这个注释。找到后,浏览器会去解析这个 Source Map 数据,从而在调试面板里为你还原出原始的、可读的源代码。
- 格式:它可以接受两种形式:
- 外部文件链接:例如
//# sourceMappingURL=bundle.js.map,告诉浏览器去下载单独的.map文件。 - Data URI(内联形式):例如
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...。它直接将整个 Source Map 的 JSON 数据转换成 Base64 编码,塞在了当前文件里,不需要额外发起网络请求去下载.map文件。
- 外部文件链接:例如
告知浏览器 source map 文件
方法一(常用):通过在优化文件底部添加特别注释,向浏览器表明源地图可用。
//# sourceMappingURL=/path/to/script.js.map该注释通常由用于生成源映射的程序添加。开发者工具只有在启用源映射支持且开发者工具开启时才会加载该文件。
方法二:也可以通过在压缩后的 JavaScript 文件响应中发送 HTTP 头 X-SourceMap 来指定源映射可用。
X-SourceMap: /path/to/script.js.map4. webpack 中的 Sourcemap
webpack 是如何生成 Sourcemap 的?在 processAssets 钩子中遍历产物文件 assets 数组,调用 webpack-sources 提供的 map 方法,最终计算出 asset 与源码 originSource 之间的映射关系。
webpack 中的 Sourcemap 配置项是 devtool,该配置项大致有 27 种取值,具体的配置见文档:
但都是以下 7 个关键字的不同组合。
eval:使用 eval 包裹模块代码,且模块的 Sourcemap 信息通过
//# sourceURL直接挂载在模块代码内。例如:javascripteval("var foo = 'bar'\n\n\n//# sourceURL=webpack:///./src/index.ts?")eval 模式编译速度通常比较快,但产物中直接包含了 Sourcemap 信息,因此只推荐在开发环境中使用。
source-map:webpack 会生成 Sourcemap 文件。例如,产物会额外生成
.map文件,形如:json{ "version": 3, "file": "bundle.js", "sources": [ "webpack:///./src/index.ts" ], "sourcesContent": [ "const foo = 'bar';\nconsole.log(foo);" ], "names": [ "console", "log" ], "mappings": "AACAA,QAAQC,IADI", "sourceRoot": "" }实际上,除 eval 之外的其它枚举值都包含该字段。
cheap 关键字:生成的 Sourcemap 内容会抛弃列维度的信息,也不包含 loader 的 sourcemap,这就意味着浏览器只能映射到代码行维度。例如生成的产物:
json{ "version": 3, "file": "bundle.js", "sources": [ "webpack:///bundle.js" ], "sourcesContent": [ "console.log(\"bar\");" ], // 带 cheap 效果: "mappings": "AAAA", // 不带 cheap 效果: // "mappings": "AACAA,QAAQC,IADI", "sourceRoot": "" }module:只在 cheap 场景下生效,例如
cheap-module-source-map、eval-cheap-module-source-map。当 devtool 包含 cheap 时,webpack 根据 module 关键字判断按 loader 联调处理结果作为 source,还是按处理之前的代码作为 source。包含 loader 的 sourcemap(比如 jsx to js,babel 的 sourcemap),否则无法定义源文件
nosources:生成的 Sourcemap 内容中不包含源码内容,即没有 sourcesContent 字段。例如生成的产物:
json{ "version": 3, "sources": [ "webpack:///./src/index.ts" ], "names": [ "console", "log" ], "mappings": "AACAA,QAAQC,IADI", "file": "bundle.js", "sourceRoot": "" }虽然没有带上源码,但
.map产物中还带有文件名、 mappings 字段、变量名等信息,依然能够帮助开发者定位到代码对应的原始位置,配合 sentry 等工具提供的源码映射功能,可在异地还原诸如错误堆栈之类的信息。inline:webpack 会将 Sourcemap 内容编码为 Base64 DataURL,直接追加到产物文件中。例如生成的产物:
console.log("bar"); //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOlsiY29uc29sZSIsImxvZyJdLCJtYXBwaW5ncyI6IkFBQ0FBLFFBQVFDLElBREkiLCJmaWxlIjoiYnVuZGxlLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgZm9vID0gJ2Jhcic7XG5jb25zb2xlLmxvZyhmb28pOyJdLCJzb3VyY2VSb290IjoiIn0=inline 模式编译速度较慢,且产物体积非常大,只适合开发环境使用。
hidden:通常,产物中必须携带
//# sourceMappingURL=指令,浏览器才能正确找到 Sourcemap 文件,当 devtool 包含 hidden 时,编译产物中不包含//# sourceMappingURL=指令。例如:devtool = 'hidden-source-map'devtool = 'source-map'/******/ (() => { // webpackBootstrapvar __webpack_exports__ = {};/*!**********************!*\!*** ./src/index.ts ***!\**********************/var Person = /** @class */ (function () {}());/******/ })();/******/ (() => { // webpackBootstrapvar __webpack_exports__ = {};/*!**********************!*\!*** ./src/index.ts ***!\**********************/var Person = /** @class */ (function () {}());/******/ })();//# sourceMappingURL=bundle.js.map两者区别仅在于编译产物最后一行的
//# sourceMappingURL=指令,当你需要 Sourcemap 功能,又不希望浏览器 Devtool 工具自动加载时,可使用此选项。需要打开 Sourcemap 时,可在浏览器中手动加载。
总结一下,Webpack 的 devtool 值都是由以上七种关键字的一个或多个组成,虽然提供了 27 种候选项,但逻辑上都是由上述规则叠加而成,例如:
cheap-source-map:代表 不带列映射 的 Sourcemap ;eval-nosources-cheap-source-map:代表 以eval包裹模块代码 ,且.map映射文件中不带源码,且 不带列映射 的 Sourcemap。
其它选项以此类推。最后再总结一下:
对于开发环境,适合使用:
eval:速度极快,但只能看到原始文件结构,看不到打包前的代码内容;cheap-eval-source-map:速度比较快,可以看到打包前的代码内容,但看不到 loader 处理之前的源码;cheap-module-eval-source-map:速度比较快,可以看到 loader 处理之前的源码,不过定位不到列级别;eval-source-map:既把代码放进
eval()里用 sourceURL 贴标签,又在末尾追加了内联的 sourceMappingURL。这样既利用了 eval 的高性能,又拥有了完整的源码映射。//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,...初次编译较慢,但定位精度最高。
inline-source-map:
//# sourceMappingURL=data:application/json;charset=utf-8;base64,...没有使用
eval(),而是直接在整个打包文件的最末尾,以 Data URI 的形式附带上了所有代码的 sourceMappingURL(即完整的 base64 Source Map 数据)。文件体积会非常大。
对于生产环境,则适合使用:
source-map:信息最完整,但安全性最低,外部用户可轻易获取到压缩、混淆之前的源码,慎重使用;hidden-source-map:信息较完整,安全性较低,外部用户获取到.map文件地址时依然可以拿到源码;nosources-source-map:源码信息缺失,但安全性较高,需要配合 Sentry 等工具实现完整的 Sourcemap 映射。
5. 使用 source-map 插件
上面介绍的 devtool 配置项,本质上只是一种方便记忆、使用的规则缩写短语,Sourcemap 的底层处理逻辑实际由 SourceMapDevToolPlugin 与 EvalSourceMapDevToolPlugin 插件实现。
在 devtool 基础上,插件还提供了更多、更细粒度的配置项,用于满足更复杂的需求场景,包括:
- 使用
test、include、exclude配置项过滤需要生成 Sourcemap 的 Bundle; - 使用
append、filename、moduleFilenameTemplate、publicPath配置项设定 Sourcemap 文件的文件名、URL 。
使用方法与其它插件无异,如:
const webpack = require('webpack');
module.exports = {
// ...
devtool: false,
plugins: [new webpack.SourceMapDevToolPlugin({
exclude: ['vendor.js']
})],
};插件配置规则较简单,此处不赘述。