webpack5升级小结

这阵子把组内最大的前端项目做了下 webpack 的升级,构建效率直线上升,主要得益于 webpack5 的缓存策略

webpack5 带来了什么?

  • 持久化缓存。webpack4 需要通过 cache-loader/hardSourcePlugin 来实现中间缓存,webpack5 相当于内置了这部分功能
  • 更好的 hash 算法。hash—>fullhash,比如 webpack 4 如果添加空白、注释或修改变量名是会影响 contenthash 值的计算,webpack5 则不会影响,从而能继续使用缓存,这个方式降低了缓存的失效率,间接加快了应用 rebuild 的速度
  • Asset Modules。指的是图片和字体等这一类型文件模块,它们无须使用额外的预处理插件
  • 模块联邦。实现应用级的模块复用,这个是现在蛮多新兴微前端框架的基础
  • tree-shaking 改进。据说可以减少约 30%的 bundle-size,不过实际项目中并没有体会到这个变化
  • 更严格的代码检查。这个也是造成很多 webpack4 项目在升级后突然在运行时报错的原因,会导致页面 crash
  • 确定的 moduleId/chunkId
  • Node Polyfill 脚本被移除
  • 原生 worker 支持。可以参考 react 项目中使用 web worker 里面有提及 webpack5 下如何使用 web worker

开始升级

这个过程也是遇到了一些问题,总结如下:

因为我们的项目是基于 CRA4 搭建的,并且基于 react-app-rewired 扩展 webpack 配置,所以首先需要升级 react-scripts、react-app-rewired、customize-cra

额外引入的 loader、plugin 都要升级到支持 webpack5 的版本。没有支持 webpack5,那就去 github issue 里面看看啥时候支持?

首先是 addLessLoader 不适配,需要更换插件如下:

// config-overrides.js
const addLessLoader = require("customize-cra-less-loader");
//...
addLessLoader({
  lessOptions: {
    javascriptEnabled: true,
    sourceMap: false,
  },
}),

可以借助 npm-check-updates 这个插件检查所有依赖版本

去除一些废弃的插件,比如以下的资源插件

  • url-loader 将文件作为 dataURI 内联到 bundle 中
  • file-loader 将文件发送到输出目录
  • raw-loader 将文件导入为字符串

webpack5 通过以下配置就可以完成对资源文件的解析

  • asset/resource 发送一个单独的文件并导出 URL(file-loader)
  • asset/inline 导出一个资源的 dataURI(url-loader)
  • asset 在导出一个 dataURI 和发送一个单独的文件之间自动选择。webpack4 需要通过使用 url-loader 并且配置资源体积限制来实现
  • asset/source 导出资源的源代码

配置方式参考:

// ...
rules: [
  {
    test: /\.png$/i,
    use: 'file-loader'
  },
  {
    test: /\.(jpg|gif)$/i,
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 1024,
        },
      },
    ],
  }
],
// 改成
// ...
rules: [
  {
    test: /\.png$/i,
    use: 'asset/resource'
  },
  {
    test: /\.(jpg|gif)$/i,
    type: 'asset',
    parser: {
      dataurlCondition: {
        maxSize: 1024 // 单位是B
      }
    }
  }
],

这里可以参考下我给项目做的资源配置

// config-overrides.js
// ...
  addWebpackModuleRule({
      test: /\.(png|jpe?g|gif|webp|svg|bmp|ttf|eot|woff|woff2)$/i,
      type: "asset",
      parser: {
        dataUrlCondition: {
          maxSize: 1024 * 1024,
        },
      },
      generator: {
        filename: "img/[name].[hash:4][ext]",
        publicPath: process.env.PUBLIC_URL,
      },
    }),
    addWebpackModuleRule({
      test: /\.(objs?|mtl)$/i,
      type: "asset/resource",
      generator: {
        filename: "model/[name].[hash:4][ext]",
        publicPath: process.env.PUBLIC_URL,
      },
    }),
// ...

去除 hard-source-webpack-plugin,取而代之的是 cache

cache: {
    // memory(内存)|filesystem(持久化缓存)
    type: "filesystem",
    buildDependencies: {
        config: [__filename],
    },
    version: '1.0'
}

缓存文件会保存在 node_modules/.cache。参考 https://github.com/webpack/webpack/issues/6527

如果配置 filesystem 做持久化储存,webpack5 还是会同时使用 memory,用于 watch 模式

如果是 eject 导出完整 webpack 配置的项目,可能还会遇到以下问题:

部分插件的引入方式需要调整,比如 webpack-merge 改成解构的方式引入

const webpackMerge = require("webpack-merge");
const ManifestPlugin = require("webpack-manifest-plugin");
// 改成
const { merge: WebpackMerge } = require("webpack-merge");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");

如果有单独引用 dev-sever 也要升级,启动命令调整如下:

// package.json
{
  "scripts": {
    "serve": "webpack-dev-server --config webpack.config.dev.js"
  }
}
// 改成
{
  "scripts": {
    "serve": "webpack server --config webpack.config.dev.js"
  }
}

通过 require 引入的图片会无法正常加载,需要在 url-loader 配置中添加 esModule: false

sourcemap 的名称可能也需要调整

devtool: "cheap-eval-module-source-map";
// 改成
devtool: "eval-cheap-module-source-map";

开启 hot: true 热更新无效,需要添加配置如下:

{
  target: process.env.NODE_ENV === "development" ? "web" : "browserslist",
}

这像是一个 bug,参考 https://github.com/webpack/webpack-dev-server/issues/2758

除去编译问题后,接下来主要在于解决一些运行时的报错,基本上按照提示一个个去解就行了

JSON 模块只能使用默认引入,调整如下

import { name } from "package.json";
// 改为
import pkgInfo from "package.json";
const { name } = pkgInfo;

其他的问题比如重复的函数或变量、undefined 等,在 webpack5 更严格的检查下会暴露,逐个修复就行了

进一步优化

CRA5 在 webpeck 的配置上已经做了很充分的优化工作,参考源码 https://github.com/facebook/create-react-app/blob/main/packages/react-scripts/config/webpack.config.js

进一步优化的空间其实不多,可以做下 code spilting,配置参考:

// config-overrides.js
module.exports = {
  webpack: override(
    // ...
    isProduction &&
      setWebpackOptimizationSplitChunks({
        chunks: "all",
        cacheGroups: {
          react: {
            test: /[\\/]node_modules[\\/](react|react-dom)/,
            name: "react",
            priority: 1,
          },
          three: {
            test: /[\\/]node_modules[\\/](three)/,
            name: "three",
            priority: 1,
          },
          antd: {
            test: /[\\/]node_modules[\\/](antd|@ant-design)/,
            name: "antd",
            priority: 1,
          },
          echarts: {
            test: /[\\/]node_modules[\\/](echarts|zrender)/,
            name: "echarts",
            priority: -1,
          },
          antv: {
            test: /[\\/]node_modules[\\/](@antv)/,
            name: "antv",
            priority: -1,
          },
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: "vendor",
            priority: -5,
          },
        },
      })
  ),
};
// ...

如果是多页或者多路由页面的应用,还可以结合 React.lazy 进行页面级别的分包,按需加载页面及其资源

总结

实际升级下来,其实问题不多,花个一天左右就填完坑了,可能是 react-scripts@5.0 已经做了大部分工作了,然后疑难问题基本上都可以找到解决方法。升级效果也很明显,主要体现在 rebuild 的速度上。我觉得还没升级的话,可以大胆升级一波,升级完之后就可以尝试 webpack5 的各种新特性啦 ~