投易通 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 outdatednpm 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 配置及相关依赖插件
    • 自定义插件的更新与开发:BuilderManagerPluginsBuildercaseSensitiveSassLoaderBenchmark
    • 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-loaderurl-loader:webpack5 asset modules(后文详解)
    • More...
  • 业务依赖插件/组件的兼容性
    • 需修改业务代码进行兼容
    • 影响巨大,可能导致无法预知的意外
    • 尽量少的修改,由业务调用处视应用情况逐个确认


对第三方库的重度依赖,会加深项目升级复杂度

2.1 webpack 插件的兼容性


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

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

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-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 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 关于 sassnode-sassdart-sass


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

  • webpack5 首次构建:

  • webpack5 首次构建(thread-loader):

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


对于 vitesnowpack 这类 No Bundles 的下一代前端构建方案,其开发体验无疑是相当舒适的。由于无需构建、按需编译的特性,无论多么大型的项目几乎都可以在几秒内启动。

不过当前从简单的测试及社区反馈来看,在处理一些复杂的个性化需求时,会存在许多细节问题难以完美的处理。webpack 这类的编译方案还有很长的路可以走。

下一代构建工具

当前流行的构建工具主要有:

  • 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:: Next Generation Frontend Tooling.

Javascript / TypeScript 编译器:

  • babel:: Babel is a JavaScript compiler
  • tsc:: TypeScript 自带的编译工具
  • esbuild:: 使用 Go 编写,用于加速编译过程
  • swc:: Super fast JavasSript / TypeScript compiler. 使用 Rust 编写

下一代构建工具


Snowpackvite 这类当前被称为下一代构建工具的核心思想,是基于 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 自行配置实现。例如对 fspathbuffernodejs 原生模块进行兼容:

{
  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 迁移指南: