Webpack 4.0 学习笔记(八)
phao 在路上的前端开发

一、前言

如果觉得本文内有些代码或路径编写让人感觉比较绕的,建议把项目拉到本地,参照着项目结构去阅读文章。

项目结构

本文会以 demo08 作为项目文件夹,各文件直接拿之前项目 demo07 的,接下来做些修改。

package.json
1
2
3
4
{
"name": "demo08",
...
}
package-lock.json
1
2
3
4
{
"name": "demo08",
...
}
webpack.common.js
1
2
3
4
5
6
7
8
9
10
11
12
...
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
...
title: 'demo08自定义title'
}),
...
],
...
}

为了方便后面案例的讲述,去除 webpack.ProvidePlugin 这个插件的使用。

webpack.common.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
// const webpack = require('webpack')

module.exports = {
...
plugins: [
...
// new webpack.ProvidePlugin({
// $: 'jquery',
// _join: ['lodash', 'join']
// })
],
...
}

在本文中一些目录及文件不需要 ESLint 去进行代码检测。

.eslintignore
1
2
3
...
dist
dll
jquery.ui.js(原文件内容清空)
1
2
3
4
5
import $ from 'jquery'

export default function ui() {
$('body').css('background', 'green')
}
index.js(原文件内容清空)
1
2
3
4
5
6
7
8
9
import $ from 'jquery'
import _ from 'lodash'
import ui from './jquery.ui'

ui()

const dom = $('<div>');
dom.html(_.join(['a', 'b', 'c'], ' -- '))
$('body').append(dom)

安装依赖

1
npm ci

二、Webpack 性能优化

跟上技术迭代

在项目开发中尽可能去使用新版本的 Webpack、node 以及 npm、yarn 等包管理工具。新版本的工具能够利用一些特性去提高我们的打包速度。

在尽可能少的模块上使用 Loader

一般在打包 js 文件时,我们会通过 babel 去进行转译,但在引入第三方模块的 js 文件,该文件已经被打包编译过了,再对它进行一次转译是没有意义的,只会降低打包的速度。

我们可以在打包 js 文件时进行一些设置来避免对第三方模块 js 文件的再次转译。

1
2
3
4
5
6
7
8
9
10
rules:[
{
test: /\.js$/,
// 不对 node_modules 目录下的 js 文件进行处理。
exclude: /node_modules/,
// 也有 include 这种写法,当打包遇到 js 文件时,只有包含在 src 目录下才会去进行 babel 语法的转译。
// include: path.resolve(__dirname, '../src'),
loader: 'babel-loader'
}
]

当 exclude 和 include 同时存在,会包含 include 的所有内容,不包含 exclude 的所有内容,但如果 exclude 和 include 存在相同的内容,就会引起报错。

Plugin 尽可能精简并确保可靠

例如之前文章的案例中,对 css 压缩处理只在生产环境去使用该插件,开发环境并不需要该插件,这时候开发环境就节约了代码压缩这部分的打包时间。

插件的选择上,一般会使用 Webpack 官网推荐的插件,这些插件的性能经过了官方的测试,是比较快的,以及可以选择社区认可的插件来使用。

resolve 参数的合理配置

extensions

在一些项目开发中,我们可能会遇到过这样一种情况,引入文件不需要写后缀名也能成功引入。例如本文中的代码,index.js 对 jquery.ui.js 的引入,写的是 import $ form './jquery.ui' 而不是 import $ form './jquery.ui.js'

Webpack 默认支持省略引入文件 js 后缀名的编写,可以通过在 Webpack 的打包配置文件里设置 resolve 配置项实现其他文件后缀名的省略。

webpack.common.js
1
2
3
4
5
6
7
8
9
10
11
12
...
module.exports = {
entry: {
...
},
resolve: {
// 当业务代码在引入其他模块的时候,会先在该目录下找以 js 为后缀名的文件,
// 找不到的话再去找以 vue 结尾的文件。
extensions: ['.js', '.vue']
},
...
}

当我们配置了太多的文件后缀名,例如:extensions: ['.jpg', '.css', '.js', '.vue'],就意味着每当引入一个文件就需要进行很多次的查找,实际上是有性能损耗的,所以一般去引入一些逻辑性的文件,例如 js、vue 才会去进行相应的设置。

mainFiles

假设现在 src 目录下有个 child 目录,child 目录下有个 index.js,那么 src 目录下的 index.js 通过 import child form ./child/ 就可以引入 child 目录下的 index.js,因为 Webpack 默认进行了相应的配置,我们可以通过设置 mainFiles 去实现其他文件名的省略。

1
2
3
4
5
6
resolve: {
...
// 当引入一个目录的时候,不知道引入的具体文件,
// 会先去找以 index 命名的文件,找不到再去找以 child 命名的文件。
mainFiles: ['index', 'child']
}

当 mainFiles 配置了太多的文件名,同样存在性能上的问题。一般不需要去设置 mainFiles,默认找 index 命名的文件即可。

alias

假设现在 src 目录下的 index.js 引入这样一个 child 目录下的 index.js 文件:

index.js(仅举例)
1
import child form './a/b/c/child/index.js'

引入的文件路径比较长,我们就可以进行 alias 配置项的设置。

webpack.common.js(仅举例)
1
2
3
4
5
6
7
8
resolve: {
...
alias: {
// 因为该打包配置文件位于与 src 目录同级的 build 目录下,所以得先通过 ../src 找到 src 目录。
// index.js 就不用写了,Webpack 默认的 mainFiles 配置就能找到。
child: path.resolve(__dirname, '../src/a/b/c/child')
}
}

这时候 index.js 内只需这样引入:

index.js(仅举例)
1
import child form 'child'

控制打包文件大小

当在项目中引入了一些模块却没有使用,就需要配置 Tree Shaking 或者手动去除引入,减小打包体积。

还可以通过 Code Splitting 把大文件拆分为几个小文件来提高 Webpack 打包速度。

合理使用 SourceMap

我们需要思考在不同环境下使用什么样的 SourceMap 是最合适的,结合业务场景去使用。

结合 stats 分析打包结果

通过运行命令把打包过程及结果存储到 stats.json,把该文件结合线上或者本地的打包分析工具,查看哪些打包模块耗时比较久,体积比较大,来做相应的优化。

多进程打包

Webpack 默认是通过 NodeJS 来运行的,是个单进程的打包过程,有时候可以借助 node 中的多进程来帮助我们提高打包速度,接触 thread-loader、parallel-webpack、happypack 这些,使用到 node 中的多进程,同时使用多个 cpu 进行项目打包,提高打包速度。

课程内没有具体展开去讲,对 thread-loader、parallel-webpack、happypack 这些多进程打包工具感兴趣的小伙伴可以去搜索相关文档。

开发环境使用 DDLPlugin 提高打包速度

当项目中引入了第三方模块后对打包速度会有些影响,每次重新打包都需要重新去分析这些第三方模块进行打包,现在我们的项目 demo08 中引入了 lodash 以及 jquery,运行开发环境打包命令进行打包,打包三四次观察打包耗时,都在 1600ms 以上。

1
npm run dev-build

我们希望项目中引入的第三方模块只在第一次打包时做分析,打包到一个 js 文件里,第二第三次就没必要重新分析了,直接拿第一次打包生成的 js 文件使用,提高打包速度。

在 build 目录下新建 webpack.dll.js 做第三方模块打包用的配置文件。

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

module.exports = {
mode: 'development',
entry: {
vendors: ['jquery', 'lodash']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
// 打包生成的 vendors.dll.js,将该文件内所有内容通过 vendors 这个全局变量暴露出去。
library: '[name]'
}
}

package.json 新增 scripts 脚本命令打包第三方模块

package.json
1
2
3
4
5
6
7
8
{
...
"scripts": {
...
"build:dll": "webpack --config ./build/webpack.dll.js"
},
...
}

运行第三方模块打包命令

1
npm run build:dll

项目根路径下生成 dll 目录以及 dll 目录下 vendors.dll.js,该 js 文件内就打包了我们项目中需要的第三方模块 jquery 以及 lodash。

这时候需要 dist 目录下 index.html 引入 vendors.dll.js 进行使用,安装插件 add-asset-html-webpack-plugin。

1
npm install add-asset-html-webpack-plugin@3.1.2 -D

在开发环境使用该插件

webpack.dev.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
const path = require('path')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')

const devConfig = {
...
plugins: [
...
new AddAssetHtmlWebpackPlugin({
// 往 HtmlWebpackPlugin 生成的 index.html 添加打包好的第三方模块。
filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
})
],
...
}
...

运行开发环境打包命令,把生成的 dist 目录下 index.html 放入浏览器,在控制台打印 vendors,能够正常打印结果。

但现在项目内去获取第三方模块还是到 node_modules 目录下取,想要做到引入第三方模块时使用我们打包的 dll 文件引入,还需在打包 dll 文件时做一个映射。

webpack.dll.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
const webpack = require('webpack')

module.exports = {
...
entry: {
vendors: ['jquery', 'lodash']
},
output: {
...
},
plugins: [
// 这里的占位符 [name] 取值为 vendors,是因为配置的 entry 入口名为 vendors。
new webpack.DllPlugin({
name: '[name]',
// 把库里面一些第三方模块的映射关系放到 vendors.manifest.json。
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}

重新运行第三方模块打包命令,dll 目录下除了 vendors.dll.js,还新增了 vendors.manifest.json 这个映射文件。

接着在开发环境使用一个引用 dll 的插件

webpack.dev.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
const devConfig = {
...
plugins: [
...
// 使用该插件后,在打包项目代码的时候发现里面引入了一些第三方模块,
// 就会到 vendors.manifest.json 去找这些映射关系,当找到映射关系,就知道这个模块没必要打包进来,
// 直接从 vendors.dll.js 拿过来用就行,它底层会去全局变量拿。
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
],
...
}
...

这时候重新运行开发环境打包命令,能够发现确实比一开始的打包速度快了一些,感知不明显是因为我们案例涉及代码量太少。

模块拆分

当 dll 文件想要做模块的拆分,可以进行相应的配置。

webpack.dll.js
1
2
3
4
5
6
7
8
9
...
module.exports = {
...
entry: {
vendors: ['jquery'],
lodash: ['lodash']
},
...
}

运行第三方模块打包命令,dll 目录下新增 lodash.dll.js 以及 lodash.manifest.json。

接着配置下开发环境的打包配置文件,还是上面那套流程,需要在打包后生成的 index.html 内引入 lodash.dll.js 以及分析 lodash.manifest.json 内映射关系。

webpack.dev.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
const devConfig = {
...
plugins: [
...
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/lodash.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/lodash.manifest.json')
})
],
...
}
...

在大型项目中,打包生成的 dll 文件也许会很多,就需要不断地配置 AddAssetHtmlWebpackPlugin 以及 webpack 的 DllReferencePlugin。这时候我们可以换一种写法,通过 node 去分析 dll 目录下有几个文件,动态地往 plugins 里添加相应的插件配置。

webpack.dev.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
27
28
29
30
31
32
33
34
35
...
const fs = require('fs')

const plugins = [
new webpack.HotModuleReplacementPlugin()
]

// 把 dll 目录内文件名放入一数组
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))

files.forEach(file => {
// 匹配 xxx.dll.js
if (/.*\.dll\.js/.test(file)) {
plugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
})
)
}
// 匹配 xxx.manifest.json
if (/.*\.manifest\.json/.test(file)) {
plugins.push(
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
})
)
}
})

const devConfig = {
...
plugins,
...
}
...

运行开发环境打包命令,正常打包。

开发环境内存编译(devServer)

在开发环境中我们使用 WebpackDevServer,它不会生成 dist 目录,而是把编译的文件放到内存里,内存读取肯定比硬盘读取快得多,因此通过 WebpackDevServer,开发中打包的性能得到了很大提升。

开发环境无用插件剔除

开发环境把 mode 设为 development,Webpack 打包代码不会去进行压缩,方便我们调试,没有意义的压缩代码只会让打包速度下降。以及开发环境中不需要对 css 代码压缩处理等等。

小结

提高 Webpack 打包性能的方法及配置非常多,需要我们在未来的实战配置中逐步积累经验。

三、多页面打包配置

像我们现在常用的 vue 这些主流的框架,都是单页面应用开发,但在涉及一些老项目,我们需要去了解下 Webpack 多页面的打包配置。

首先,在 src 目录下新建 list.js 以及 detail.js。

list.js
1
document.write('list')
detail.js
1
document.write('detail')

修改打包配置文件 webpack.common.js

webpack.common.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
...
module.exports = {
entry: {
// 原来写法
// main: './src/index.js'

// 配置多个入口文件
index: './src/index.js',
list: './src/list.js',
detail: './src/detail.js'
},
...
plugins: [
// 原来写法
// new HtmlWebpackPlugin({
// template: 'src/index.html',
// title: 'demo07自定义title'
// }),

// 打包生成多个 html 文件
new HtmlWebpackPlugin({
// 生成 html 的模板
template: 'src/index.html',
// 生成文件名
filename: 'index.html',
// 生成 html 文件内可能会引入的 chunk,也就是要引入的 js 文件,
// runtime 是之前抽离出来的 manifest 相关代码,
// vendors 是下面配置的 cacheGroups 里 vendors 缓存组内设置的 name: 'vendors',
// index、list、detail 是入口 entry 配置的名字。
chunks: ['runtime', 'vendors', 'index']
}),
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'list.html',
chunks: ['runtime', 'vendors', 'list']
}),
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'detail.html',
chunks: ['runtime', 'vendors', 'detail']
}),
...
],
optimization: {
...
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors'
}
}
}
},
...
}

运行生产环境打包命令

1
npm run build

这时候就完成了多页面的打包配置,达到的效果是 dist 目录下生成 index.html、list.html、detail.html 三个 html 文件。每个 html 文件除了都引入需要的 runtime、vendors 这两个 chunk 相关 js 文件,还会去引入自身需要的 js 文件,index.html 引入 index,list.html 引入 list,detail.html 引入 detail。

各 html 文件放入浏览器能够正常显示页面效果,证明打包是成功的。

除了这种写法,我们还可以通过 node 去读取配置文件里的 entry,动态往 plugins 里添加相应的插件配置。

webpack.common.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
...
const configs = {
entry: {
index: './src/index.js',
list: './src/list.js',
detail: './src/detail.js'
},
resolve: {
...
},
module: {
...
},
// plugins: [
// ...
// ],
optimization: {
...
},
performance: false,
output: {
...
}
}

// 读取 entry,动态添加 plugins 内容。
const makePlugins = (configs) => {
const plugins = [
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
]

// Object.keys(configs.entry) 打印内容为 ['index', 'list', 'detail']
Object.keys(configs.entry).forEach(item => {
plugins.push(
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: `${item}.html`,
chunks: ['runtime', 'vendors', item]
})
)
})

return plugins
}

configs.plugins = makePlugins(configs)

module.exports = configs

四、总结

通过以上学习,我们了解了怎样对 Webpack 的打包性能进行优化以及多页面应用的打包配置。

在我们进行 Webpack 的配置时,不仅仅就是去编写一些配置,还可以通过 node 的语法去增加一些逻辑进行处理,这样我们的打包配置就非常灵活了。

这次并没有专门一个小节去推官方文档建议看的内容,大家可以结合本文涉及的知识点,对有疑惑或者感兴趣的地方去网上进行拓展学习。

本文最后的代码我会上传到 码云(gitee.com/phao97)上,项目文件夹为 demo08。

如果觉得本篇文章对你有帮助,不妨点个赞或者给相关的 Git 仓库一个 Star,你的鼓励是我持续更新的动力!

  • 本文标题:Webpack 4.0 学习笔记(八)
  • 本文作者:phao
  • 创建时间:2022-05-19 19:37:28
  • 本文链接:http://phaode.cn/2022/05/19/Webpack-4-0-学习笔记(八)/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!