Webpack 4 迁移 RFC
- Start Date: 2022-12-05
- Target Major Version: v2
- Reference Issues: none
- Implemention PR:
Summary
-
将项目的构建工具从
webpack 3
迁移至webpack 4
,并将各相关 webpack 插件升级到webpack 4
兼容版本。 -
优化构建效率,包括用
esbuild
代替babel
、修改webpack
配置、引入插件等,将项目开发环境冷启动时间和生产打包时间缩短约 50%。 -
尝试将 Vue 2 迁移到
vue^2.7.x
稳定版本。- 由于迁移 Vue 并非构建效率或者基础架构上的优化,所以分支上未提交相关改动
Motivation
构建和 HMR 效率低下
除大页面外,v2 承担了桌面端的绝大部分业务,已经具有相当的复杂度,且随着业务全量迁移到 v2,项目的体积和复杂度会不断增长。
项目庞大的体量导致,在开发阶段npm run dev
进行的冷启动时间,以及生产构建npm run build
进行的项目打包耗时都会越来越长。
除冷启动速率外,在webpack 3
下,项目 HMR (Hot Module Replacement,模块热替换) 的准确率也不尽人意,体现在对某些更改不能准确保留状态,而触发不必要的 live reload。且由于 webpack 在每次 HMR 时都会重新构建整个项目,原本缓慢的构建时间也会拖慢 HMR 的效率。
工具链生态过旧
v2 使用发布于五年前的[email protected]
进行构建,部分依赖现已不再有人维护,且vue 2.x
在 2023 年年底也将停止维护。应用层框架(vue
、react
)往往依赖于babel
、postcss
等编译时工具,而这些工具在实践中又以loader
、plugins
等形式集成于webpack
,因此,未来若要将框架升级,则应提早完成对webpack
及底层生态的升级。
另外,v2 项目通过 Vue CLI 生成,其配置自带许多当时通用的 boilerplate,但有些地方已经不是当前的最佳实践(如将所有样式打包成一个.css
文件会影响首屏性能等)。在迁移时,通过审阅原有的配置文件,也可优化不合理、移除不必要的部分。
从 vue2.x 到 vue2.7
vue2.7
是 Vue 团队为了将 Vue 3 的特性向后兼容,于 2022 年 7 月发布的 Vue 2 最终版本,提供了 Vue 3 最核心的 Composition API 特性与更好的类型系统。
兼容 Composition API 和
<script setup>
语法糖的意义
- 更好的代码组织,将实现一个业务功能的代码组织在一起,而不是分散到
data()
、computed
、methods
等多个选项中;- 更好的逻辑复用,Composition API 引入的
composables
提供了比mixin
更好的逻辑复用范式;- 更好的类型提示,由于 Composition API 使用函数组织组件,相比以往基于 Class 的方式,类型推断更加准确
- 缩小打包体积,
<script setup>
被编译为函数,可直接通过作用域访问到渲染上下文,相比于原来通过实例化代理对象的访问更快,且一致的变量名更利于压缩。
Detailed design
node.js
版本:^14.20.0
- 由于更新了部分依赖,需先使用
npm ci
安装依赖
构建工具升级
更新webpack
核心依赖
选用发布于两年前的[email protected]
作为目标版本。v4.46.0
是webpack 4
的稳定版本,能较好适配各种为webpack 4
设计的插件,且由于在4.x
中版本较新,能为未来迁移到webpack 5
提供便利。
对照发布时间,分别安装和升级 CLI 与 DevServer 依赖到[email protected]
、[email protected]
。
更新原有webpack
工具链
-
Loader:
babel-loader
:^7.1.1
==>^8.3.0
- 用
esbuild
替代babel
在浏览器兼容上的作用,详情见构建效率优化一节。
- 用
css-loader
:^0.28.0
==>^5.2.7
eslint-loader
:移除。- 在
webpack 4
中被废弃,用eslint-webpack-plugin
代替。
- 在
file-loader
:^1.1.4
==>^6.2.0
postcss-loader
:^2.0.8
==>^4.3.0
url-loader
:^4.0.8
==>^4.1.1
vue-loader
:^13.2.0
==>^15.10.1
vue-loader 15.x
是vue-loader
目前的最高版本,可为将vue 2.x
升级到稳定版本2.7
做铺垫。
vue-style-loader
:^3.0.1
==>^4.1.3
-
Plugins:
copy-webpack-plugin
:^4.0.1
==>^6.4.0
eslint-webpack-plugin
:新增^2.7.0
。- 用于代替
eslint-loader
。
- 用于代替
extract-text-webpack-plugin
:移除。- 在
webpack 4
中被废弃,改用mini-css-extract-plugin
。
- 在
friendly-errors-webpack-plugin
:^1.6.1
==>^1.7.0
html-webpack-plugin
:^2.30.1
==>^4.5.2
mini-css-extract-plugin
:新增^1.6.2
。- 用于替代
extract-text-webpack-plugin
。
- 用于替代
optimize-css-assets-webpack-plugin
:移除。- 改用
esbuild
压缩 css,以获取更快的速度。
- 改用
speed-measure-webpack-plugin
:新增^1.5.0
。- 用于检视各构建流程用时,在迁移完成后可移除。
uglifyjs-webpack-plugin
:移除。- 在
webpack 4
中被废弃,webpack 4
默认使用terser
压缩 JS,但这里改用更快的esbuild
压缩。
- 在
-
Deps:
-
@babel/core
:新增^7.20.5
。babel-loader
要求。
-
@babel/preset-env
:新增^7.20.2
。- 生产模式下,对
jsx
模块进行兼容性处理。
- 生产模式下,对
-
@vue/babel-helper-vue-jsx-merge-props
:新增^1.4.0
。 -
@vue/babel-preset-jsx
:新增^1.4.0
。- 以上两个依赖,用于配合
babel
为element-ui
(以及修改后的el-
组件)提供jsx
转译。
- 以上两个依赖,用于配合
-
autoprefixer
:^7.1.2
==>^9.4.7
-
babel-core
:移除- 在
babel 7.x
后替换为@babel/core
- 在
-
babel-eslint
:移除- 以上两个依赖不再需要。
-
babel-helper-vue-jsx-merge-props
:移除 -
babel-plugin-syntax-jsx
:移除 -
babel-plugin-transform-runtime
:移除 -
babel-plugin-transform-vue-jsx
:移除- 以上与
jsx
相关依赖包含于@vue/babel-preset-jsx
中。
- 以上与
-
babel-preset-env
:移除 -
babel-preset-stage-2
:移除- 较新版本的
babel
改用@babel/preset-env
进行兼容。
- 较新版本的
-
node-notifier
:^5.1.2
==>^8.0.2
-
eslint-plugin-html
:移除。 -
eslint-plugin-import
:移除。 -
eslint-plugin-node
:移除。 -
eslint-plugin-promise
:移除。 -
eslint-plugin-standard
:移除。- 以上 ESLint Plugins 均未在项目中使用到。
-
eslint-plugin-vue
:^5.2.2
==>^9.7.0
-
postcss
:新增^8.4.5
。postcss-loader
要求。
-
postcss-import
:^11.0.0
==>^14.0.2
-
postcss-url
:^7.2.1
==>^10.1.3
-
portfinder
:^1.0.13
==>^1.0.28
-
vue-template-compiler
:^2.5.2
==>^2.7.10
-
配置文件重构
原本的webpack
配置文件(/build/
)通过webpack-merge
实现一种inheritance
(继承)式的配置复用策略,直观呈现为通用(common)、开发(dev)和生产(prod)三个庞大的对象字面量(plain object)。由于WebpackConfig
的配置项繁杂,这种结构的可读性较差。
新的架构在inheritance
的基础上,借鉴 SurviveJS 所提倡的 Config 组织方案,将配置分解为更小的单元,其粒度为配置中启用一个特定功能所对应的部分,如loader
、devServer
等。
并且,Node.js 逐渐支持 ES Module 模块系统,而 ESM 作为一种静态模块系统能提供比 CJS 更好的类型推断能力。虽然 v2 项目本身不使用 TypeScript,但若使用的第三方库编写了类型声明,可以配合 IDE 提供类型提示,故改用 ESM 编写了配置文件。
v2 (before)
├─ build
│ ├─ ...
│ ├─ webpack.base.conf.js // 通用
│ ├─ webpack.dev.conf.js // 开发
│ └─ webpack.prod.conf.js // 生产
v2 (after)
├─ build
│ ├─ ...
│ ├─ webpack.common.mjs // 通用
│ ├─ webpack.dev.mjs // 开发
│ └─ webpack.prod.mjs // 生产
构建效率优化
使用esbuild
代替babel
esbuild
是基于 Go 的新一代 JavaScript 转译器,相比用 JS 编写的babel
,具有性能上的天然优势。
esbuild
为什么比其他 bundler 快?
- 用 Go 编写,并编译成了原生代码
- 常见的 bundler 用 JS 编写,而对于即时编译(JIT-compiled)语言来说,命令行应用(command-line application)是其性能最差的场景。每次运行 bundler 时,JavaScript VM(V8)需要在没有任何优化提示的情况下先编译 bundler 的 JS 源码。而 esbuild 的原生代码产物可以直接运行(即开始打包)。
- 大量使用并发
- 另外,Go 是以并发为核心设计的,它具有共享内存的 IPC 方式。Go 和 JavaScript 都有并行的垃圾回收器,但是 Go 的堆是线程间共享的,而每个 JavaScript 线程有一个单独的堆,这使得 JavaScript 的可行并行量减少了一半,因为一半 的 CPU 核在为另一半收集垃圾。
- esbuild 是从头开始编写的(from scratch)
- esbuild 是以性能为第一需求而从头编写的,相比于使用第三方库,从头编写可以保证所有地方使用的数据结构一致,避免了昂贵的数据结构转换。
- 以 TypeScript 为例,许多 bundler 会使用官方的 tsc 进行转译,但官方的编译器不是以性能为第一追求的,其内部大量使用了 megamorphic object shapes 和不必要的动态属性访问(dynamic property access),大大拖慢 JS 执行速度。
- 对内存的高效使用
- esbuild 只操作 JavaScript AST 三次:
- 词法分析(lexing)、解析、建立作用域和声明标识符;
- 绑定标识符、JSX/TS => JS、ESNEXT => ES2015;
- 最小化(minification)、代码生成、source map 生成。
- 这样的设计可以最大程度地重复利用 AST 数据,因为数据依然活跃在 CPU 缓存(cache)中。
- 而其他 bundler 会串行、而不是交织执行以上任务,且过程中 AST 可能会多次被转换成其他形式:
string -> TS -> JS -> string
、string -> JS -> older JS -> string
、string -> JS -> minified JS -> string
。这会占用更多内存并降低处理速度。
考虑到 v2 项目本身的适配对象是基于 Chromium 的 Chrome、Edge 等现代浏览器,契合esbuild
本身支持的最低编译目标es2015
,因此改用esbuild
转译 JS 可行。
在代替babel
时,将原来的babel-loader
及其相关依赖(presets
、plugins
)全部移除,并配置esbuild-loader
如下:
// build\webpack.common.js
{
loader: 'esbuild-loader',
// ...
options: {
target: 'es2015', // esbuild 的最低编译目标为 es2015
}
}
在转译之外,使用esbuild
对 js、css 进行压缩,可以获得比terser
、cssnano
等传统压缩库更快的速度。
// build\webpack.prod.js
{
// ...
optimization: {
// ...
minimizer: [
new ESBuildMinifyPlugin({
target: 'es2015',
css: true, // 压缩 css
}),
]
}
}
注意:
由于
element-ui
对babel
的硬性依赖,esbuild
无法正确处理其jsx
语法的转译,所以项目中涉及到element-ui
的模块(包括components/modify-component/
等目录下,修改过的el-xxx
组件),仍需使用babel
进行转译和兼容。
寻址优化
寻址优化是webpack
的传统优化手段之一,即通过合理设置 loader 的exclude
和include
属性,只对必要的模块执行解析,减少构建的任务量。
开发模式下,由于开发者自己使用较新的浏览器版本,不需要对es6+
的语法进行兼容,仅转译浏览器原生不支持的jsx
语法即可。配置babel-loader
,只处理包含jsx
语法的模块,其他原生 JS 文件和.vue
文件生成的 JS 模块则不经过 loader。
// build\webpack.dev.js
{
test: /.m?jsx?$/,
loader: 'babel-loader',
// 开发模式下, 只需转译使用了 JSX 的模块
include: [
// 所有使用了 JSX 语法的模块
// module1
// module2
// ...
],
options: {
presets: [
'@vue/babel-preset-jsx'
]
}
}
生产模式下,为了兼容性,应针对所有使用了浏览器原生不支持的语法(如jsx
)和高于es2015
版本语法的 JS 模块进行转译。
// build\webpack.prod.js
const configureBabelLoader = () => ({
test: /.m?jsx?$/,
loader: 'babel-loader',
// babel 用于处理 element-ui 相关组件
include: [
// 所有使用了 JSX 语法的模块
// module1
// module2
// ...
],
options: {
presets: [
'@vue/babel-preset-jsx',
[
'@babel/preset-env',
{
targets: {
// Chorme 58 和 Edge 14 分别是其完全支持 es2015 的最早版本
chrome: '58',
edge: '14',
},
},
],
],
},
})
const configureESBuildLoader = () => ({
test: /\.m?js$/,
loader: 'esbuild-loader',
// esbuild 用于处理 element-ui 以外的所有 JS 模块
include: [
// 所有未使用 JSX 语法的 JS 模块
// module1
// module2
// ...
],
exclude: [
// 排除使用了 JSX 语法的模块
],
options: {
// 生产模式下, 需要兼容到 es2015 语法
target: 'es2015',
},
})
生产构建关闭 ESLint
在构建时,eslint
提供的代码静态检查会占据不少时间,而这些提示信息在生产构建阶段价值不大。因此在生产构建时,移除eslint
。
生产模式不生成 SourceMap
SourceMap 是结合浏览器 DevTool 将打包产物映射回源码的工具文件,在构建时生成和输出 SourceMap 会耗费大量时间,且产物占据大量内存,考虑到项目没有远程调试的需求,应当可以移除 SourceMap。
\build\webpack.prod.js
{
devtool: false // 关闭 SourceMap
// ...
}
另外,vite
在生产构建默认不生成 SourceMap,也可作为本项改动的理论支撑之一。
webpack 4
版本特定的一些优化
Reference
垃圾回收 (GC)
Reference
webpack 4
使用全局的string.replace(/.../g, '...')
来生成控制台信息,这会导致大量的垃圾回收。
虽然 V8 的垃圾回收进程是并行的,但其采用管线化的 IPC 方式,所以 GC 仍会带来较大的进程间通信开销。
开发模式下,关闭控制台输出模块路径信息的功能:
// build\webpack.dev.js
{
// ...
output: {
pathinfo: false // Tell webpack not to include comments in bundles with information about the contained modules.
// ...
}
}
而webpack
在生产模式下默认不展示pathinfo
,不需要另外配置。
关闭开发模式下的一些默认优化
将optimization
及其splitChunks
、removeEmptyChunks
设置为false
可提升webpack 4
开发环境下的性能。
// build\webpack.dev.js
{
optimization: {
splitChunks: {
cacheGroups: {
default: false
}
},
removeEmptyChunks: false
}
}
文件级缓存
使用hard-source-webpack-plugin
,为模块提供中间缓存(于磁盘中),应用后会使第一次构建速度变慢(因为要生成磁盘文件和hash
),而后续构建速度会大大提升。
对于迁移后的 v2,首次构建会生成大约 250 MB 的缓存文件,但后续构建速度提升非常夸张。
注意:
hard-source-webpack-plugin
与speed-measure-webpack-plugin
不兼容,故目前为了测量构建速度,未开启hard-source-webpack-plugin
。
迁移到 Vue 2.7
更新依赖:
vue
:^2.5.3
==>^2.7.14
vue-template-compiler
:移除。vue 2.7
已不需要该依赖。
迁移后,经测试可使用 Vue 2.x 版本的 Composition API 和<script setup>
语法。
由于项目对element-ui
的一些组件进行了重写,而element-ui
的源码后续更新时变动较大,所以不能直接将element-ui
也升级到最新版本。
注意:
因与迁移
webpack
的工程化意义不同,迁移vue
属于更偏应用层的更新,可在团队对业务代码层有额外的明确需求时再进行,如:
- 为了更好的代码组织、逻辑复用或向 Vue 3 过渡,从 Options API 迁移到 Composition API;
- 为提高项目的可维护性,加入 TypeScript;
- …
但如果实际迁移到 Vue 2.7,应该也能稍微提升构建效率,因为不再需要配置
webpack
为 Node.js 注入 polyfill。
Bug Fixing
js-base64
幽灵依赖问题
幽灵依赖(Phantom Dependencies)是指由于node_modules
扁平的文件结构设计,在package.json
中未声明的依赖却可以被单独显式引入的依赖。
在 v2 中,js-base64
在一些组件中被显式引入,但package.json
中未对其声明,这是postcss
依赖于js-base64
所导致。本次迁移中将js-base64^2.6.4
加入依赖。
Benchmarking
Methodology
Reference:
开发模式,冷启动:
- 运行
npm run dev
,记录webpack-cli
在控制台打印的信息即可。
HMR:
- 服务端(DevServer)- 记录控制台
webpack-cli
打印的构建时间即可。- Root - 在
App.vue
添加HMR-n
(n from 1 to 5),记录每次 recompile 用时; - Leaf - 在
xxx.vue
修改姓名
为姓名n
(n from 1 to 5),记录每次 recompile 用时。
- Root - 在
- 客户端(Browser)- 另开一个 node 进程,通过
fs.watch()
监听文件修改并打印时间戳,求与客户端渲染时间戳的差值。- Root - 在
App.vue
添加n-{{Date.now()}}
(n from 1 to 5),记录控制台与客户端显示时间戳; - Leaf - 在
xxx.vue
添加n-{{Date.now()}}
(n from 1 to 5),记录控制台与客户端显示时间戳。
- Root - 在
监听文件修改的脚本:
// watch.js // `node watch.js`运行即可 const fs = require('fs') fs.watch('./src/App.vue', (e, filename) => { console.log(Date.now(), filename) }) fs.watch('./src/views/taskmanager/task/task.vue', (e, filename) => { console.log(Date.now(), filename) })
生产模式,打包:
- 运行
npm run build
,记录webpack-cli
在控制台打印的信息即可。
Comparison
Dev
- 迁移前(
main
分支,#38fc8
版本):
Times | 1 | 2 | 3 | 4 | 5 | Average |
---|---|---|---|---|---|---|
Cold Start (ms) | 98272 | 93993 | 89986 | 94089 | 84342 | 92136 |
HMR Server Root (ms) | 2714 | 2729 | 2536 | 2567 | 2324 | 2574 |
HMR Server Leaf (ms) | 2961 | 2836 | 2928 | 2628 | 2602 | 2791 |
HMR Client Root (ms) | 3927 | 3657 | 3691 | 3609 | 3512 | 3679 |
HMR Client Leaf (ms) | 4015 | 3962 | 3480 | 4067 | 3881 |
- 迁移后(
migrate2webpack@4
分支,#26b08
版本):
Times | 1 | 2 | 3 | 4 | 5 | Average |
---|---|---|---|---|---|---|
Cold Start (ms) | 44887 | 45776 | 42537 | 42784 | 47785 | 44754 (51% faster) |
HMR Server Root (ms) | 840 | 759 | 616 | 646 | 611 | 694 (73% faster) |
HMR Server Leaf (ms) | 1019 | 766 | 788 | 745 | 685 | 800 (71% faster) |
HMR Client Root (ms) | 1382 | 1156 | 1188 | 1096 | 1070 | 1178 (68% faster) |
HMR Client Leaf (ms) | 1588 | 1747 | 1293 | 1236 | 1276 | 1428 (65% faster) |
Prod
- 迁移前(
main
分支,#e1b47c6
版本):
Times | 1 | 2 | 3 | 4 | 5 | 6 | Average |
---|---|---|---|---|---|---|---|
Build Time (s) | 125.45 | 173.37 | 171.40 | 181.32 | 140.26 | 153.77 | 157.60 |
- 迁移后(
migrate2webpack@4
分支,#8333ce4
版本):
Times | 1 | 2 | 3 | 4 | 5 | 6 | Average |
---|---|---|---|---|---|---|---|
Build Time (s) | 77.99 | 71.34 | 98.76 | 64.17 | 73.88 | 88.27 | 79.07 (49.83% faster) |
Miscellaneous
构建产物优化
分离webpack
运行时
Reference:
webpack
运行时(runtime)包含了项目初始需要加载哪些文件的清单(manifest),当需要加载的文件名(hash
)改变时,运行时会使旧版的文件失效。
结合为产物命名添加的hash
,将运行时单独分离出来可促使对客户端缓存的有效利用。
// build\webpack.prod.js
{
// ...
optimization: {
// ...
runtimeChunk: {
name: 'runtime'
}
}
}
这个策略与 v2 原本分离的manifest
块作用基本一致。
Bundle Splitting
Vendor separating
在原本的 v2 项目中,将所有来自node_modules
的依赖都打包到一个vendor
块中,这会生成一个体积庞大的 JS 文件,且这个 JS 文件将在项目启动时就会被加载,导致首屏性能下降,但可提升后续访问性能。
由于该配置有利有弊,故迁移后依然保留了这个配置:
// build\webpack.prod.js
{
// ...
optimization: {
splitChunks: {
cacheGroups: {
commons: {
name: 'vendor',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 0
},
// ... other chunks
}
}
}
}
实际上,webpack 4
在生产模式下已经默认启用了一种分块策略,这种策略已经可以满足大部分情况下的构建需要。
webpack 4
生产模式下的默认分块策略
- 可被共享的代码块或来自
node_modules
的模块;- 压缩前,体积超过 20kb 的代码块;
- 按需的最大并行请求数低于等于 30;
- 首屏加载的最大并行请求数低于等于 30。
可由团队协商后,决定是继续沿用该策略还是使用webpack 4
自带的分块策略。
CSS extraction
v2 原本通过extract-text-webpack-plugin
将项目的所有 CSS 代码提取到单独一个文件中,与上面的vendor
一样,其导出的 CSS 文件体积庞大,将阻塞应用的首屏渲染。
迁移后,由于extract-text-webpack-plugin
在webpack 4
中被废弃,改用mini-css-extract-plugin
在生产环境下进行 CSS 处理,而mini-css-extract-plugin
的设计理念就在于为每个包含 CSS 代码的 JS 文件生成单独的 CSS 文件,以提供异步加载、提高客户端缓存利用等特性。
vite
也针对 CSS 默认开启了分块策略,而不是将整个项目的 CSS 提取到单个文件中。
与上面的 Vendor Separating 相同,目前仍保留了这个配置,迁移后通过配置webpack 4
的SplitChunksPlugin
实现:
// build\webpack.prod.js
{
// ...
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.(css)|(styl(us)?)|(vue)$/,
chunks: 'all',
priority: 5
},
// ... other chunks
}
}
}
}
注意:
可能是由于
webpack 4
中SplitChunksPlugin
的固有特性,该配置在提取 CSS 之外,引入了将相关 JS 模块也合并在一起的副作用。在webpack 5
中,该副作用貌似可以通过用types: 'css/mini-extract'
代替test: /\.(css)|(styl(us)?)|(vue)$/
避免。
关于是继续沿用提取唯一一个 CSS 文件还是为每个模块生成单独的 CSS 文件,可由团队在商议并进行测试后决议。
关于 ESLint 的配置
在 v1、v2 与 h5 里,只有 v2 是在webpack
构建时启用了 eslint
的。
h5 的
eslint
提示是 IDE 读取.eslintrc.js
并配合 ESLint 扩展所提供的。
// v1 本身就没有安装 eslint 依赖
// h5, build\webpack.base.conf.js
module: {
rules: [
...(config.dev.useEslint ? [] : []), // h5 中,关于是否应用eslint的配置处始终返回空数组
]
}
而 v2 中,原有的eslint
配置如下:
// .eslintrc.js
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module',
allowImportExportEverywhere: true,
},
env: {
browser: true,
node: true,
es6: true,
},
globals: {
// ...
},
extends: 'eslint:recommended', // 注意此处 extends 配置
plugins: [
// 注意此处 plugins 配置
'html',
'vue',
],
rules: {
// custom rules ...
},
}
其中有两个值得注意的点:
plugins: ['html', 'vue']
- 将插件eslint-plugin-html
和eslint-plugin-vue
接入eslint
,但这些插件只是为eslint
提供了静态分析.html
和.vue
文件的能力,并不会自动引入任何规则集。extends: 'eslint:recommended'
-extends
选项只为eslint
引入了eslint:recommended
这一规则集,并没有引入package.json
所声明的包括eslint-plugin-vue
在内的各插件内置的规则。
且在更新eslint
相关依赖后,新版的eslint:recommended
增加了许多error
等级的规则,导致迁移后构建时新增许多报错。因代码规范需要整个团队协商决定,故目前在migrate2webpack@4
分支上未开启eslint-webpack-plugin
,可由团队确定具体措施后,予以开启。
或者也可像 h5 一样,关闭构建时的eslint
,通过 IDE 扩展实时地对代码进行静态分析。
注意:
- 在迁移后,考虑到项目需要,
eslint
相关依赖只保留了eslint-plugin-vue
,为vue
组件提供的额外规则集,可由团队商议后决定是否应用。- 另外,在开发环境下开启
eslint
必然会增加构建工作量、降低构建速度,所以建议开启eslint-webpack-plugin
的缓存配置,这可加快后续的构建效率。
- 为
eslint-webpack-plugin
配置cache: true
,会在 linting 后生成一个.eslintcache
文件,已在.gitignore
中声明忽略该文件。
Alternatives
在优化构建效率的过程中,尝试了其他一些来自社区的方法,但由于效果不佳,没有应用到最终版本中,现将其列举如下,可后续再考虑是否应用。
缓存
使用cache-loader
对大开销 loader 处理结果进行缓存,构建时对比模块的内容hash
决定是重新构建还是复用过去结果。
在自己的机器上对vue-loader
、css-loader
、stylus-loader
等几个大开销 loader 应用后效果不佳,故未采用。
猜想可能是引入的cache-loader
增加了读取和写入文件的开销,使得效率提升不高,可能因机器而异。
多线程
使用happypack
或thread-loader
为大开销 loader 开启多线程,通过并发提升打包效率。
同样,在自己的机器上对不同的 loader 应用thread-loader
并应用不同的配置(线程数、线程最大休眠时间等)后,效果并不佳,故未采用。
thread-loader
开启后,线程和线程之间通信会带来额外的开销,猜想可能是处理.vue
模块时对<template>
、<script>
、<style>
块的处理需要各线程频繁通信,导致拖慢构建效率。
output.futureEmitAssets
自从 webpack^4.29,配置output.futureEmitAssets = true
可使webpack 4
应用webpack 5
的构建逻辑,根据官方文档,这会在输出资源后释放内存,从而提升构建效率。
output.futureEmitAssets
的作用
Compilation.assets
中的资源被替换为SizeOnlySource
;- 不允许生成资源后再读取资源;
- 这使得内存可以被垃圾回收。
在自己引入后,发现开发环境下的构建效率并未提高;而生产环境若使用 CI/CD 的模式进行发包(即每次都是全新的环境),则对内存的清理并不重要,故未采用。
注意:
官方文档提到,开启这个配置可能导致那些会假定之前输出的资源仍在内存中的插件出错,但这类插件在整个生态中的占比非常小,且该逻辑在
webpack 5
中已是默认开启的,所以如果决定启用该配置,并不需要在插件适配上有太多顾虑。
Node polyfills
原来配置的 boilerplate 中为vue
配置了一些 Node.js 模块的 polyfill,注入 polyfill 会导致降低构建速度且使构建产物体积增大,在将vue
升级到vue^2.7
后,便可以将其完全移除。
Drawbacks
- 本次优化是针对项目宏观层面的,虽然构建成功,但不能保障每个运行时不出错,而这可能需要对 v2 相关业务做全面的测试才能得知;
- 本次优化是仅针对 v2 的,并将
node.js
的版本升至^14.20.0
,与 v1 差距较大,若发布生产时 v1、v2 使用统一的node
环境,则 v1 的构建可能出错。
Adoption strategy
本次迁移在migrate2webpack@4
分支上开发,在团队进行评估,并通过开发、生产环境的测试后,可选取一个版本合并该分支。
关于测试
- 由于该迁移是对整个 v2 前端基础设施的升级,与具体业务无关,所以可能需要特殊的测试策略。
合并流程
以下提供一种合并设想,假设目标版本分支为target-branch
:
git checkout migrate2webpack@4
,切换到migrate2webpack@4
分支;git rebase target-branch
,将迁移相关提交提升至历史栈顶;git reset --soft <hash-of-the-last-commit-on-target-branch>
,将迁移相关提交还原至暂存区;git commit -m 'migration: migrate webpack 3 to webpack 4'
,合并所有迁移相关修改到一次提交中,避免污染提交历史;- 原本的提交中一些地方包含了修改相关的注释,若有必要,可以
migrate2webpack@4
为原型新建一个分支,备份原来的提交信息;
- 原本的提交中一些地方包含了修改相关的注释,若有必要,可以
git checkout target-branch
,切换到target-branch
;git merge migrate2webpack@4
,将迁移代码合并到该版本,后续合并到开发分支上。
注意:
migrate2webpack@4
分支最后一次rebase
的目标是main #5238cfb1
。