webpack4 升级 webpack5 过程踩坑

语言: CN / TW / HK

一、背景

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

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

image.png

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

image.png

二、升级过程

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

1. 先升级 webpack 和 webpack-cli

npm install --save-dev [email protected] [email protected] [email protected] [email protected] 我之前版本这里是

"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升级以后,使用方式改为如下:

修改前: js const webpackMerge = require("webpack-merge"); 修改后: js const { merge } = require('webpack-merge');

2. 执行npm start看看效果。

package.jsonscriptsstart 命令如下:

json "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

调整命令: json "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 [email protected] 我安装的版本 "react-dev-utils": "^12.0.1"

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

```js // 引入 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 中移除

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

v3 中:

json devServer: { clientLogLevel: "info", overlay: true, progress: true, }, v4 中:

json 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 中: json devServer: { contentBase: path.join(__dirname, "public"), contentBasePublicPath: "/serve-content-base-at-this-url", serveIndex: true, watchContentBase: true, watchOptions: { poll: true, }, }, v4中: json 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 命令如下:

json "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" 原本的配置不需要做修改

```js 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-pluginnpm install css-minimizer-webpack-plugin --save-dev webpack中使用:

```js 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 [email protected] 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提供方(暴露模块供消费方使用) 为例: ```js // 引入模块 const { ModuleFederationPlugin } = require("webpack").container

// app_one 配置 module.exports = { plugins: [ new ModuleFederationPlugin({ name: "app_one", remotes: { app_two:"[email protected]://localhost:3000/remoteEntry.js", }, exposes: { AppContainer: "./src/App" }, shared: ["react", "react-dom", "react-router-dom"] }), ], }; `` 设置了remotes: { app_two: "app_two_remote" }`,在代码中就可以直接利用以下方式直接从对方应用调用模块:

js import { Search } from "app_two/Search";

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

js const Search = React.lazy(() => import('app_two/Search')); 我们引入的 app_two 配置如下:

js // 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, }, }

参考文章