什么是 webpack

webpack 是一个用于现代 JavaScript 应用程序的静态模块绑定器。当webpack处理你的应用程序时,它会在内部构建一个依赖关系图,映射项目所需的每个模块,并生成一个或多个包(bundles)。

webpack 可以转换打包多种类型的文件、模块、资源,包括 ES Modules 、CommonJS 和 AMD 模块, 也可以将 TypeScript 转换为 JavaScript,将 Handlebars 字符串转换为函数,将图片转换为 Base64,你也可以自己编写插件(plugins)来实现将任何你的应用程序需要的资源进行转换与打包。

webpack 支持所有兼容 ES5 的浏览器。webpack 需要 Promise 来 import() 和 require.ensure() ,在更老旧的浏览器中使用 webpack 时,需要使用 polyfill。

当前(2021年1月19日)最新版本的 webpack 是 v5.15.0, 以下内容默认适用于该版本, 另外 webpack5 运行时需要 Nodejs 版本在 10.13.0 以上。

(2021年12月2日)做了部分更新,当前最新版本的 webpack 是 v5.64.4。

webpack可以做什么

资源管理

将各种类型的文件/模块作为资源进行统一的处理,webpack 通过 资源视图 和 loader 来实现对项目资源的管理。

输出管理

通过丰富的插件,webpack 可以在打包过程中进行一系列的处理,同时支持多个入口和输出。

开发

区分 开发 模式和 生产 模式,可以对开发模式进行更有利于 debug 的编译(source map),热更新等。提供了 webpack-dev-server 本地简易服务器进行开发调试。

代码分割

支持将代码拆分为不同的包,然后可以按需或并行加载。它可以用来拆分更小的包和控制资源负载优先级,如果使用正确,会对负载时间产生重大影响。

多种方法可以实现代码分割,包括

  1. 使用 SplitChunksPlugin 插件将公用依赖导出到特定包
  2. 使用 mini-css-extract-plugin 来将 CSS 从主应用中拆分出来
  3. 使用动态 import
  4. 使用 预获取/预加载 (prefetch/preload)
  5. 包解析
    1. 使用官方提供的 webpack --profile --json > stats.json 生成分析的JSON文件
    2. webpack-chart
    3. webpack-visualizer
    4. webpack-bundle-analyzer
    5. webpack bundle optimize helper
    6. bundle-stats

缓存

浏览器使用缓存来避免无用的请求以及让资源加载更快,webpack 可以根据文件内容在输出文件名中添加 hash 串来控制浏览器的缓存更新策略。通常,可以将 node_modules 中的依赖打包为 vendors.js 然后固定 MODULE id,这样 vendors.[conenthash].js 便不会一直更新。

环境变量

通过设置 --env 参数设置环境变量,在不同的环境下执行不同的编译方案。

webpack的核心配置

Mode

通过将 mode 参数设置为 development 、 production 或 none,可以启用与每个环境相对应的 webpack 内置优化。默认值为 production 。

Entry

entry 指示 webpack 应该使用哪个模块来开始构建其内部依赖关系图。 webpack 将找出 entry 所依赖的其他模块和库(直接和间接)。

默认情况下,其值为 ./src/index.js,但可以通过在 webpack 配置中设置 entry 属性来指定不同的(或多个入口点)。例如:

webpack.config.js
1
2
3
4
module.exports = {
entry: './path/to/my/entry/file.js'
};

Output

output 属性告诉 webpack 在何处导出它创建的包以及如何命名这些文件。对于主输出文件,默认为 ./dist/main.js ,对于任何其他生成的文件,默认为./dist 文件夹。

可以通过在配置中指定输出字段来配置流程的这一部分:

webpack.config.js
1
2
3
4
5
6
7
8
9
const path = require('path');

module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};

Module Loaders

默认情况下, webpack 只理解 JavaScript 和 JSON 文件。Loaders 允许 webpack 处理其他类型的文件,并将它们转换为有效的模块,这些模块可以被应用程序使用并添加到依赖关系图中。

Loaders 是按照规则(rules)进行的,module 的 rules 是一个数组,其中每个 rule 在配置中有两个重要属性,test 属性标识应转换的文件,use 属性指示应该使用哪个加载程序进行转换。

Loaders可以是链式的,在链中的每个loader都会按照反向的顺序被执行。

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const path = require('path');

module.exports = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};

常用 loaders :

loader名字 作用 文件扩展名
style-loader 将 CSS 注入 DOM .css
css-loader css-loader 将会像 import/require() 那样解析 @importurl() .css
postcss-loader 用于对css文件进行预处理,比如添加前缀 .css
file-loader file-loader 将文件的 import/require() 解析为 url,并将文件发送到输出目录,在 webpack5 中使用自带的 asset/resource 来代替 .png .jpg .jpeg .gif
url-loader 将文件转换为 base64 url ,在 webpack5 中使用自带的 asset/resource 来代替 .png .jpg .gif
raw-loader 允许导入文件作为字符串,在 webpack5 中使用自带的 asset/resource 来代替 .txt
csv-loader 允许导入csv文件作为字符串 .csv
xml-loader 允许导入xml文件作为字符串 .xml
less-loader 将 less 文件编译为 css .less
sass-loader 将 sass 文件编译为 css .sass
stylus-loader 将 styl 文件编译为 css .styl
markdown-loader 将 md 文件编译为 html .md
babel-loader 用于对js文件进行预处理,例如将js文件中的语法进行兼容 .js
ts-loader 将ts文件编译为js .ts tsx
html-loader 将html文档以字符串形式导出 .html

Resolve

Resolve 决定了当导入一个模块时,模块应当如何被解析,最常用的用法是定义 import 模块的别名。

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
module.exports = {
resolve: {
// 解析模块请求的选项
modules: [
"node_modules",
path.resolve(__dirname, "app")
],
// 用于查找模块的目录

extensions: [".js", ".json", ".jsx", ".css"],
// 使用的扩展名

alias: {
// 模块别名列表

"module": "new-module",
// 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"

"only-module$": "new-module",
// 起别名 "only-module" -> "new-module",但不匹配 "only-module/path/file" -> "new-module/path/file"

"module": path.resolve(__dirname, "app/third/module.js"),
// 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
// 模块别名相对于当前上下文导入
},
}

Plugins

Loaders 用于转换某些类型的模块,但可以利用 plugins 执行更广泛的任务,如包优化、资源管理和环境变量的注入。
为了使用 plugin ,你需要先引入它并将其添加到配置文件的 plugins 数组中。大多数插件都可以通过选项进行定制。由于可以在配置中多次使用插件以实现不同的目的,因此需要通过使用 New 操作符调用插件来创建插件的实例。

webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 引入的 plugin
const webpack = require('webpack'); // 使用 webpack 自带的 plugin
module.exports = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
},
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
})
]
};

常用 plugins :

plugin 名字 作用
webpack.BannerPlugin 在每个生成的块的顶部添加一些信息,比如版权信息
CleanWebpackPlugin 编译前自动清除输出目录 clean-webpack-plugin
webpack.optimize.CommonsChunkPlugin 提取块之间共享的公共模块
CompressionWebpackPlugin 准备资源的压缩版本,为其提供内容编码 compression-webpack-plugin
webpack.ContextReplacementPlugin 重写require表达式的推断上下文
CopyWebpackPlugin 将单个文件或整个目录复制到生成目录 copy-webpack-plugin
webpack.DefinePlugin 允许在编译时配置全局常量
webpack.DllPlugin 拆分捆绑包以显著缩短构建时间
webpack.EnvironmentPlugin 使用 DefinePlugin 的 process.env 更便捷的方法
EslintWebpackPlugin ESLint 的 webpack插件 eslint-webpack-plugin
webpack.HotModuleReplacementPlugin 允许热替换(HMR)
HtmlWebpackPlugin 为包创建HTML html-webpack-plugin
webpack.IgnorePlugin 从包中排除某些模块
webpack.optimize.LimitChunkCountPlugin 设置分块的最小/最大限制以更好地控制分块
webpack.optimize.MinChunkSizePlugin 保持块大小高于指定的限制
MiniCssExtractPlugin 为每个需要CSS的JS文件创建一个CSS文件 mini-css-extract-plugin
webpack.NoEmitOnErrorsPlugin 出现编译错误时不再抛出
webpack.NormalModuleReplacementPlugin 替换与 regexp 匹配的资源
NpmInstallWebpackPlugin 开发时自动安装缺失的依赖 npm-install-webpack-plugin
OccurrenceOrderPlugin 通过模块调用次数给模块分配ids,常用的ids就会分配更短的id,使ids可预测,减小文件大小
webpack.ProgressPlugin 汇报编译进度
webpack.ProvidePlugin 使用模块而不必使用import/require
webpack.SourceMapDevToolPlugin 支持对源映射进行更细粒度的控制
webpack.EvalSourceMapDevToolPlugin 支持对eval源映射进行更细粒度的控制
TerserPlugin 使用 Terser 压缩混淆项目中的JS terser-webpack-plugin
UglifyJsPlugin js压缩插件 uglifyjs-webpack-plugin
WebpackBarPlugin 优化webpack加载命令行界面为进度条格式 progress-bar-webpack-plugin

DevServer

DevServer 用来配置和 webpack-dev-server 相关的选项。

Externals

Externals 用来防止将某些 import 的包打包到 bundle 中,而是在运行时再去从外部获取这些扩展依赖。

webpack安装

使用npm/yarn安装

1
2
3
4
5
npm install --save-dev webpack
// 或者
yarn add webpack --dev
// webpack4以上的版本需要同时安装webpack-cli
npm install --save-dev webpack-cli

添加 build 脚本到 package.json 中

1
2
3
"scripts": {
"build": "webpack --config webpack.config.js"
}

配合使用的npm包

webpack webpack包
webpack-cli webpack工具包,从webpack4开始需要安装该包
webpack-bundle-analyzer webpack打包文件分析工具
webpack-dev-server webpack开发服务器
webpack-merge webpack配置文件合并工具,可以将两个配置文件合并为一个

哪些方法可以缩减webpack编译时间?

详见 build performance
以下提供了一些常见的方法

  1. 将加载程序应用到所需的最小模块数 module.rules[n].include
  2. 每个额外的加载程序/插件都有一个启动时间。尽量少用工具。
  3. 使用 DllPlugin 将更改较少的代码移动到单独的编译中
  4. 开发时在内存中编译 使用 webpack-dev-server
  5. 了解 devtool 选项的区别,大部分情况下使用 eval-cheap-module-source-map
  6. 开发时避免使用生产环境工具 例如 [contenthash]、TerserPlugin、AggressiveSplittingPlugin
  7. 勿使用 Node.js 8.9.10-9.11.1版本,这些版本的 Map Set 实现有性能问题
  8. 使用多线程编译 parallel-webpack & cache-loader

## webpack编译流程

  1. 初始化参数: 从配置文件 webpack.config.js 和 cli args 参数中读取参数 options 并进行合并
  2. 开始编译: 根据输入参数初始化 Compiler 对象,加载配置的插件 plugin,调用插件的的 apply 方法,执行 Compiler 对象的 run 方法开始执行编译(实例化 Compilation 对象)
  3. 确认入口: 根据配置中的 entry 找出所有的入口文件
  4. 编译模块: 从入口文件开始解析,调用配置的 loader 对不同类型的模块进行载入,递归这个过程直到所有的入口文件都经过处理,得到所有模块的依赖关系 Module Chain
  5. 输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk, 每个 chunk 生成一个资源列表 chunk assets, 之后会把多个 chunk 合并转换成单独的文件 bundle 加入到输出列表
  6. 输出完成: 根据 output 配置确定输出的路径和文件名,把文件内容写入到文件系统中

loader 和 plugin 的区别

  1. loader 是资源加载器,通过链式调用,loader 赋予了 webpack 加载不同类型资源的能力,例如解析 Less文件、解析图片资源等。
  2. plugin 是插件,通过在 webpack 编译过程中预先设置钩子函数,plugin 可以在 webpack 编译过程中实现各种各样的功能,例如打包优化、资源管理、环境变量注入等。

webpack 生命周期

  1. environment
  2. afterEnvironment
  3. entryOption
  4. afterPlugins
  5. afterResolvers
  6. initalize
  7. beforeRun
  8. run
  9. normalModuleFactory
  10. contextModuleFactory
  11. beforeComlile
  12. compile
  13. thisCompilation
  14. compilation
  15. make
  16. finishMake
  17. afterCompile
  18. shouldEmit
  19. emit
  20. afterEmit
  21. done
  22. afterDone

如何编写一个loader

loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 Loader API,并通过 this 上下文访问。

  1. 一个 loader 只能传入一个参数,一个包含资源文件内容的字符串。
  2. loader 会返回一个或者两个值,第一个值的类型是 string 或者 Buffer 类型的数据,第二个可选值是 SourceMap。
  3. 如果是单个处理结果,可以在 同步模式 中直接 return。如果有多个处理结果,则必须调用 this.callback()
  4. 异步模式 中,必须调用 this.async() 来告知 loader runner 等待异步结果,它会返回 callback 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。
  5. loader 可以提供 pitch 方法来在 pitching 阶段执行一些操作,在 pitch 方法中返回数据可以跳过之后的 loader。
  6. loader 函数中的 this 可以访问 loader 上下文中的方法和属性
loader.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import path from 'path';

// 一个函数
export default function (source) {
var callback = this.async();
var headerPath = path.resolve('header.js');

this.addDependency(headerPath);

fs.readFile(headerPath, 'utf-8', function (err, header) {
if (err) return callback(err);
callback(null, header + '\n' + source);
});
}

如何自定义plugin

plugin 向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以在 webpack 构建流程中引入自定义的行为。

webpack 插件由以下组成:

  1. 一个 JavaScript 命名函数或 JavaScript 类。
  2. 在插件函数的 prototype 上定义一个 apply 方法。
  3. 指定一个绑定到 webpack 自身的事件钩子。
  4. 处理 webpack 内部实例的特定数据。
  5. 功能完成后调用 webpack 提供的回调。
MyExampleWebpackPlugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 一个 JavaScript 类
export default class MyExampleWebpackPlugin {
// 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.hooks.emit.tapAsync(
'MyExampleWebpackPlugin',
(compilation, callback) => {
console.log('这是一个示例插件!');
console.log(
'这里表示了资源的单次构建的 `compilation` 对象:',
compilation
);

// 用 webpack 提供的插件 API 处理构建过程
compilation.addModule(/* ... */);

callback();
}
);
}
}

webpack Tree-shaking原理

  1. Tree-shaking 是一种通过清除多余代码的方式来优化项目打包体积的技术。
  2. Tree-shaking 利用了 ES6 模块的特点,ES6 的模块加载是静态的,因此整个依赖树可以在编译时被静态的推导出解析语法树(AST),从而在编译时删除未使用的部分
  3. webpack2 开始就已经支持 Tree-shaking 的特性,webpack4 中 mode 设置为 production 时默认开启 Tree-shaking
  4. 确保没有把 compiler 将 ES6 模块语法转换为 CommonJS 模块。这一块很重要,在你使用 babel-loader 或者 ts-loader 编译代码时,一定要保留 import 和 export。
  5. 如果打包的代码有副作用(Side Effects),并且需要导出,可以通过 package.json 的 sideEffect 属性来声明