Webpack 4 核心配置剖析

Webpack 是现代 Web 应用程序的静态模块打包工具,它会递归的构建应用程序各个模块的依赖关系图,然后将所有模块打包成一个或多个bundle。目前 Webpack 已经更新至 4.29.6 版本,增加了诸多打包和执行性能相关的支持,是目前应用最广泛、社区最活跃的 Web 前端代码打包方案。而更新版本的 Webpack 5 已经进入 Beta 发布阶段,未来将会带来更多构建性能的提升,本文依然以更为稳定的 Webpack 4 为讲解对象。

Webpack 提出了入口entry输出output加载器loader插件plugin这四个核心概念,本文将会在简单介绍 Webpack 相关基础概念之后,对其原生实现的import模块导入机制进行分析,以清晰的展现 Wepback 在底层所进行的工作;最后逐步备注笔者在开发、生产环境下使用到的各类插件和加载器,并分享在avesrhino两个开源脚手架项目当中(分别基于 Vue2 和 React16)所使用到的最佳配置实践。

入口 entry

entry属性用来指定 webpack 应该使用哪个模块作为构建工作的起点,进入起点后 webpack 将会寻找入口起点直接或间接的依赖,并最后输出到称为bundle的文件。Webpack 中的entry属性值可以是一个或多个字符串、一个对象或数组。

下面的示例代码当中只定义了一个路径字符串:

1
2
3
module.exports = {
entry: "./app.js",
};

而下面的示例代码则通过传入对象指明了多个入口点,实质上 Webpack 会使用CommonsChunkPlugin从应用 bundle 中提取vendor引用到vendor bundle,并将引用vendor的部分替换为__webpack_require__()调用。

1
2
3
4
5
6
const config = {
entry: {
app: "./app.js",
vendors: "./jquery.js",
},
};

Webpack 内置的CommonsChunkPlugin可以为每个页面的共享代码创建 bundle,使得即使在多页面应用下,也能够复用入口起点之间的大量代码和模块。

输出 output

Webpack 的 output 属性指定其所创建的 bundles 输出到何处以及如何命名,

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

module.exports = {
entry: './app.js',
output: {
filename: 'app.bundle.js' // filename属性用于指定bundle的名称
path: path.resolve(__dirname, 'build'), // path属性指定输出bundle的位置
}
};

Webpack 中的chunk([tʃʌŋk] n.大块,厚块,数据块)是指一个独立的文件,如果创建了多个chunk,则应该使用占位符去确保每个文件具有唯一的名称。

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

// 将带有hash字符串后缀的app.js和search.js保存至build目录下
module.exports = {
{
entry: {
app: './app.js',
mobile: './mobile.js'
},
output: {
filename: '[name][chunkhash].js',
path: path.resolve(__dirname, 'build'),
}
}
};

output属性的filename拥有如下 5 个占位符:

占位符 描述
[id] 模块标识符。
[name] 模块名称。
[hash] 模块标识符的 hash 值。
[chunkhash] chunk内容的 hash 值。
[query] 模块的query,例如文件名?后的字符串。

加载器 loader

Webpack 的 loader 加载器机制用于处理非 JavaScript 文件,并将其转换为 Webpack 能够处理的有效模块。即将 JavaScript 之外的其它类型文件,转换为bundle可以直接引用的模块;例如:将文件从 TypeScript 转换为 JavaScript,或者把内联图片转换为 data URL,甚至允许在 JavaScript 模块内引入 CSS 文件。

Webpack 配置文件中的module属性主要用于加载各种模块,可以通过其内嵌的rules属性指定加载某种类型文件时所需要使用到的loader加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const path = require('path');

module.exports = {
entry: {
app: './app.js',
mobile: './mobile.js'
},
output: {
filename: '[name][chunkhash].js',
path: path.resolve(__dirname, 'build'),
}
module: {
// 首先需要npm install ts-loader css-loader
rules: [
{
test: /\.ts$/,
use: 'ts-loader'
}, {
test: /\.css$/,
use: 'css-loader'
},
]
}
};

rules下的use属性指定了需要使用的目标加载器,而test属性则用于标识需要加载器进行处理的文件。根据需要,开发人员还可以对同一类文件应用多个loader进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
test: /\.css$/,
use: [
{
loader: 'style-loader',
}, {
loader: 'css-loader',
options: {
modules: true
}
}
]
}

loader还可以在import语句中内联进行使用,用于针对特定import语句使用指定的加载器。

1
import Styles from "style-loader!css-loader?modules!./styles.css";

加载器 loader 的解析是通过resolver库进行的,resolver用于帮助 Webpack 找到bundle中需要引入的模块代码,这些代码包含在每条require/import语句当中。Webpack 打包模块时会使用到自家的开源项目enhanced-resolve来解析引入的文件路径。

插件 plugin

如同上面所描述的那样,loader 用于转换某些类型的模块,而插件 plugin 得益于其丰富的接口,可以用来处理加载器 loader 无法实现的更加丰富的任务,例如:打包优化与压缩、重新定义环境变量等等。

Webpack 中通过plugins属性来使用插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
entry: {
app: './app.js',
mobile: './mobile.js'
},
output: {
filename: '[name][chunkhash].js',
path: path.resolve(__dirname, 'build'),
}
module: {
// 首先需要npm install ts-loader css-loader
rules: [
{ test: /\.ts$/, use: 'ts-loader' },
{ test: /\.css$/, use: 'css-loader' },
]
},
// 使用new操作符来创建一个插件实例
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new HtmlWebpackPlugin({
template: './index.html'
})
]
};

Webpack 的插件 plugin 本质是一个拥有apply属性的 JavaScript 对象,这个apply属性会被 Webpack 的compiler对象调用,而该对象可以在整个 Webpack 编译生命周期内进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
// LogOnBuildPlugin.js
function LogOnBuildPlugin() {
// ... ...
}

// 注意下面的apply原型属性和compiler对象
LogOnBuildPlugin.prototype.apply = function (compiler) {
compiler.plugin("run", function (compiler, callback) {
console.info("Webpack构建过程开始!");
callback();
});
};

Webpack 模块

相比 NodeJS 的模块机制,Webpack 模块所涵盖的范围显然更加丰富:

  • ES2015 的import语句
  • CommonJS 的require()语句
  • AMD 的define/require语句
  • css/sass/less 中的@import语句。
  • CSS 文件中的url(...)或 HTML 文件中的<img src=...>所指向的图片资源链接。

Webpack 通过enhanced-resolve可以解析三种文件路径:

  • 绝对路径:文件在操作系统上的绝对路径。
1
2
import "/home/common.js";
import "C:\\Users\\common.js";
  • 相对路径:相对于当前引入操作的文件的位置。
1
2
import "./source/demo.js";
import "../demo.js";
  • 模块路径:在resolve.modules中指定的目录内搜索,默认是["node_modules"]
1
2
import Vue from "vue";
import React from "react";

打包策略

Webpack 构建的应用程序中,主要存在以下三种类型的代码:

  1. 开发人员编写的业务代码。
  2. 引入的第三方vendor库。
  3. Webpack 运行时与所有模块进行交互的manifest[ˈmænɪfest] n.清单)。

manifest是浏览器运行时,Webpack 用来连接模块所需的加载和解析逻辑(无论import还是require模块语法,最后都会被 Webpack 转换为指向模块标识符__webpack_require__方法)。

为了有效规避缓存问题,并最大化浏览器渲染性能,可以考虑将上述三种类型代码单独打包到三类文件。

在 Webpack 3.0 可以通过CommonsChunkPlugin插件完成这件工作。该插件可以将公共的依赖模块提取为一个新的的chunk文件,通过将公共模块分拆并且合成之后,便于应用进行缓存和后续使用,避免浏览器重复加载并运行同一段功能代码。

1
2
3
4
5
6
7
8
9
10
new webpack.optimize.CommonsChunkPlugin({
name: string, names: string[], // 通用chunk的名称。
filename: string, // 公共chunk的文件名模板,可以包含与output.filename相同的占位符。
chunks: string[], // 通过chunk名称选择chunks的来源,其中chunk必须是公共chunk的子模块。
children: boolean, // 如果设置为true,所有公共chunk的子级模块会被选择。
deepChildren: boolean, // 如果设置为true,所有公共chunk的全部子孙模块都会被选择。
async: boolean|string, // 设置为true会创建一个异步的公共chunk,它会作为name的子模块以及chunks的兄弟模块,并与chunks并行被加载。
minSize: number, // 公共chunk创建之前,所有公共模块的最小文件尺寸。
minChunks: number|Infinity|function(module, count) -> boolean, // 传入公共chunk之前,所需要包含chunks的最少数量。
})
1
2
3
4
5
6
7
8
9
entry: {
app: './app.js',
vendor: ['vue', 'vuex', 'vue-router', 'element-ui', 'axios']
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest']
}),
],

Webpack4 当中已经废弃了CommonsChunkPlugin的使用,转而采用更加简单明了的optimization.splitChunksoptimization.runtimeChunk属性完成类似工作,下面的章节马上会进行介绍。

optimization

Webpack4 开始提供一个依赖于mode属性进行配置和优化的选项,代替过去CommonsChunkPluginUglifyjsWebpackPlugin等第三方插件的使用。

minimize

让 Webpack 使用UglifyjsWebpackPlugin进行最小化打包。Webpack 配置对象的mode属性为production时该属性默认为true

1
2
3
4
5
module.exports = {
optimization: {
minimize: false,
},
};

minimizer

通过提供一个或多个不同的UglifyjsWebpackPlugin实例来指定一个新的压缩器。

1
2
3
4
5
6
7
8
9
10
11
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {=
optimization: {
minimizer: [
new UglifyJsPlugin({
/* 自定义配置 */
})
]
}
}

splitChunks

Webpack4.0 为动态导入模块提供了一个新的通用代码文件打包策略,具体配置项可以参考本文中的SplitChunksPlugin章节。

runtimeChunk

设置该属性为true时,可以添加额外的代码块至每个只在运行时包含的 Webpack 入口点。该属性可以通过提供一个字符串值来使用插件的预设模式:

single: 建立一个所有代码块共享的运行时文件。 multiple: 为多个通用代码块建立多个运行时文件。

设置该属性为对象时,它只可能去提供name属性,为运行时代码块提供可替代的名称或命名工厂。

该属性默认值为false,表示每个入口代码块都嵌入到运行时。

1
2
3
4
5
6
7
module.exports = {
optimization: {
runtimeChunk: {
name: (entrypoint) => `runtimechunk~${entrypoint.name}`,
},
},
};

noEmitOnErrors

当编译阶段出现错误时,使用optimization.noEmitOnErrors跳过emitting阶段,从而确保不会有错误的资源被emittedstats中的emitted标志对于所有资源都是false的。

1
2
3
4
5
module.exports = {
optimization: {
noEmitOnErrors: true,
},
};

nodeEnv

告诉 Webpack 设置process.env.NODE_ENV为指定的字符串,optimization.nodeEnv底层使用了DefinePlugin插件,除非设置为false。如果 Webpack 对象设置了mode属性,那么optimization.nodeEnv默认为mode属性的值,否则将会回退为"production"

  • 任意字符串:需要设置到process.env.NODE ENV的值。
  • false:不设置或修改process.env.NODE_ENV的值。

SplitChunksPlugin

DefinePlugin

webpack.DefinePlugin

ModuleConcatenationPlugin

附上英文原文链接

过去 webpack 打包的时候,每个 module 都会被包装到独立的函数闭包,这些包装函数会让 JavaScript 在浏览器中执行更缓慢。经过比较,Closure CompilerRollupJS提升、连接全部模块作用域到一个闭包的方式,会让代码在浏览器中执行得更加迅速。因此,Webpack3 当中提供了如下 plugin 来开启类似特性。

1
new webpack.optimize.ModuleConcatenationPlugin();

Webpack3 作用域提升的实现依赖于 ECMAScript 模块语法,因此 Webpack 会基于开发人员当前使用的模块系统回滚到过去的打包方式。

不使用 ModuleConcatenationPlugin 打包的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  bundles git:(master) ll
总用量 8.7M
-rw-rw-r-- 1 hank hank 24K 9月 6 11:44 0.210c1525bfe14cba4ee0.js
-rw-rw-r-- 1 hank hank 61K 9月 6 11:44 0.210c1525bfe14cba4ee0.js.map
-rw-rw-r-- 1 hank hank 5.9K 9月 6 11:44 1.d4b8545dc3dc93af7f6a.js
-rw-rw-r-- 1 hank hank 28K 9月 6 11:44 1.d4b8545dc3dc93af7f6a.js.map
-rw-rw-r-- 1 hank hank 765 9月 6 11:44 2.57378280a5698f76c576.js
-rw-rw-r-- 1 hank hank 6.4K 9月 6 11:44 2.57378280a5698f76c576.js.map
-rw-rw-r-- 1 hank hank 244K 9月 6 11:44 app.13fca0a1be03d8b4a15b.js
-rw-rw-r-- 1 hank hank 710K 9月 6 11:44 app.13fca0a1be03d8b4a15b.js.map
-rw-rw-r-- 1 hank hank 1.4K 9月 6 11:44 app.24004a5d2621177eb1e6bcdabc919636.css
-rw-rw-r-- 1 hank hank 1.9K 9月 6 11:44 app.24004a5d2621177eb1e6bcdabc919636.css.map
-rw-rw-r-- 1 hank hank 1.6K 9月 6 11:44 manifest.c3fe0dc0d40ea3fac1a5.js
-rw-rw-r-- 1 hank hank 15K 9月 6 11:44 manifest.c3fe0dc0d40ea3fac1a5.js.map
-rw-rw-r-- 1 hank hank 983K 9月 6 11:44 vendor.128582091fc1c542ae63.js
-rw-rw-r-- 1 hank hank 6.7M 9月 6 11:44 vendor.128582091fc1c542ae63.js.map

使用 ModuleConcatenationPlugin 打包的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  bundles git:(master) ✗ ll
总用量 8.7M
-rw-rw-r-- 1 hank hank 23K 9月 6 11:47 0.331a96e6fa24027ff29f.js
-rw-rw-r-- 1 hank hank 60K 9月 6 11:47 0.331a96e6fa24027ff29f.js.map
-rw-rw-r-- 1 hank hank 5.8K 9月 6 11:47 1.241c026697aa61a8ca09.js
-rw-rw-r-- 1 hank hank 27K 9月 6 11:47 1.241c026697aa61a8ca09.js.map
-rw-rw-r-- 1 hank hank 714 9月 6 11:47 2.731ff84fbfc108bc253b.js
-rw-rw-r-- 1 hank hank 5.9K 9月 6 11:47 2.731ff84fbfc108bc253b.js.map
-rw-rw-r-- 1 hank hank 1.4K 9月 6 11:47 app.24004a5d2621177eb1e6bcdabc919636.css
-rw-rw-r-- 1 hank hank 1.9K 9月 6 11:47 app.24004a5d2621177eb1e6bcdabc919636.css.map
-rw-rw-r-- 1 hank hank 244K 9月 6 11:47 app.d041e73d9c21f495b099.js
-rw-rw-r-- 1 hank hank 709K 9月 6 11:47 app.d041e73d9c21f495b099.js.map
-rw-rw-r-- 1 hank hank 1.6K 9月 6 11:47 manifest.46788d45af925b1aa79a.js
-rw-rw-r-- 1 hank hank 15K 9月 6 11:47 manifest.46788d45af925b1aa79a.js.map
-rw-rw-r-- 1 hank hank 983K 9月 6 11:47 vendor.154264c34d147a00c862.js
-rw-rw-r-- 1 hank hank 6.7M 9月 6 11:47 vendor.154264c34d147a00c862.js.map

结论:文件尺寸有一定程度上的缩小。

import 实现机制

Webpack 在 2.0 版本之后原生实现了import,而不再需要 Babel 之类加载器进行转换。

最佳实践

Webpack 4 核心配置剖析

http://www.uinio.com/Web/Webpack/

作者

Hank

发布于

2016-04-13

更新于

2016-05-17

许可协议