从零开始学 Webpack 4.0(八)
原文本人写于 2022-05-19 19:37:28
一、前言
如果觉得本文内有些代码或路径编写让人感觉比较绕的,建议把项目拉到本地,参照着项目结构去阅读文章。
项目结构
本文会以 demo08 作为项目文件夹,各文件直接拿之前项目 demo07 的,接下来做些修改。

package.json
{
"name": "demo08",
...
}
package-lock.json
{
"name": "demo08",
...
}
webpack.common.js
...
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
...
title: 'demo08自定义title'
}),
...
],
...
}
为了方便后面案例的讲述,去除 webpack.ProvidePlugin 这个插件的使用。
webpack.common.js
...
// const webpack = require('webpack')
module.exports = {
...
plugins: [
...
// new webpack.ProvidePlugin({
// $: 'jquery',
// _join: ['lodash', 'join']
// })
],
...
}
在本文中一些目录及文件不需要 ESLint 去进行代码检测。
.eslintignore
...
dist
dll
jquery.ui.js(原文件内容清空)
import $ from 'jquery'
export default function ui() {
$('body').css('background', 'green')
}
index.js(原文件内容清空)
import $ from 'jquery'
import _ from 'lodash'
import ui from './jquery.ui'
ui()
const dom = $('<div>');
dom.html(_.join(['a', 'b', 'c'], ' -- '))
$('body').append(dom)
安装依赖
npm ci
二、Webpack 性能优化
跟上技术迭代
在项目开发中尽可能去使用新版本的 Webpack、node 以及 npm、yarn 等包管理工具。新版本的工具能够利用一些特性去提高我们的打包速度。
在尽可能少的模块上使用 Loader
一般在打包 js 文件时,我们会通过 babel 去进行转译,但在引入第三方模块的 js 文件,该文件已经被打包编译过了,再对它进行一次转译是没有意义的,只会降低打包的速度。
我们可以在打包 js 文件时进行一些设置来避免对第三方模块 js 文件的再次转译。
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
...
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 去实现其他文件名的省略。
resolve: {
...
// 当引入一个目录的时候,不知道引入的具体文件,
// 会先去找以 index 命名的文件,找不到再去找以 child 命名的文件。
mainFiles: ['index', 'child']
}
当 mainFiles 配置了太多的文件名,同样存在性能上的问题。一般不需要去设置 mainFiles,默认找 index 命名的文件即可。
alias
假设现在 src 目录下的 index.js 引入这样一个 child 目录下的 index.js 文件:
index.js(仅举例)
import child form './a/b/c/child/index.js'
引入的文件路径比较长,我们就可以进行 alias 配置项的设置。
webpack.common.js(仅举例)
resolve: {
...
alias: {
// 因为该打包配置文件位于与 src 目录同级的 build 目录下,所以得先通过 ../src 找到 src 目录。
// index.js 就不用写了,Webpack 默认的 mainFiles 配置就能找到。
child: path.resolve(__dirname, '../src/a/b/c/child')
}
}
这时候 index.js 内只需这样引入:
index.js(仅举例)
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 以上。
npm run dev-build

我们希望项目中引入的第三方模块只在第一次打包时做分析,打包到一个 js 文件里,第二第三次就没必要重新分析了,直接拿第一次打包生成的 js 文件使用,提高打包速度。
在 build 目录下新建 webpack.dll.js 做第三方模块打包用的配置文件。
webpack.dll.js
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
{
...
"scripts": {
...
"build:dll": "webpack --config ./build/webpack.dll.js"
},
...
}
运行第三方模块打包命令
npm run build:dll
项目根路径下生成 dll 目录以及 dll 目录下 vendors.dll.js,该 js 文件内就打包了我们项目中需要的第三方模块 jquery 以及 lodash。
这时候需要 dist 目录下 index.html 引入 vendors.dll.js 进行使用,安装插件 add-asset-html-webpack-plugin。
npm install add-asset-html-webpack-plugin@3.1.2 -D
在开发环境使用该插件
webpack.dev.js
...
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
...
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
...
const devConfig = {
...
plugins: [
...
// 使用该插件后,在打包项目代码的时候发现里面引入了一些第三方模块,
// 就会到 vendors.manifest.json 去找这些映射关系,当找到映射关系,就知道这个模块没必要打包进来,
// 直接从 vendors.dll.js 拿过来用就行,它底层会去全局变量拿。
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
],
...
}
...
这时候重新运行开发环境打包命令,能够发现确实比一开始的打包速度快了一些,感知不明显是因为我们案例涉及代码量太少。
模块拆分
当 dll 文件想要做模块的拆分,可以进行相应的配置。
webpack.dll.js
...
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
...
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
...
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
document.write('list')
detail.js
document.write('detail')
修改打包配置文件 webpack.common.js
webpack.common.js
...
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'
}
}
}
},
...
}
运行生产环境打包命令
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
...
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,你的鼓励是我持续更新的动力!
转载自:https://juejin.cn/post/7216624116337279036