大型项目升级至 webpack5 实践与总结

这是一篇历时一年多的 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 Release
  • 2021-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 outdatednpm 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 配置及相关依赖插件
    • 自定义插件的更新与开发:BuilderManagerBenchmarkPluginsBuildercaseSensitiveSassLoader
    • html-webpack-plugin: API 变更,依赖其 API 的自定义逻辑调试分析、兼容更新
    • css-loader 与 cssModule 的变更
    • CopyWebpackPlugin 使用方式变更、参数配置变更
    • webpack.IgnorePlugin 参数配置变更
    • DLL 相关的配置
    • More…
  • 移除不再使用的 webpack 插件
    • script-ext-html-webpack-plugin: 在 html-webpack-plugin 插件最新版本支持相关功能需求
    • thread-loadercache-loader:改为使用 webpack5 内置的缓存和多线程配置等
    • file-loaderurl-loader
    • More…
  • 业务依赖插件的兼容性
    • 影响巨大,可能导致无法预知的意外:尽量少的修改,由业务调用处视应用情况逐个确认

兼容性细节问题最为复杂,问题分析相当耗时,是难以顺利升级的主要原因。
对第三方库的重度依赖,会加深项目升级复杂度。

2.1 webpack 插件的兼容性

一般来说三方插件的兼容性是升级 webpack5 最主要的问题。早期各插件兼容性跟进程度不一,表现怪异的细节问题相当多。

  • webpack API 变更,需开发插件兼容版本。因为部分 API 的变更或废弃,许多插件都需要部分重写以进行兼容。
  • 自编插件的兼容。为了一些特定需求自编的 webpack 插件基本上全部需要进行接口级兼容,开发调试工作量显著增大。
  • 三方插件新旧版本差异大。一些插件在新版本的默认参数行为可能会发生较大的变化,这也会导致各种奇怪的问题出现,调试起来比较复杂,需仔细逐步分析。例如 css-loadercssModule 的默认处理方式变更问题。

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-loaderurl-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 using raw-loader.
  • asset/resource: emits a separate file and exports the URL. Previously achievable by using file-loader.
  • asset/inline: exports a data URI of the asset. Previously achievable by using url-loader.
  • asset: automatically chooses between exporting a data URI and emitting a separate file. Previously achievable by using url-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-sassdart-sass

  • sass:css 扩展语言
  • node-sass: C++ 模块,具有系统平台和 Node.js 版本依赖性
    • 优点:性能高、编译速度快
    • 缺点:由于平台依赖性高,安装失败的改立
  • dart-sassdart编写,编译为 wasm 模块
    • 优点:跨平台,安装等问题少,已成为官方主推的选择
  • sass-loader 已将默认的解释器修改为了 dart-sass

可以直接切换为 dart-sass 了吗?

dart-sass-err.png

业务代码兼容性更新繁琐且需大量的回归测试。

3 构建性能效率:持久性缓存与多线程

webpack 构建过程中,最为消耗时间的是各种源文件的编译过程。提升构建效率的思路则主要是从多线程并行避免重复编译(缓存编译结果)两个角度入手。

3.1 webpack4 及之前的构建性能优化

长期开发维护的大型项目的构建时长,往往需要几十至数百秒。
在 webpack4 以前,为了使用缓存来提升编译构建效率,happypackhard-source-webpack-pluginthread-loadercache-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-loaderesbuild-loader

webpack5 中,thread-loader 依然有其发挥之地。通过 parallelism 只能简单的指定并行构建的线程数,thread-loader 则可以针对如 ts-loader 等一类特殊的 loader 单独进行多线程构建测试与优化。

此外,使用 esbuild-loader 替换 babel-loader 来实现编译加速也是加速构建效率的可选方案,利弊则在于构建输出和开发体验上对其各自生态的依赖程度,其中热更新的差异性在开发的体验上的对比还是挺大的。

webpack5 首次构建(thread-loader):

3.4 下一代构建工具方案的效率

对于 vitesnowpack 这类 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 自行配置实现。例如对 fspathbuffernodejs 原生模块进行兼容:

{
  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 编写

Snowpackvite 这类当前被称为下一代构建工具的核心思想,是基于 ES20200 规范的 ES Modules 原生支持,构建过程主要将源码编译为 ES Module 规范格式,并在浏览器中直接引用:

<script type="module" src="index.js" ></script>

主要特性/目标:

  • No bundles,真正的按需编译、按需加载
  • 极快的冷启动
  • 极速的热更新
  • 大幅度提升开发效率与体验
  • More…

存在的主要问题:

  • 兼容性、编译结果差异:旧项目的迁移存在风险
  • 一些特性用法/语法不容易转换支持
  • 插件支持丰富度不够
  • 自编插件开发的成本
  • More…

6 相关参考

点赞 (1)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code