这是一篇历时一年多的 webpack5 升级式实践总结。最早在 2020年4月份 webpack5 处于 beta
阶段时即开始相关的项目升级尝试,后续过程中又间断的进行了几次尝试,均因自编插件和第三方插件的兼容性等原因一直停留在不同的版本分支中。
2020-04
:webpack5@5.0.0-beta.15+2020-07
:webpack5@5.0.0-beta.21+2020-10
:webpack5@5.0.0 Release2021-01~04
:WEB 新项目,webpack5、React17…2021-07
:webpack@5.4x
以下为最终在项目中完成升级后的相关实践记录。
项目背景(广发投易通):五年以上、5-15人持续高频开发的大型 Electron 项目,React 技术栈,当前项目规模 TypeScript 源码 40+ 万行。
1 webpack 及各依赖插件版本的升级
安装最新的 webpack 版本:
# 早期 yarn add webpack@next -D # webpack5 正式发布后 yarn add webpack -D
依赖插件升级:按需更新项目中使用的相关插件至其最新版本。此过程中可能存在各种兼容性问题,需根据实际情况进行分析与解决。
1.1 使用 npm-check-updates
升级依赖版本
问题:160+
第三方依赖,如何快速确认最新版本与是否需要升级?
可选方案:
npm outdated
和npm update
过于简单粗暴- 手动升级:在 Microsoft Visual Code 编辑器中,当鼠标停留在依赖行并保持不动时,它会去查询该行依赖的最新版本
- 辅助工具:
npm-check-updates
安装和使用 npm-check-updates
:
# 全局安装 npm i -g npm-check-updates ncu # 或者使用 npx npx npm-check-updates
npm-check-updates
命令使用示例:
# 交互式升级:每一个依赖的修改都需手工确认 ncu -u -i # 只更新补丁版本 -- 按指定的<target>级别更新。target 可选值:latest, newest, greatest, minor, patch ncu -u --target patch # 仅更新 devDependenties 下的依赖。dep 可选参数: prod, dev, peer, optional, bundle(多个可用逗号分隔) ncu -u --dep dev # 仅更新包名称包含 react 字符串的依赖 ncu -u --filter /react/ # 不更新包名称包含 electron 字符串的依赖 ncu -u --reject /electron/ # 检查更新是否可以通过测试(npm test),并指定项目管理器为 yarn ncu -u --doctor -p yarn
项目升级实践:
# devDependencies 依赖:全部快速升级至最新(types 类型定义等少数无需升级的手动修正还原) ncu -u -p yarn --dep dev # dependencies 依赖:交互式逐个确认 ncu -u -i -p yarn --reject /react/ --dep prod
1.2 使用 TypeScript 编写 webpack 配置
建议使用 TypeScript 编写 webpack 配置文件,借助 TypeScript 的类型检测可以快速确定哪些属性配置是有效的、不再有效或错误的。示例:
import webpack from 'webpack'; const config: webpack.Configuration = { name: 'lzwme', mode: 'development', }; webpack.watch(config, (err, stats) => {});
启动 webpack:
ts-node -T webpack.config.ts #或者 node --max_old_space_size=8192 -r ts-node/register/transpile-only webpack.config.ts
当然,对于 js 格式的配置文件,也可以在配置文件的顶部添加 @ts-check
开启 js 文件的 TS 类型检查能力。示例:
// @ts-check /** @type {import('webpack').Configuration} */ const config = {};
2 webpack 构建配置更新与兼容性
- 编码质量与效率
- ESLint 升级
husky
升级:大型项目中,git hook 性能大幅度提升- 公共模块、ESLint / TS Check 相关
combokeys
->Shortcut
编码 TS 重构- More…
webpack
配置及相关依赖插件- 自定义插件的更新与开发:
BuilderManager
、Benchmark
、PluginsBuilder
、caseSensitiveSassLoader
等 html-webpack-plugin
: API 变更,依赖其 API 的自定义逻辑调试分析、兼容更新css-loader
与 cssModule 的变更CopyWebpackPlugin
使用方式变更、参数配置变更webpack.IgnorePlugin
参数配置变更- DLL 相关的配置
- More…
- 自定义插件的更新与开发:
- 移除不再使用的
webpack
插件script-ext-html-webpack-plugin
: 在html-webpack-plugin
插件最新版本支持相关功能需求thread-loader
、cache-loader
:改为使用 webpack5 内置的缓存和多线程配置等file-loader
、url-loader
- More…
- 业务依赖插件的兼容性
- 影响巨大,可能导致无法预知的意外:尽量少的修改,由业务调用处视应用情况逐个确认
兼容性细节问题最为复杂,问题分析相当耗时,是难以顺利升级的主要原因。
对第三方库的重度依赖,会加深项目升级复杂度。
2.1 webpack 插件的兼容性
一般来说三方插件的兼容性是升级 webpack5
最主要的问题。早期各插件兼容性跟进程度不一,表现怪异的细节问题相当多。
webpack API 变更,需开发插件兼容版本。
因为部分 API 的变更或废弃,许多插件都需要部分重写以进行兼容。自编插件的兼容。
为了一些特定需求自编的 webpack 插件基本上全部需要进行接口级兼容,开发调试工作量显著增大。三方插件新旧版本差异大。
一些插件在新版本的默认参数行为可能会发生较大的变化,这也会导致各种奇怪的问题出现,调试起来比较复杂,需仔细逐步分析。例如css-loader
对cssModule
的默认处理方式变更问题。
2.2 css-loader 与 cssModule
需配置 options.modules
参数以保持与 css-loader@2
之前的版本保持一致的逻辑:
{ loader: require.resolve('css-loader'), options: { importLoaders: 1, modules: { mode: 'global', }, // localIdentName: '[name]__[local]__[hash:base64:5]', }, },
esModules
默认规则的变更(标准 ESM):
// 旧的用法失效 import * as styles from './style.scss'; // 批量替换为 import styles from './style.scss';
2.3 output
err 报错问题
output: { path: pluginDesc, chunkFilename: '[id].chunk.js', filename: '[name].js', // @see https://github.com/webpack/webpack/issues/11660 // chunkLoading: false, wasmLoading: false, }
2.4 废弃的 file-loader
与 url-loader
DEPRECATED for v5: please consider migrating to asset modules.
raw-loader
: to import a file as a stringfile-loader
: 处理import/require
等方式加载的文件依赖url-loader
: 依赖file-loader
,包含其所有能力,并且可以将文件转换为base64
方式
webpack5 中的资源处理 asset modules:
asset/source
: exports the source code of the asset. Previously achievable by usingraw-loader
.asset/resource
: emits a separate file and exports the URL. Previously achievable by usingfile-loader
.asset/inline
: exports a data URI of the asset. Previously achievable by usingurl-loader
.asset
: automatically chooses between exporting a data URI and emitting a separate file. Previously achievable by usingurl-loader
with asset size limit.
2.4.1 在 webpack5 中使用 url-loader
假若旧项目对 url-loader
的依赖难以修改和移除,仍然有对应的配置可以实现:
module.exports = { module: { rules: [ { test: /\.(png|jpg|gif|svg)$/i, // 避免被 asset modules 处理 dependency: { not: ['url'] }, // 避免 webpack5 重复处理 type: 'javascript/auto', use: [ { loader: 'url-loader', options: { limit: 8192 }, }, ], }, ], } } Re
2.4.2 asset modules
配置与使用示例
如果条件允许,直接使用 asset modules
是面向未来的最好方式。
module.exports = { entry: './src/index.js', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), // 自定义输出路径 -- urlLoader.outputPath assetModuleFilename: 'images/[hash][ext][query]', }, module: { rules: [{ test: /\.(png|jpg|gif|svg)$/i, type: 'asset/resource', }, { test: /\.html/, type: 'asset/resource', // 配置独立的输出路径 generator: { filename: 'static/[hash][ext][query]', }, }, { // 文件内联 test: /\.svg/, type: 'asset/inline', }, { // 根据文件大小采用不同处理 -- url-loader test: /\.txt/, type: 'asset', parser: { dataUrlCondition: { maxSize: 4 * 1024 // 4kb } } }], }, };
2.5 关于 sass
: node-sass
与 dart-sass
sass
:css 扩展语言node-sass
: C++ 模块,具有系统平台和 Node.js 版本依赖性- 优点:性能高、编译速度快
- 缺点:由于平台依赖性高,安装失败的改立
dart-sass
:dart
编写,编译为wasm
模块- 优点:跨平台,安装等问题少,已成为官方主推的选择
sass-loader
已将默认的解释器修改为了dart-sass
可以直接切换为 dart-sass
了吗?
业务代码兼容性更新繁琐且需大量的回归测试。
3 构建性能效率:持久性缓存与多线程
webpack 构建过程中,最为消耗时间的是各种源文件的编译过程。提升构建效率的思路则主要是从多线程并行和避免重复编译(缓存编译结果)两个角度入手。
3.1 webpack4 及之前的构建性能优化
长期开发维护的大型项目的构建时长,往往需要几十至数百秒。
在 webpack4 以前,为了使用缓存来提升编译构建效率,happypack
、 hard-source-webpack-plugin
、thread-loader
、cache-loader
等插件几乎是项目构建的标配,但即使如此时常也不尽人意,毕竟这些第三方的插件无法做到完美的相互协作并融合到构建过程的各个角落。
- cache-loader: 缓存构建结果至
node_modules/.cache-loader
目录(可指定位置) - 开启
babel-loader
的配置项:cacheDirectory: true
,效果类似于cache-loader
- hard-source-webpack-plugin: 缓存构建结果至
node_modules/.cache/hard-source
目录(可指定位置) - thread-loader :使用多线程执行
loader
构建过程 - happypack :第三方实现的早期的多线程方案,官方出了
thread-loader
后基本不再更新了 webpack dll
: 预编译不会经常修改的资源- more…
3.2 webpack5 中的 cache
配置
在 webpack5
中,通过 cache
字段实现缓存配置,而且其效果表现更好。
{ // The number of parallel processed modules in the compilation. parallelism: require('os').cups().length -1, cache: isProd ? { type: 'memory' } : { type: 'filesystem', name: `test-${subModuleName}`, version: package.version, }, }
webpack5 缓存效果对比
webpack5 首次构建:
webpack5 二次构建(有缓存):
webpack4 缓存效果对比
webpack4 首次构建(使用了thread-loader
):
webpack4 二次构建(有缓存):
3.3 更进一步:thread-loader
与 esbuild-loader
在 webpack5
中,thread-loader
依然有其发挥之地。通过 parallelism
只能简单的指定并行构建的线程数,thread-loader
则可以针对如 ts-loader
等一类特殊的 loader 单独进行多线程构建测试与优化。
此外,使用 esbuild-loader
替换 babel-loader
来实现编译加速也是加速构建效率的可选方案,利弊则在于构建输出和开发体验上对其各自生态的依赖程度,其中热更新的差异性在开发的体验上的对比还是挺大的。
webpack5 首次构建(thread-loader):
3.4 下一代构建工具方案的效率
对于 vite
和 snowpack
这类 No Bundles
的下一代前端构建方案,其开发体验无疑是相当舒适的。由于无需构建、按需编译的特性,无论多么大型的项目几乎都可以在几秒内启动。
不过当前从简单的测试及社区反馈来看,在处理一些复杂的个性化需求时,会存在许多细节问题难以完美的处理。webpack 这类的编译方案还有很长的路可以走。
4 webpack5 的其他配置更新简析
4.1 optimization
optimization
是一个比较重要的配置,该配置属性在 webpack5 中有比较大的变更,一些以前需要使用内置插件来开启的优化功能,被改为在这里以配置项的方式进行开启或关闭。具体可参考这里:
- https://github.com/webpack/changelog-v5/blob/master/MIGRATION%20GUIDE.md#update-outdated-options
4.2 node 环境的兼容
在以前 webpack
的目标是构建可以支持浏览器端运行的应用,所以对一些 Node.js
环境中才有的变量存在默认的兼容性支持。在 webpack5
中则默认不再支持这些兼容处理。现在 node
属性只能配置一个 global
属性,其他的兼容都通过 resolve.fallback
自行配置实现。例如对 fs
、path
、buffer
等 nodejs
原生模块进行兼容:
{ node: { global: true, } resolve: { fallback: { crypto: false, fs: false, child_process: false, electron: false, // 不作处理:例如对运行环境有检测逻辑,只在 node/Electron 环境下才会实际执行调用 path: false, // 浏览器端兼容 // path: require.resolve('path-browserify'), buffer: require.resolve('buffer/'), } } }
值为 false
表示不作任何处理,这和 externals
中进行 dll
过滤的效果基本类似。
对 Buffer
的兼容,还需要 ProvidePlugin
插件:
new webpack.ProvidePlugin({ Buffer: ["buffer", "Buffer"] })
4.3 即将废弃 API 的警告信息
对于一些即将废弃的 API,webpack5 使用 util.deprecate 进行了包装。它使得这些 API 被调用时会在命令行打印相关警告信息。
在升级至 webpack5 过程中,你应该设置 process.traceDeprecation=true
或加上命令行参数 --trace-deprecation
来调试查看哪些地方废弃的 API 调用,并尝试更新或使用相关插件的最新版本。
如果你必须使用的一些插件最新版本也暂未对这种 API 调用进行处理,可以设置 process.noDeprecation=true
来关闭它:
// 关闭 util.deprecate 警告信息 (process as any).noDeprecation = true;
4.4 其他
webpack5
将一些常见配置进行了默认设置,如entry: 'src/index.js'
- 如果你用了
Yarn PnP
,并且配置了pnp-resolver-plugin
,那么你可以移除相关配置,它现在是内置的了。
可以先参考一下官方的 webpack4 -> webpack5
迁移指南:
- https://github.com/webpack/changelog-v5/blob/master/MIGRATION%20GUIDE.md
5 下一代构建工具?
前端构建提效三大策略与方向:
- 最大化利用缓存、多线程
- Native 语言开发工具链,提升执行效率
- 技术发展带来的新思路:ESM No bundles
当前流行的构建工具主要有:
- webpack webpack is a static module bundler for modern JavaScript applications
- rollup Rollup is a module bundler for JavaScript
- Parcel 极速零配置Web应用打包工具
- Snowpack The faster frontend build tool.
- vite 下一代前端开发与构建工具
Javascript / TypeScript 编译器:
- babel Babel is a JavaScript compiler
- tsc(typescript)
- esbuild 使用
Go
编写,主要用于构建加速 - swc Super fast javascript / typescript compiler. 使用
Rust
编写
Snowpack
和 vite
这类当前被称为下一代构建工具的核心思想,是基于 ES20200 规范的 ES Modules
原生支持,构建过程主要将源码编译为 ES Module
规范格式,并在浏览器中直接引用:
<script type="module" src="index.js" ></script>
主要特性/目标:
- No bundles,真正的按需编译、按需加载
- 极快的冷启动
- 极速的热更新
- 大幅度提升开发效率与体验
- More…
存在的主要问题:
- 兼容性、编译结果差异:旧项目的迁移存在风险
- 一些特性用法/语法不容易转换支持
- 插件支持丰富度不够
- 自编插件开发的成本
- More…
6 相关参考
- https://github.com/webpack/changelog-v5
- persistent-caching
- 语义化版本 2.0.0
- Webpack5.0 新特性尝鲜实战
- Webpack 5 中的新特性
- Webpack 5 升级实验
- 精读《Webpack5 新特性 – 模块联邦》
- 探索webpack5新特性Module-federation
- 从构建进程间缓存设计 谈 Webpack5 优化和工作原理
- 让你的 webpack sass 和 css 处理性能 10 倍提升
- webpack5踩坑指南
- https://lzw.me/pages/share/ppt/update-to-webpack5.html