webpack 快速入门 系列 —— 性能
其他章节请看:
性能
本篇主要介绍 webpack 中的一些常用性能,包括热模块替换、source map、oneOf、缓存、tree shaking、代码分割、懒加载、渐进式网络应用程序、多进程打包、外部扩展(externals)和动态链接(dll)。
准备本篇的环境
虽然可以仅展示核心代码,但笔者认为在一个完整的环境中边看边做,举一反三,效果更佳。
这里的环境其实就是实战一一文完整的示例,包含打包样式、打包图片、以及打包javascript
项目结果如下:
webpack-example3
- src // 项目源码
- index.html // 页面模板
- index.js // 入口
- package.json // 存放了项目依赖的包
- webpack.config.js // webpack配置文件
代码如下:
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=`, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>请查看控制台</p>
<span class='m-box img-from-less'></span>
</body>
</html>
// index.js
console.log('hello');
// package.json
{
"name": "webpack-example3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.14.2",
"babel-loader": "^8.2.2",
"core-js": "3.11",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-webpack-plugin": "^2.5.4",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.2",
"less-loader": "^7.3.0",
"mini-css-extract-plugin": "^1.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.3.0",
"postcss-preset-env": "^6.7.0",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2"
}
}
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
process.env.NODE_ENV = 'development'
const postcssLoader = {
loader: 'postcss-loader',
options: {
// postcss 只是个平台,具体功能需要使用插件
// Set PostCSS options and plugins
postcssOptions:{
plugins:[
// 配置插件 postcss-preset-env
[
"postcss-preset-env",
{
// browsers: 'chrome > 10',
// stage:
},
],
]
}
}
}
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/i,
// 将 style-loader 改为 MiniCssExtractPlugin.loader
use: [MiniCssExtractPlugin.loader, "css-loader", postcssLoader],
},
{
test: /\.less$/i,
loader: [
// 将 style-loader 改为 MiniCssExtractPlugin.loader
MiniCssExtractPlugin.loader,
"css-loader",
postcssLoader,
"less-loader",
],
},
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 指定文件的最大大小(以字节为单位)
limit: 1024*6,
},
},
],
},
// +
{
test: /\.html$/i,
loader: 'html-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
// +
{
// 配置处理polyfill的方式
useBuiltIns: "usage",
// 版本与我们下载的版本保持一致
corejs: { version: "3.11"},
"targets": "> 0.25%, not dead"
}
]
]
}
}
}
]
},
plugins: [
new MiniCssExtractPlugin(),
new OptimizeCssAssetsPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
// new ESLintPlugin({
// // 将启用ESLint自动修复功能。此选项将更改源文件
// fix: true
// })
],
mode: 'development',
devServer: {
open: true,
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
},
};
Tip: 由于本篇不需要 eslint,为避免影响,所以先注释。
在 webpack-example3 目录下运行项目:
// 安装项目依赖的包
> npm i
// 启动服务
> npm run dev
浏览器会自动打开页面,如果看到”请查看控制台“,控制台也输出了“hello”,说明环境准备就绪。
注:笔者运行 npm i
时出现了一些问题,在公司运行 npm i
验证此文是否正确,结果下载得很慢(好似卡住了),于是改为淘宝镜像 cnpm i
,这次仅花少许时间就执行完毕,接着运行 npm run dev
却在终端报错。于是根据错误提示安装 babel-loader@7 ,再次重启服务,问题仍旧没有解决。回家后,运行 npm i
,依赖安装成功,可能环境也很重要。
// 终端报错
...
babel-loader@8 requires Babel 7.x (the package '@babel/core'). If you'd like to use Babel 6.x ('babel-core'), you should install 'babel-loader@7'.
热模块替换
模块热替换(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新所有类型的模块,而无需完全刷新。
Tip: HMR 不适用于生产环境,这意味着它应当用于开发环境
下面我们就从 html、css 和 js 三个角度来体验热模块替换。
启用 hmr
此功能可以很大程度提高生产效率。我们要做的就是更新 webpack-dev-server 配置, 然后使用 webpack 内置的 HMR 插件。
配置 hot: true
就能启用 hmr。
// webpack.config.js
module.exports = {
devServer: {
// 开启热模块替换
hot: true
}
}
css 使用 hmr
新建一个 css 文件,通过 index.js 引入:
// a.css
p{color:blue;}
// index.js
import './a.css'
首先我们先不开启 hmr,重启服务(npm run dev
),浏览器文字显示蓝色。如果改为红色(color:red;
),你会发现整个页面都刷新了,文字变为红色。
接着开启hmr(hot: true
),重启服务,再次修改颜色,文字的颜色会改变,但整个页面不会刷新。
Tip:如果觉得每次重启服务,都会自动打开浏览器页面,你可以注释掉 open: true
来关闭这个特征。
这里 css 热模块之所以生效,除了在 dev-server 中开启了 hmr,另一个是借助了 mini-css-extract-plugin 这个包;而借助 style-loader 使用模块热替换来加载 CSS 也这么简单。
html 使用 hmr
没有开启热模块替换之前,修改 index.html 中的文字,浏览器页面会自动刷新;而开启之后,修改 html 中的文字,浏览器页面就不会自动刷新。
将 index.html 也配置到入口(entry)中:
// webpack.config.js
module.exports = {
- entry: './src/index.js',
// 将 index.html 也作为入口文件
+ entry: ['./src/index.js', './src/index.html'],
}
重启服务,再次修改 index.html,浏览器页面自动刷新,热模块替换对 html 没生效。
// index.html
- <p>请查看控制台</p>
+ <p>请查看控制台2</p>
Tip:热模块替换,就是一个模块发生了变化,只变更这一个,其他模块无需变化;而 index.html 不像 index.js 会有多个模块,index.html 只有一个模块,就是它自己,所以也就不需要热模块替换。
js 使用 hmr
首先在 dev-server 中开启 hmr,然后创建一个 js 模块,接着在 index.js 中引入:
// a.js
const i = 1;
console.log(i);
// index.js
// 引入 a.js 模块
import './a';
此刻,你若修改 i 的值(const i = 2;
),则会发现浏览器页面会刷新。
要让热模块替换在 js 中生效,我们需要修改代码:
// index.js
// 引入 a.js 模块
import './a';
if (module.hot) {
module.hot.accept('./a', () => {
console.log('Accepting the updated printMe module!');
});
}
再次修改 i 的值,控制台会输出新的值,但浏览器页面不会再刷新。
此时,如果你尝试给入口文件(index.js)底部增加一条语句 console.log('a');
,你会发现浏览器还是会刷新。
所以这种方式对入口文件无效,只能处理非入口 js。
注:如果一个 js 模块没有 HMR 处理函数,更新就会冒泡(bubble up)。
小结
模块热替换比较难以掌握。
社区还提供许多其他 loader,使 HMR 与各种框架和库平滑地进行交互:
- Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
- React Hot Loader: 实时调整 react 组件。
source map
source map,提供一种源代码到构建后代码的映射,如果构建后代码出错了,通过映射可以方便的找到源代码出错的地方。
初步体验
我们先故意弄一个语法错误,看浏览器的控制台如何提示:
// a.js
const i = 1;
// 下一行语法错误
console.log(i)();
// 控制台提示 a.js 第3行出错
Uncaught TypeError: console.log(...) is not a function a.js:3
点击“a.js:3”,显示内容为:
var i = 1; // 下一行语法错误
console.log(i)();
定位到了源码,很清晰。
假如换成 es6 的语法,点击进入的错误提示就没这么清晰了。请看示例:
// a.js
class Dog {
constructor(name) {
this.name = name;
}
say() {
console.log(this.name)();
}
}
new Dog('xiaole').say();
...
var Dog = /*#__PURE__*/function () {
function Dog(name) {
_classCallCheck(this, Dog);
this.name = name;
}
_createClass(Dog, [{
key: "say",
value: function say() {
console.log(this.name)(); // {1}
}
}]);
return Dog;
}();
new Dog('xiaole').say();
错误提示会定位了行{1},我们看到的不在是自己编写的源码,而是通过 babel 编译后的代码。
接下来我们通过配置 devtool,选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。
Tip:Devtool 控制是否生成,以及如何生成 source map。
// webpack.config.js
module.exports = {
devtool: 'source-map'
}
重启服务,通过错误提示点击进去,则会看到如下代码:
class Dog {
constructor(name) {
this.name = name;
}
say() {
console.log(this.name)(); // {1}
}
}
new Dog('xiaole').say();
不在是编译后的代码,而是我们的源码,而且在行{1}处,对错误也有清晰的提示。
不同的值
source map 格式有多种不同的值,以下是笔者对其中几种值的研究结论:
devtool: 'source-map'
> npm run build
1. 会生成一个 dist/main.js.map 文件
2. 在 dist/main.js 最后一行,有如下一行代码:
//# sourceMappingURL=main.js.map
3. 上文我们知道,调试能看到源码,官网文档的描述是 `quality 是 original`
4. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)
5. 官网推荐其可作为生产的选择
devtool: inline-source-map
> npm run build
1. 没生成一个 dist/main.js.map 文件
2. 在 dist/main.js 最后一行,有如下一行代码:
//# sourceMappingURL=data:application/json;charset=
3. 调试能看到源码
4. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)
devtool: eval-source-map
> npm run build
1. 没生成一个 dist/main.js.map 文件
2. 在 dist/main.js 中有 15 处 sourceMappingURL。而 inline-source-map 只有一处。
3. 调试能看到源码
4. 构建(build)速度最慢(slowest),但重建(rebuild)速度正常(ok)
5. 官网推荐其可作为开发的选择
devtool: hidden-source-map
> npm run build
1. 生成一个 dist/main.js.map 文件
2. 点击错误提示,看到的是编译后的代码
Uncaught TypeError: console.log(...) is not a function main.js:11508
3. 构建(build)速度和重建(rebuild)速度都是最慢(slowest)
注:官网说 hidden-source-map 的品质是 original,但笔者这里却是编译后的!
如何选择
source map 有很多不同的值,我们该如何选择?
幸好官网给出了建议。
开发环境,我们要求构建速度要快,方便调试:
- eval-source-map,每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。
生成环境,考虑到代码是否要隐藏,是否需要方便调试:
- source-map,整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。官网推荐其可作为生产的选择。
- (none)(省略 devtool 选项),不生成 source map,也是一个不错的选择
Tip:若你还有一些特别的需求,就去官网寻找答案
oneOf
oneof 与下面程序的 break 作用类似:
let count = 1
for(; count < 10; count++){
if(count === 3){
break;
}
}
console.log(`匹配了${count}次`) // 匹配了3次
这段代码,只要 count 等于 3,就会被 break 中断退出循环。
通常,我们会这样定义多个规则:
module: {
rules: [{
test: /\.css$/i,
loader: ...
},
{
test: /\.css$/i,
loader: ...
},
{
test: /\.less$/i,
loader: ...
},
{
test: /\.(png|jpg|gif)$/i,
loader: ...
}
...
]
当 a.css 匹配了第一个规则,还会继续尝试匹配剩余的规则。而我希望提高一下性能,只要匹配上,就不在匹配剩余规则。则可以使用 Rule.oneOf,就像这样:
module: {
rules: [
{
oneOf: [{
test: /\.css$/i,
loader: ...
},
{
test: /\.less$/i,
loader: ...
},
{
test: /\.(png|jpg|gif)$/i,
loader: ...
}
...
]
}
]
如果同一种文件需要执行多个 loader,就像这里 css 有 2 个 loader。我们可以把其中一个 loader 提到 rules 中,就像这样:
module: {
rules: [
{
test: /\.css$/i,
// 优先执行
enforce: 'pre'
loader: ...
},
{
oneOf: [{
test: /\.css$/i,
loader: ...
},
...
]
}
]
Tip: 可以通过配置 enforce 指定优先执行该loader
缓存
babel 缓存
让第二次构建速度更快。
配置很简单,就是给 babel-loader 添加一个选项:
{
loader: 'babel-loader',
options: {
presets: [
...
],
// 开启缓存
cacheDirectory: true
}
}
Tip:因为要经过 babel-loader 编译,如果代码量太少,就不太准确,建议找大量的 es6 代码自行测试。
静态资源的缓存
Tip: 本小节讲的其实就是 hash、chunkhash和conenthash。
通常我们将代码编译到 dist 目录中,然后发布到服务器上,对于一些静态资源,我们会设置其缓存。
具体做法如下:
通过命令 npm run build
将代码编译到 dist 目录;
接着通过 express 启动服务,该服务会读取 dist 中的内容,相当于把代码发布到服务器上:
// 安装依赖
> npm i -D express@4
// 在项目根目录下创建一个服务:server.js
const express = require('express')
const app = express()
const port = 3001
app.use(express.static('dist'));
// 监听服务
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
> nodemon server.js
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
Example app listening at http://localhost:3001
通过浏览器访问 http://localhost:3001
,多刷新几次,在网络中会看见 main.js 的状态是 304,笔者这里的时间在2ms或5ms之间。
Tip:304 仍然会发送请求,通常请求头中 If-Modified-Since 的值和响应头中 Last-Modified 的值是相同的。
If-Modified-Since: Sat, 17 Jul 2021 02:34:06 GMT
Last-Modified: Sat, 17 Jul 2021 02:34:06 GMT
接下来我给静态资源增加缓存,这里就增加一个 10 秒的缓存:
// server.js
- app.use(express.static('dist'));
+ app.use(express.static('dist', { maxAge: 1000 * 10 }));
再次请求,发现 main.js 首先是 304,接下来10秒内状态码则是200,大小则指示来自内存,时间也变为 0 ms。过10秒后再次请求,又是 304。
现在有一个问题,在强缓存期间,如果出现了bug,我们哪怕修复了,用户使用却还是缓存中有问题的代码。
我们模拟一下这个过程图:先将缓存改长一点,比如 1 天,用户访问先输出 1,让浏览器缓存后,我们再修改代码让其输出 2,用户再次访问会输出什么?
// server.js
app.use(express.static('dist', { maxAge: '1d' }));
// index.js
console.log('1');
重新打包生成 dist,接着用户通过浏览器访问,控制台输出 1。
修改 js,重新打包生成 dist,再次访问,控制台还是输入 1。
// index.js
console.log('2');
注:不要强刷,因为用户不知道强刷,也不会去管。
于是我们打算从文件名入手来解决此问题,我们依次来看看 hash、chunkhash和conenthash。
hash
核心代码如下:
// index.js
import './a.css'
console.log('1');
// a.css
p{color:red;}
// webpack.config.js
module.exports = {
output: {
filename: 'main.[hash:10].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:10].css",
})
]
}
重新打包:
> npm run build
> webpack-example3@1.0.0 build
> webpack
Hash: b2e057d598ca9092abd3
Version: webpack 4.46.0
Time: 4837ms
Built at: 2021-07-14 8:17:54 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.b2e057d598.css 12 bytes main [emitted] [immutable] main
main.b2e057d598.js 5.22 KiB main [emitted] [immutable] main
Entrypoint main = main.b2e057d598.css main.b2e057d598.js main.b2e057d598.js.map
主要看生成的 css 和 js 文件,名字中都带有相同的值 b2e057d598
,取的是生成的 Hash 的前10位。index.html 中也会自动引入对应的文件名。
现在浏览器访问,文字是红色,控制台输出1。
接着模拟修复缺陷,将文字改为蓝色,再次打包。
p{color:blue;}
> npm run build
> webpack-example3@1.0.0 build
> webpack
Hash: ed2cd907a36536276d20
Version: webpack 4.46.0
Time: 4771ms
Built at: 2021-07-14 8:29:14 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.ed2cd907a3.css 13 bytes main [emitted] [immutable] main
main.ed2cd907a3.js 5.22 KiB main [emitted] [immutable] main
浏览器访问,文字确实变为蓝色。但 js 和 css 都重新请求了,再看打包生成的文件,js 和 css 也都重新生成了新的文件名。这个会导致一个问题,只修改一个文件,其他的所有缓存都会失效。
Tip:这里修复的是 css,如果修复 js 也同样会导致所有缓存失效。
chunkhash
hash 会导致所有缓存失效,我们将其改为 chunkhash,还是存在相同的问题。请看示例:
将 hash 改为 chunkhash:
// webpack.config.js
module.exports = {
output: {
filename: 'main.[chunkhash:10].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[chunkhash:10].css",
})
]
}
修改 css,然后重新打包,发现 js 和 css 文件也都重新生成了,虽然 chunkhash 与 hash 值不相同,但 main.js 和 main.css 中的 chunkhash 是一样的:
> npm run build
> webpack-example3@1.0.0 build
> webpack
Hash: 8c1c035175aae3d36fea
Version: webpack 4.46.0
Time: 5000ms
Built at: 2021-07-14 9:16:46 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.619734f520.css 13 bytes main [emitted] [immutable] main
main.619734f520.js 5.22 KiB main [emitted] [immutable] main
Tip: 通过入口文件引入的模块都属于一个 chunk。这里 css 是通过入口文件(index.js)引入的,所以 main.js 和 main.css 的 chunkhash 值相同。
contenthash
contenthash 是根据文件内容来的,可以较好的解决以上问题。请看示例:
将 chunkhash 改为 contenthash,然后打包:
// webpack.config.js
module.exports = {
output: {
filename: 'main.[contenthash:10].js',
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[contenthash:10].css",
})
]
}
> npm run build
> webpack-example3@1.0.0 build
> webpack
Hash: 12994324788654e2ffc4
Version: webpack 4.46.0
Time: 5115ms
Built at: 2021-07-14 9:26:59 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.21668176f0.css 12 bytes main [emitted] [immutable] main
main.8983191438.js 5.22 KiB main [emitted] [immutable] main
这次,js 和 css 的 hash 值不在相同。通过浏览器访问多次后,main.js 和 main.css 也都被强缓存。
修改css:
p{color:yellow;}
打包发现 js(main.8983191438.js) 没有变,只有 css 变了:
> npm run build
> webpack-example3@1.0.0 build
> webpack
Hash: 1598c3794090ebc6964c
Version: webpack 4.46.0
Time: 4905ms
Built at: 2021-07-14 9:31:14 ├F10: PM┤
Asset Size Chunks Chunk Names
index.html 417 bytes [emitted]
main.0241bb73c4.css 13 bytes main [emitted] [immutable] main
main.8983191438.js 5.22 KiB main [emitted] [immutable] main
再次通过浏览器访问,发现 css 请求了新的文件,而 js 还是来自缓存。
Tip: 是否要将 hash 清除?
注:此刻运行 npm run build 会报错,为了不影响下面的介绍,所以将 hash 去除,source map 也不需要,一并删除。
ERROR in chunk main [entry]
Cannot use [chunkhash] or [contenthash] for chunk in 'main.[contenthash:10].js' (use [hash] instead)
tree shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。
使用树摇非常简单,只需要满足两个条件:
- 使用 es6 模块化
- 模式(mode)开启production
直接演示,请看:
a.js 中导出 a 和 b,但在index.js 中只使用了a:
// a.js
export let a = 'hello'
export let b = 'jack'
// index.js
import { a } from './a.js'
console.log(a);
首先在开发模式下测试,发现 a.js 中的”hello“和”jack“都打包进去了,请看示例:
module.exports = {
mode: 'development',
}
// dist/main.js
// a 和 b 都被打包进来,尽管 b 没有被用到
var a = 'hello';
var b = 'jack';
而在生成模式下,只有用到的 a 才被打包进去,请看示例:
module.exports = {
mode: 'production',
}
// dist/main.js
// 只找到 hello,没有找到 jack
console.log("hello")
将文件标记为 side-effect-free(无副作用)
在一个纯粹的 ESM 模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler 哪些代码是“纯粹部分”。
通过 package.json 的 "sideEffects" 属性,来实现这种方式。
{
"sideEffects": false
}
如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack 它可以安全地删除未用到的 export。
Tip:"side effect(副作用)" 的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。
我们通过一个例子说明下:
在入口文件引入 css 文件:
// index.js
import './a.css'
import { a } from './a.js'
console.log(a);
// a.css
p{color:yellow;}
// webapck.config.js
mode: 'production'
打包会生成 css:
> npm run build
Asset Size Chunks Chunk Names
index.html 342 bytes [emitted]
main.css 13 bytes 0 [emitted] main
main.js 1.3 KiB 0 [emitted] main
在 package.json 添加 "sideEffects": false
,标注所有代码都不包含副作用:
{
"sideEffects": false
}
再次打包,则不会生成 css:
> npm run build
Asset Size Chunks Chunk Names
index.html 303 bytes [emitted]
main.js 1.3 KiB 0 [emitted] main
注:所有导入文件都会受到 tree shaking 的影响。这意味着,如果在项目中使用类似 css-loader 并 import 一个 CSS 文件,则需要将其添加到 side effect 列表中,以免在生产模式中无意中将它删除:
// package.json
{
"sideEffects": [
"*.css",
"*.less"
]
}
代码分割
将一个文件分割成多个,加载速度可能会更快,而且分割成多个文件后,还可以实现按需加载。
optimization.splitChunks
对于动态导入模块,默认使用 webpack v4+ 提供的全新的通用分块策略(common chunk strategy) —— SplitChunksPlugin。
开箱即用的 SplitChunksPlugin 对于大部分用户来说非常友好。
webpack 将根据以下条件自动拆分 chunks:
- 新的 chunk 可以被共享,或者模块来自于 node_modules 文件夹
- 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积)
- 当按需加载 chunks 时,并行请求的最大数量小于或等于 30
- 当加载初始化页面时,并发请求的最大数量小于或等于 30
Tip: SplitChunksPlugin的默认配置如下:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
默认配置很多,如果我们不需要修改,则不用管它们,下面我们来体验一下 splitChunks.chunks:
Tip:splitChunks.chunks,表明将选择哪些 chunk 进行优化。当提供一个字符串,有效值为 all,async 和 initial。设置为 all 可能特别强大,因为这意味着 chunk 可以在异步和非异步 chunk 之间共享。
> npm i lodash@4
// index.js
import _ from 'lodash';
console.log(_);
打包只生成一个 js:
> npm run build
Asset Size Chunks Chunk Names
index.html 303 bytes [emitted]
main.js 72.7 KiB 0 [emitted] main
配置splitChunks.chunks:
// webapck.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
再次打包,这次生成两个 js,其中Chunk Names 是 vendors~main 对应的就是 loadsh:
> npm run build
Asset Size Chunks Chunk Names
1.main.js 71.5 KiB 1 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 1.9 KiB 0 [emitted] main
同一个 chunk 中,如果 index.js 和 a.js 都引入 loadash,会如何打包?请看示例:
// index.js
import {a} from './a.js'
import _ from 'lodash';
console.log(a)
console.log(_);
// a.js
export let a = 'hello'
export let b = 'jack'
> npm run build
Asset Size Chunks Chunk Names
1.main.js 71.5 KiB 1 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 1.92 KiB 0 [emitted] main
同样是两个 js,而且 loadash 应该是公用了,因为 main.js 较上次只增加了 0.02 kb。
动态导入
使用动态导入可以分离出 chunk。
请看示例:
上文我们知道,这段代码打包会生成两个 js,其中 main.js 包含了 a.js。
// index.js
import {a} from './a.js'
import _ from 'lodash';
console.log(a)
console.log(_);
将其中的 a.js 改为动态导入的方式:
// index.js
import _ from 'lodash';
// 动态导入
import(/* webpackChunkName: 'a' */'./a').then((aModule) => {
console.log(aModule.a);
});
console.log(_);
打包:
> npm run build
Asset Size Chunks Chunk Names
0.main.js 192 bytes 0 [emitted] a
2.main.js 94.6 KiB 2 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 2.75 KiB 1 [emitted] main
其中 a.js 被单独打包成一个js(从 Chunk Names 为 a 可以得知)
懒加载
懒加载就是用到的时候在加载。
请看示例:
我们在入口文件注册一个点击事件,只有点击时才加载 a.js。
// index.js
document.body.onclick = function () {
// 动态导入
import(/* webpackChunkName: 'a' */'./a').then((aModule) => {
console.log(aModule.a);
});
};
// a.js
console.log('moduleA');
export let a = 'hello'
export let b = 'jack'
启动服务,测试:
> npm run dev
第一次点击:moduleA hello
第二次点击:hello
只有第一次点击,才会请求 a.js 模块。
Tip:懒加载其实用到的就是上文介绍的动态导入
预获取
思路可能是这样:
- 首先使用普通模式
- 普通模式下,一次性加载太多,而 a.js 这个文件又有点大,于是就使用懒加载,需要使用的时候在加载 a.js
- 触发点击事件,懒加载 a.js,但 a.js 很大,需要等待好几秒中才触发,于是我想预获取来减少等待的时间
将懒加载改为预获取:
// index.js
document.body.onclick = function () {
// 动态导入
import(/* webpackChunkName: 'a', webpackPrefetch: true*/'./a').then((aModule) => {
console.log(aModule.a);
});
};
刷新浏览器,发现 a.js 被加载了;触发点击事件,输出 moduleA hello,再次点击,输出 hello。
Tip:浏览器中有如下一段代码:
// 指示着浏览器在闲置时间预取 0.main.a3f7d94cb1.js
<link rel="prefetch" as="script" href="0.main.a3f7d94cb1.js">
预获取和懒加载的不同是,预获取会在空闲的时候先加载。
渐进式网络应用程序
渐进式网络应用程序(progressive web application - PWA),是一种可以提供类似于 native app(原生应用程序) 体验的 web app(网络应用程序)。PWA 可以用来做很多事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。这是通过使用名为 Service Workers 的 web 技术来实现的。
我们首先通过一个包来启动服务:
> npm i -D http-server@0
// package.json
{
"scripts": {
"start": "http-server dist"
},
}
> npm run build
启动服务:
> npm run start
> webpack-example3@1.0.0 start
> http-server dist
Starting up http-server, serving dist
Available on:
http://192.168.85.1:8080
http://192.168.75.1:8080
http://192.168.0.103:8080
http://127.0.0.1:8080
Hit CTRL-C to stop the server
注:多个 url 与适配器有关:
> ipconfig
以太网适配器 VMware Network Adapter VMnet1:
IPv4 地址 . . . . . . . . . . . . : 192.168.85.1
以太网适配器 VMware Network Adapter VMnet8:
IPv4 地址 . . . . . . . . . . . . : 192.168.75.1
无线局域网适配器 WLAN:
IPv4 地址 . . . . . . . . . . . . : 192.168.0.103
通过浏览器访问 http://127.0.0.1:8080
。如果我们将服务器关闭,再次刷新页面,则不能再访问。
接下来我们要做的事:通过离线技术让网页再服务器关闭时还能访问。
请看示例:
添加 workbox-webpack-plugin 插件,然后调整 webpack.config.js 文件:
> npm i -D workbox-webpack-plugin@6
// webapck.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
};
完成这些设置,再次打包,看下会发生什么:
> npm run build
Asset Size Chunks Chunk Names
0.main.js 192 bytes 0 [emitted] a
2.main.js 94.6 KiB 2 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 2.75 KiB 1 [emitted] main
service-worker.js 1.11 KiB [emitted]
workbox-15dd0bab.js 13.6 KiB [emitted]
生成了两个额外的文件:service-worker.js 和 workbox-15dd0bab.js。service-worker.js 是 Service Worker 文件。
值得高兴的是,我们现在已经创建出一个 Service Worker。接下来我们注册 Service Worker。
// index.js
document.body.onclick = function () {
// 动态导入
import(/* webpackChunkName: 'a', webpackPrefetch: true*/'./a').then((aModule) => {
console.log(aModule.a);
});
};
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
再次运行 npm run build
来构建包含注册代码版本的应用程序。然后用 npm start 启动服务。访问 http://127.0.0.1:8080/ 并查看 console 控制台。在那里你应该看到:
SW registered
Tip:如果没有看见 SW registered,可以尝试强刷
现在来进行测试。停止 server 并刷新页面。如果浏览器能够支持 Service Worker,应该可以看到你的应用程序还在正常运行。然而,server 已经停止 serve 整个 dist 文件夹,此刻是 Service Worker 在进行 serve。
Tip:更过 pwa 可以参考 "mdn 渐进式应用程序";淘宝(taobao.com)以前有 pwa,现在却没有了。
多进程打包
通过多进程打包,用的好可以加快打包的速度,用得不好甚至会更慢。
这里使用一个名为 thread-loader 包来做多进程打包。每个 worker 是一个单独的 node.js 进程,开销约 600 毫秒,还有一个进程间通信的开销。
注:仅将此加载器用于昂贵的操作!比如 babel
我们演示一下:
未使用多进程打包时间是 3122ms:
// index.js
import _ from 'lodash'
console.log(_);
> npm run build
Hash: a4868f457d7ce754335b
Version: webpack 4.46.0
Time: 3031ms
加入多线程:
> npm i -D thread-loader@3
// webpack.config.js -> module.exports -> module.rules
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'thread-loader',
{
loader: 'babel-loader',
...
}
]
}
> npm run build
Hash: a4868f457d7ce754335b
Version: webpack 4.46.0
Time: 3401ms
构建时间更长。
Tip: 可能是代码中需要 babel 的 js 代码太少,所以导致多线程效果不明显。
外部扩展(externals)
externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。
externals
防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。
例如 jQuery 这个库来自 cdn,则不需要将 jQuery 打包。请看示例:
Tip: 为了测试看得更清晰,注释掉 pwa 和 splitChunks。
> npm i jquery@3
// index.js
import $ from 'jquery';
console.log($);
打包生成一个 js,其中包含了 jquery:
> npm run build
Asset Size Chunks Chunk Names
1.main.js 88 KiB 1 [emitted] vendors~main
index.html 336 bytes [emitted]
main.js 1.9 KiB 0 [emitted] main
由于开启了 splitChunks,这里 1.main.js 就是 jquery。
使用 external 将 jQuery 排除:
// webpack.config.js
module.exports = {
externals: {
// jQuery 是jquery暴露给window的变量名,这里可以将 jQuery 改为 $,但 jquery 却不行
jquery: 'jQuery'
}
};
在 index.html 中手动引入 jquery:
// src/index.html
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
Tip: 我们使用 bootstrap cdn。
再次打包,则不在包含 jquery:
> npm run build
Asset Size Chunks Chunk Names
index.html 303 bytes [emitted]
main.js 1.35 KiB 0 [emitted] main
Tip:如果你在开发模式(mode: 'development'
)下打包,你会发现 main.js 中会有如下这段代码:
/***/ "jquery":
/*!*************************!*\
!*** external "jQuery" ***!
\*************************/
/*! no static exports found */
/***/ (function(module, exports) {
eval("module.exports = jQuery;\n\n//# sourceURL=webpack:///external_%22jQuery%22?");
/***/ })
这里的 jQuery 来自我们手动通过 <script src=>
引入 jquery 所产生的全局变量。
动态链接(dll)
所谓动态链接,就是把一些经常会共享的代码制作成 DLL 档,当可执行文件调用到 DLL 档内的函数时,Windows 操作系统才会把 DLL 档加载存储器内,DLL 档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。
对于 webpack 就是事先将常用又构建时间长的代码提前打包好,取名为 dll,后面打包时则直接使用 dll,用来提高打包速度
vue-cli 删除了 dll
在 vue-cli 提交记录中发现:remove DLL option。
原因是:dll 选项将被删除。 Webpack 4 应该提供足够好的性能,并且在 Vue CLI 中维护 DLL 模式的成本不再合理。
Tip: 详情请看issue
核心代码
附上项目最终核心文件,方便学习和解惑。
webapck.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
process.env.NODE_ENV = 'development'
const postcssLoader = {
loader: 'postcss-loader',
options: {
// postcss 只是个平台,具体功能需要使用插件
// Set PostCSS options and plugins
postcssOptions: {
plugins: [
// 配置插件 postcss-preset-env
[
"postcss-preset-env",
{
// browsers: 'chrome > 10',
// stage:
},
],
]
}
}
}
module.exports = {
entry: './src/index.js',
entry: ['./src/index.js', './src/index.html'],
output: {
filename: 'main.js',
// filename: 'main.[contenthash:10].js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/i,
// 将 style-loader 改为 MiniCssExtractPlugin.loader
use: [MiniCssExtractPlugin.loader, "css-loader", postcssLoader],
},
{
test: /\.less$/i,
loader: [
// 将 style-loader 改为 MiniCssExtractPlugin.loader
MiniCssExtractPlugin.loader,
"css-loader",
postcssLoader,
"less-loader",
],
},
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
// 指定文件的最大大小(以字节为单位)
limit: 1024 * 6,
},
},
],
},
// +
{
test: /\.html$/i,
loader: 'html-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 'thread-loader',
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
// +
{
// 配置处理polyfill的方式
useBuiltIns: "usage",
// 版本与我们下载的版本保持一致
corejs: { version: "3.11" },
"targets": "> 0.25%, not dead"
}
]
],
// 开启缓存
cacheDirectory: true
}
}]
}
]
},
plugins: [
// new MiniCssExtractPlugin(),
new MiniCssExtractPlugin({
// filename: "[name].[contenthash:10].css",
}),
new OptimizeCssAssetsPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
// new ESLintPlugin({
// // 将启用ESLint自动修复功能。此选项将更改源文件
// fix: true
// }),
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
mode: 'development',
// mode: 'production',
devServer: {
// open: true,
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
},
devServer: {
// 开启热模块替换
hot: true
},
// devtool: 'eval-source-map',
optimization: {
splitChunks: {
chunks: 'all',
},
},
externals: {
// jQuery 是jquery暴露给window的变量名,这里可以将 jQuery 改为 $,但 jquery 却不行
jquery: 'jQuery'
}
};
package.json
{
"name": "webpack-example3",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"dev": "webpack-dev-server",
"start": "http-server dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/preset-env": "^7.14.2",
"babel-loader": "^8.2.2",
"core-js": "3.11",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-webpack-plugin": "^2.5.4",
"express": "^4.17.1",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.2",
"http-server": "^0.12.3",
"less-loader": "^7.3.0",
"mini-css-extract-plugin": "^1.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.3.0",
"postcss-preset-env": "^6.7.0",
"thread-loader": "^3.0.4",
"url-loader": "^4.1.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2",
"workbox-webpack-plugin": "^6.1.5"
},
"dependencies": {
"jquery": "^3.6.0",
"lodash": "^4.17.21",
"vue": "^2.6.14"
},
"sideEffects": false
}
其他章节请看:
出处:https://www.cnblogs.com/pengjiali/p/15024605.html
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。