webpack4 升级 webpack5 过程踩坑

3,440 阅读6分钟

版权声明:本文为博主原创文章,未经博主允许不得转载。 文章底部留言可联系作者。

一、背景

由于项目越来越庞大复杂,打包时间也非常长,本地开发环境每次重启都要打包好久也和你头疼,正好借此契机对webpack做了一个升级。

升级前使用webpack4,打包耗时如下图:需要 30467ms

image.png

升级webpack5之后,打包耗时如下图: 需要 5730ms

image.png

二、升级过程

可以查看官方文档 从v4升级到v5

1. 先升级 webpack 和 webpack-cli

npm install --save-dev webpack@latest webpack-cli@latest  webpack-dev-server@latest webpack-merge@latest

我之前版本这里是

"webpack": "^4.41.0",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.7.1",
"webpack-merge": "^4.2.1"

升级到的版本是

"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-merge": "^5.8.0"

webpack-merge升级以后,使用方式改为如下:

修改前:

const webpackMerge = require("webpack-merge");

修改后:

const { merge } = require('webpack-merge');

2. 执行npm start看看效果。

package.jsonscriptsstart 命令如下:

"scripts": {
    "start": "cross-env NODE_ENV=dev webpack-dev-server --hot --progress --colors  --config ./webpack.dev.js",
}

1)--colors 报错

v4版本中,我们可以使用 --colors或者 --color,但是在v5版本中只能使用 --color

image.png

调整命令:

"scripts": {
    "start": "cross-env NODE_ENV=dev webpack-dev-server --hot --progress --color  --config ./webpack.dev.js",
}

2)OpenBrowserPlugin 报错

image.png

打开浏览器的插件 open-browser-webpack-plugin目前在 webpack5 中不能使用了,所以去掉。

  • webpack5 在开发环境可以通过 devServer.open 的方式去打开浏览器,但是不太建议,因为会导致构建速度明显变慢。

    • 我这边针对加这个配置和不加分别进行三次构建,最后一次 配置open(需要60s左右启动),不配置(需要7s左右就可以启动),相差近10倍。所以建议不加。
  • 可以利用 react-dev-utils 当中的 openBrowser 来实现,这个不会太影响构建速度(测试第三次构建时大概6-7s),相当于自己写一个plugin。如下:

安装 react-dev-utils

npm install --save-dev react-dev-utils@latest

我安装的版本

"react-dev-utils": "^12.0.1"

在plugins中加一个对象,参考 Plugins 中的 compiler钩子

// 引入
const openBrowser = require('react-dev-utils/openBrowser')

.... 
// 使用
plugins:[
    {
        apply(compiler){
            let run = false
            // 在 compilation 完成时执行
            compiler.hooks.done.tap('open-browser', () => {
                if(!run){
                    openBrowser('your url')
                    run = true
                }
            })
        }
    }
]

3)devServer 中 disableHostCheck报错

image.png

这里需要参考下 webpack-dev-server v3 to v4 guide

devServer: {
    ... 
    disableHostCheck: true, 
    ... 
},

修改为:

devServer: {
    ... 
    allowedHosts: "all", 
    ... 
},

当设置为 'all' 时会跳过 host 检查。并不推荐这样做,因为不检查 host 的应用程序容易受到 DNS 重绑定攻击。

4) devServer 涉及的改动总结:

  • The inline (iframe live mode) option was removed without replacement.

v3 中有,但在 v4 中移除

devServer: {
    ... 
    inline: true, // v4中直接移除
    ... 
},
  • progress/overlay/clientLogLevel option were moved to the client option

v3 中:

devServer: {
    clientLogLevel: "info",
    overlay: true,
    progress: true,
  },

v4 中:

devServer: {
    client: {
      logging: "info",
      // Can be used only for `errors`/`warnings`
      //
      // overlay: {
      //   errors: true,
      //   warnings: true,
      // }
      overlay: true,
      progress: true,
    },
  },
  • contentBase/contentBasePublicPath/serveIndex/watchContentBase/watchOptions/staticOptions options were moved to static option:

把 contentBase 选项放到 static 的选项中:

v3 中:

devServer: {
    contentBase: path.join(__dirname, "public"),
    contentBasePublicPath: "/serve-content-base-at-this-url",
    serveIndex: true,
    watchContentBase: true,
    watchOptions: {
      poll: true,
    },
  },

v4中:

devServer: {
    static: {
      directory: path.resolve(__dirname, "static"),
      staticOptions: {},
      // Don't be confused with `devMiddleware.publicPath`, it is `publicPath` for static directory
      // Can be:
      // publicPath: ['/static-public-path-one/', '/static-public-path-two/'],
      publicPath: "/static-public-path/",
      // Can be:
      // serveIndex: {} (options for the `serveIndex` option you can find https://github.com/expressjs/serve-index)
      serveIndex: true,
      // Can be:
      // watch: {} (options for the `watch` option you can find https://github.com/paulmillr/chokidar)
      watch: true,
    },
  },

3. 执行 npm run build 看看

package.jsonscriptsbuild 命令如下:

"scripts": {
    "build": "cross-env NODE_ENV=prod webpack --progress --config ./webpack.prod.js",
}

1) html-webpack-plugin 报错

image.png

在webpack文档找到 html-webpack-plugin介绍,打开 html-webpack-plugin github:

image.png

安装最新版本的 html-webpack-plugin

"html-webpack-plugin": "^5.5.0"

原本的配置不需要做修改

const path = require('path')
const rootPath = path.resolve(__dirname, "../")

const isPro = process.env.NODE_ENV == 'pro';
...
plugins: [
    new HtmlWebpackPlugin({
        title: '项目名称',
        inject: true,
        hash: false,
        favicon: path.resolve(path.resolve(rootPath, "./app"), "./logo.png"),
        minify: {
            removeComments: isPro,       // 移除 HTML 中的注释
            collapseWhitespace: isPro,   // 删除空白符与换行符
            minifyCSS: isPro             // 压缩内联 css
        },
        filename: 'index.html',
        template: path.resolve(path.resolve(rootPath, "./app"), "./index.html")
    })
]

2) optimization.moduleIds 警告

image.png

  • 下图是webpack 文档中的介绍,主要 Warning 部分提到的:

    optimization.moduleIds.png

  • 并且项目使用到的 NamedChunksPlugin要做如下调整:

    • NamedChunksPlugin → optimization.chunkIds: 'named'

    • 下图是 webpack 官网对 optimization.chunkIds 的说明

    optimization.chunkIds.png

所以修改前:

optimization: {
    moduleIds: "hashed"
    ...
}

修改后:

optimization: {
    moduleIds: "hashed",
    chunkIds: 'named',
}

3) 压缩css 使用 css-minimizer-webpack-plugin

之前使用的 optimize-css-assets-webpack-plugin 在github 首页也明确表示,Webpack5 之后优先使用 Webpack 官方出品的 css-minimizer-webpack-plugin。 也可以看webpack文档关于 CssMinimizerWebpackPlugin 的介绍

image.png

安装 css-minimizer-webpack-plugin

npm install css-minimizer-webpack-plugin --save-dev

webpack中使用:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
      },
    ],
  },
  optimization: {
    minimizer: [
      // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
  plugins: [new MiniCssExtractPlugin()],
};

4) @babel/runtime 相关报错

image.png

在github上搜到了一个提问# Compile error with Webpack 5 after upgrading but working good with Webpack 4.4.1 和我报错类似吧,建议是把 @babel/runtime 升级到 ^7.12.5,不过这个比较早了,所以我升级到了最新版本,build编译就通过了。

"@babel/runtime": "^7.19.0"

到此为止没有报错了,但是还没有结束奥,因为有些特性还没有修改,下面再介绍一下

4. 去除dll动态链接库

所谓动态链接,就是把一些经常会共享的代码制作成 DLL 档,当可执行文件调用到 DLL 档内的函数时,Windows 操作系统才会把 DLL 档加载存储器内,DLL 档本身的结构就是可执行档,当程序有需求时函数才进行链接。透过动态链接方式,存储器浪费的情形将可大幅降低。

具体可以查看这个文章:《辛辛苦苦学会的 webpack dll 配置,可能已经过时了》

vue-cli 和 create-react-app 都移除了 dll,具体原因:

在这个 issue 里尤雨溪解释了去除的原因:

dll option will be removed. Webpack 4 should provide good enough perf and the cost of maintaining DLL mode inside Vue CLI is no longer justified.

create-react-app 在这个 PR 中也做出了说明:

image.png

所以如果项目用了webpack4,再使用dll收益不大,所以我们项目里也做了移除

三、新特性

1. 资源模块(asset module)

资源模块(asset module)是一种模块类型,它允许使用资源文件(字体,图标等)而无需配置额外 loader。

在 webpack5 之前,我们一般都会用以下loader

webpack5 内置了静态资源构建能力,所以直接使用下面4中模块类型,来替换这些loader

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist')
  },
+ module: {
+   rules: [
+     {
+       test: /.(png|jpg|svg|gif)$/,
+       type: 'asset/resource'
+     }
+   ]
+ },
};

2. 内置 fileSystem Cache能力

  • cache.type:缓存类型,支持 'memory' | 'filesystem',需要设置 filesystem 才能开启持久缓存
module.exports = {
    ...,
    cache: {
        type: 'filesystem',
        // 可选配置
        buildDependencies: {
            config: [__filename],  // 当构建依赖的config文件(通过 require 依赖)内容发生变化时,缓存失效
        },
        name: '',  // 配置以name为隔离,创建不同的缓存文件,如生成PC或mobile不同的配置缓存
        ...,
    },
}

3.不再为 Node.js 模块 自动引用 Polyfills,Polyfill 交由开发者自由控制

移除了 Node.js Polyfills,会导致一些包变得不可用(会在控制台输出 'XXX' is not defined),如果前端包里使用了 process、path 这些依赖,需要手动添加 Polyfill 支持。

4. Tree Shaking 改进

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。

Webpack5 能够支持深层嵌套的 export 的 Tree Shaking.

image.png

5. 模块联邦(Module Federation)

具体可以查看这篇文章了解 精读《Webpack5 新特性 - 模块联邦》

简单来讲模块联邦可以让跨应用间真正做到模块共享。

点击这里看 webpack文档 # Module Federation

模块联邦的使用方式如下:

引入 ModuleFederationPlugin 模块,有如下几个重要参数:

  • name: 当前应用的名称,需要唯一性;
  • library: 其中这里的 name 为作为 umd 的 name;
  • exposes: 需要导出的模块,用于提供给外部其他项目进行使用;
  • remotes: 需要依赖的远程模块,用于引入外部其他模块;
  • filename: 入口文件名称,用于对外提供模块时候的入口文件名;
  • shared: 配置共享的组件,一般是对第三方库做共享使用;

我们以 app_one 项目是消费方(消费其他remote模块),app_two提供方(暴露模块供消费方使用) 为例:

// 引入模块
const { ModuleFederationPlugin } = require("webpack").container

// app_one 配置
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
        name: "app_one",
        remotes: {
            app_two:"app_two@http://localhost:3000/remoteEntry.js",
        },
        exposes: {
            AppContainer: "./src/App"
        },
        shared: ["react", "react-dom", "react-router-dom"]
    }),
  ],
};

设置了 remotes: { app_two: "app_two_remote" },在代码中就可以直接利用以下方式直接从对方应用调用模块:

import { Search } from "app_two/Search";

我们也可以结合React 组件懒加载使用

const Search = React.lazy(() => import('app_two/Search')); 

我们引入的 app_two 配置如下:

// app_two 配置
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
        name: "app_two",
        library: { type: "var", name: "app_two" },
        filename: "remoteEntry.js",
        exposes: {
            Search: "./src/Search" // Search 在 exposes 被导出
        },
        shared: ["react", "react-dom"]
    }),
  ],
};

6. 顶层await(Top Level Await)

在顶层使用 await,在 async 函数外部使用 await 字段。它就像巨大的 async 函数,原因是 import 它们的模块会等待它们开始执行它的代码,因此,这种省略 async 的方式只有在顶层才能使用。

通过以下配置开启:

module.exports = {
    ...,
    experiments: {
        topLevelAwait: true,
    },
}

参考文章