webpack4 升級 webpack5 過程踩坑
一、背景
由於專案越來越龐大複雜,打包時間也非常長,本地開發環境每次重啟都要打包好久也和你頭疼,正好藉此契機對webpack做了一個升級。
升級前使用webpack4,打包耗時如下圖:需要 30467ms
升級webpack5之後,打包耗時如下圖: 需要 5730ms
二、升級過程
可以檢視官方文件 從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.json
中 scripts
的 start
命令如下:
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
調整命令:
json
"scripts": {
"start": "cross-env NODE_ENV=dev webpack-dev-server --hot --progress --color --config ./webpack.dev.js",
}
2)OpenBrowserPlugin 報錯
開啟瀏覽器的外掛 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報錯
這裡需要參考下 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.json
中 scripts
的 build
命令如下:
json
"scripts": {
"build": "cross-env NODE_ENV=prod webpack --progress --config ./webpack.prod.js",
}
1) html-webpack-plugin 報錯
在webpack文件找到 html-webpack-plugin介紹,開啟 html-webpack-plugin github:
安裝最新版本的 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 警告
-
下圖是webpack 文件中的介紹,主要 Warning 部分提到的:
-
並且專案使用到的
NamedChunksPlugin
要做如下調整:NamedChunksPlugin
→optimization.chunkIds: 'named'
- 下圖是
webpack
官網對optimization.chunkIds
的說明
所以修改前:
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 的介紹
安裝 css-minimizer-webpack-plugin
:
npm 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 相關報錯
在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 中也做出了說明:
所以如果專案用了webpack4,再使用dll收益不大,所以我們專案裡也做了移除
三、新特性
1. 資源模組(asset module)
資源模組(asset module)是一種模組型別,它允許使用資原始檔(字型,圖示等)而無需配置額外 loader。
在 webpack5 之前,我們一般都會用以下loader
raw-loader
將檔案匯入為字串url-loader
將檔案作為 data URI 內聯到 bundle 中file-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.
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,
},
}