投易通 webpack5 升级实践与总结
李志文
2021/08
目录概览
- 一、webpack 及各依赖插件版本的升级
- 二、webpack 构建配置更新与兼容性
- 三、构建性能和效率:持久性缓存与多线程
- 四、其他更新简析
- FAQ
背景
这是一篇历时一年多的 webpack5 升级实践总结:
- 2020-04:webpack5@5.0.0-beta.15+
- 2020-07:webpack5@5.0.0-beta.21+
- 2020-10:webpack5@5.0.0 Release
- 2021-01~04:投易通 WEB 新项目,webpack5、React17...
- 2021-07:webpack@5.4x
最早在 2020年4月份 webpack5 处于 beta
阶段时即开始相关的项目升级尝试,后续过程中又间断的进行了几次尝试,均因自编插件和第三方插件的兼容性等原因而一直停留在不同的版本分支中。
以下为最终在项目中完成升级后的相关实践记录。
1 webpack 及各依赖插件版本的升级
安装最新的 webpack 版本:
# 早期 yarn add webpack@next -D # webpack5 正式发布后 yarn add webpack -D
依赖插件升级:
按需更新项目中使用的相关插件至其最新版本。此过程中可能存在各种兼容性问题,需根据实际情况进行分析与解决。
问题:
160+
第三方依赖,如何快速确认最新版本与是否需要升级?
1.1 使用 npm-check-updates
升级依赖版本
可选方案:
npm outdated
和npm update
: 过于简单粗暴手动升级:在
Microsoft Visual Code
编辑器中,当鼠标停留在依赖行并保持不动时,它会去查询该行依赖的最新版本安装和使用
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 依赖全部快速升级至最新 ncu -u -p yarn --dep dev # dependencies 依赖交互式逐个确认 ncu -u -i -p yarn --reject /react/ --dep prod
1.2 使用 TypeScript 编写 webpack 配置
建议使用 TypeScript 编写 webpack 配置文件,借助 TypeScript 的类型检测可以快速确定哪些属性配置是有效、不再有效或错误的。示例:
webpack 配置:
import webpack from 'webpack';
const config: webpack.Configuration = {
name: 'lzwme',
mode: 'development',
...defaultConfig,
};
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
1.2 使用 TypeScript 编写 webpack 配置
对于 js 格式的配置文件,也可以在配置文件的顶部添加 @ts-check
开启 js 文件的 TS 类型检查能力。
// @ts-check
/** @type {import('webpack').Configuration} */
const config = {};
2 webpack 构建配置的更新与兼容性
- 编码质量与效率
husky
升级:大型项目 git hook 性能大幅度提升- ESLint 升级:新的规则变动、Lint 异常 -- 手动修正+白名单机制
- 公共模块、ESLint / TS Check 相关
combokeys
->Shortcut
编码 TS 重构- More...
webpack
配置及相关依赖插件- 自定义插件的更新与开发:
BuilderManager
、PluginsBuilder
、caseSensitiveSassLoader
、Benchmark
等 html-webpack-plugin
: API 变更,依赖其 API 的自定义逻辑调试分析、兼容更新(不同阶段不同版本多次测试,耗时最长)css-loader
:cssModule 默认规则的变更CopyWebpackPlugin
使用方式变更、参数配置变更webpack.IgnorePlugin
参数配置变更- DLL 相关的配置
- More...
- 自定义插件的更新与开发:
兼容性细节问题最多,难以顺利升级的主要原因
- 移除不再使用的
webpack
插件script-ext-html-webpack-plugin
: 在html-webpack-plugin
插件最新版本支持相关功能需求cache-loader
:改为使用 webpack5 内置的缓存和多线程配置等file-loader
、url-loader
:webpack5asset modules
(后文详解)- More...
- 业务依赖插件/组件的兼容性
- 需修改业务代码进行兼容
- 影响巨大,可能导致无法预知的意外
- 尽量少的修改,由业务调用处视应用情况逐个确认
对第三方库的重度依赖,会加深项目升级复杂度
2.1 webpack 插件的兼容性
一般来说三方插件的兼容性是升级 webpack5
最主要的问题。早期各插件兼容性跟进程度不一,表现怪异的细节问题相当多。
webpack API 变更,需开发插件兼容版本。
因为部分 API 的变更或废弃,许多插件都需要部分重写以进行兼容。自编插件的兼容。
为了一些特定需求自编的 webpack 插件基本上全部需要进行接口级兼容,开发调试工作量显著增大。第三方插件新旧版本差异大。
一些插件在新版本的默认参数行为可能会发生较大的变化,这也会导致各种奇怪的问题出现,调试起来比较复杂,需仔细逐步分析。例如css-loader
对cssModule
的默认处理方式变更问题。
2.2 css-loader 与 cssModule
配置
options.modules
参数以保持与css-loader@1
之前的版本保持一致的逻辑:{ 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 string
file-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 asset modules
配置与使用示例
如果条件允许,直接使用 asset modules
是面向未来的最好方式。
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]', // urlLoader.outputPath
},
module: {
rules: [{
test: /\.(png|jpg|gif|svg)$/i,
type: 'asset/resource',
}, {
test: /\.html/,
type: 'asset/resource',
generator: {
filename: 'static/[name][ext]',
},
}, { // 文件内联
test: /\.svg/,
type: 'asset/inline',
}, { // 根据文件大小采用不同处理 -- url-loader
test: /\.txt/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 4 * 1024 } // 4kb
}
}],
},
};
2.4.2 在 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: 4096 },
},
],
},
],
}
}
2.5 关于 sass
:node-sass
与 dart-sass
sass
:css 扩展语言node-sass
: C++ 编写的.node
模块,具有系统平台和 Node.js 版本依赖性- 优点:性能高、编译速度快
- 缺点:由于平台依赖性高,安装失败的改立
dart-sass
:dart
编写,编译为 WebAssembly(wasm) 模块- 优点:跨平台,安装等问题少,已成为官方主推的选择
sass-loader
:webpack 插件。已将默认的解释器修改为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)
webpack5 首次构建:
webpack5 首次构建(thread-loader):
3.4 下一代构建工具方案的效率
对于 vite
和 snowpack
这类 No Bundles
的下一代前端构建方案,其开发体验无疑是相当舒适的。由于无需构建、按需编译的特性,无论多么大型的项目几乎都可以在几秒内启动。
不过当前从简单的测试及社区反馈来看,在处理一些复杂的个性化需求时,会存在许多细节问题难以完美的处理。webpack 这类的编译方案还有很长的路可以走。
下一代构建工具
当前流行的构建工具主要有:
Javascript / TypeScript 编译器:
- babel:: Babel is a JavaScript compiler
- tsc:: TypeScript 自带的编译工具
- esbuild:: 使用
Go
编写,用于加速编译过程- esbuild-loader:: 在 webpack 中使用 esbuild
- swc:: Super fast JavasSript / TypeScript compiler. 使用
Rust
编写
下一代构建工具
Snowpack
和 vite
这类当前被称为下一代构建工具的核心思想,是基于 ES20200 规范的 ES Modules
原生支持,构建过程主要将源码编译为 ES Module
规范格式,并在浏览器中直接引用:
<script type="module" src="index.js" ></script>
下一代构建工具
主要特性/目标:
- No bundles,真正的按需编译、按需加载
- 极快的冷启动
- 极速的热更新
- 大幅度提升开发效率与体验
- More...
存在的主要问题:
- 兼容性、编译结果差异:旧项目的迁移存在风险
- 一些特性用法/语法不容易转换支持
- 插件支持丰富度低
- 自编插件开发的成本增高
- More...
4 webpack5 的其他更新简析
optimization
optimization
是一个比较重要的配置,该配置属性在 webpack5 中有比较大的变更,一些以前需要使用内置插件来开启的优化功能,被改为在这里以配置项的方式进行开启或关闭。具体可参考这里:
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,
path: false, // 不处理:如只在 node 环境下其逻辑才会被执行调用
// path: require.resolve('path-browserify'), // 浏览器端兼容
buffer: require.resolve('buffer/'),
}
}
}
- 值为
false
表示不作任何处理,这和externals
中进行dll
过滤的效果基本类似。 - 对
Buffer
的兼容,还需要ProvidePlugin
插件:
new webpack.ProvidePlugin({
Buffer: ["buffer", "Buffer"]
})
即将废弃 API 的警告信息
对于一些即将废弃的 API,webpack5 使用 util.deprecate 进行了包装。它使得这些 API 被调用时会在命令行打印相关警告信息。
在升级至 webpack5 过程中,你应该设置 process.traceDeprecation=true
或加上命令行参数 --trace-deprecation
开启跟踪调试开关,查看具体是哪些地方存在对废弃的 API 调用,并尝试更新或使用相关插件的最新版本。
如果你必须使用的一些插件最新版本也暂未对这种 API 调用进行处理,可以设置 process.noDeprecation=true
来关闭它:
// 关闭 util.deprecate 警告信息
(process as any).noDeprecation = true;
其他
webpack5
将一些常见配置进行了默认设置,如entry: 'src/index.js'
- 如果你用了
Yarn PnP
,并且配置了pnp-resolver-plugin
,那么你可以移除相关配置,它现在是内置的了。 - 建议参考一下官方的
webpack4 -> webpack5
迁移指南:
FAQ
* * *