webpack4 升級 webpack5 過程踩坑

語言: CN / TW / HK

一、背景

由於專案越來越龐大複雜,打包時間也非常長,本地開發環境每次重啟都要打包好久也和你頭疼,正好藉此契機對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升級以後,使用方式改為如下:

修改前: 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 react-dev-utils@latest 我安裝的版本 "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 http://github.com/expressjs/serve-index) serveIndex: true, // Can be: // watch: {} (options for the `watch` option you can find http://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 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提供方(暴露模組供消費方使用) 為例: ```js // 引入模組 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" }`,在程式碼中就可以直接利用以下方式直接從對方應用呼叫模組:

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

參考文章