Webpack5構建速度提升令人驚嘆,早升級早受益
- 2021 年 5 月 13 日
- 筆記
為什麼要升級?
webpack4用的好好的,運行穩定,為什麼要升級到webpack5, 每次升級,都要經歷一場地震,處理許多loader和plugin API的破壞性改變。 請給我們一個充分的升級理由,不然真的沒有動力去折騰。沒問題,給你們一個充分的理由,webpack5對構建速度做了突破性的改進,開啟文件快取之後,再次構建,速度提升明顯。在我參與的項目中,本地伺服器開發環境,第一次構建速度是38.64s,第二次構建速度是1.69s,提升了一個數量級。My God, 是不是很驚喜,很意外。


生產打包構建速度,同樣有顯著提升,第一次打包耗時1.01m,第二次打包耗時10.95s. 看到這裡,你是不是有了升級的熱情,那請繼續往下看。


為什麼構建速度有了質的飛躍?
主要是因為:
1.webpack4是根據程式碼的結構生成chunkhash,添加了空白行或注釋,會引起chunkhash的變化,webpack5是根據內容生成chunkhash,改了注釋或者變數不會引起chunkhash的變化,瀏覽器可以繼續使用快取。
2.優化了對快取的使用效率。在webpack4 中,chunkId與moduleId都是自增id。只要我們新增一個模組,那麼程式碼中module的數量就會發生變化,從而導致moduleId發生變化,於是文件內容就發生了變化。chunkId也是如此,新增一個入口的時候,chunk數量的變化造成了chunkId的變化,導致了文件內容變化。所以對實際未改變的chunk文件不能有效利用。webpack5採用新的演算法來計算確定性的chunkId和moduleId。可以有效利用快取。在production模式下,optimization.chunkIds和optimization.moduleIds默認會設為』deterministic』。
3.新增了可以將快取寫入磁碟的配置項, 在命令行終止當前構建任務,再次啟動構建時,可以復用上一次寫入硬碟的快取,加快構建過程。
這兩項的默認配置為:
module.exports = (env) => { return { splitChunks: { chunks: 'async', // 指明要分割的插件類型, async:非同步插件(動態導入),inital:同步插件,all:全部類型 minSize: 20000, //文件最小大小,單位bite;即超過minSize有可能被分割; minRemainingSize: 0, // webpack5新屬性,防止0尺寸的chunk minChunks: 1, // 被提取的模組必須被引用1次 maxAsyncRequests: 30, // 非同步載入程式碼時同時進行的最大請求數不得超過30個 maxInitialRequests: 30, // 入口文件載入時最大同時請求數不得超過30個 enforceSizeThreshold: 50000, cacheGroups: { // 分組快取 // 將來自node_modules的模組提取到一個公共文件中 (由v4的vendors改名而來) defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: -10, reuseExistingChunk: true, }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true, }, }, }, }, };
開啟升級之旅
webpack每個大版本的升級,都是破壞性變革,很少向後兼容,webpack4到webpack5的升級,同樣也不例外。升級猶如去西天取經一樣,需要經過九九八十一難,才能取得真經,體會到成就感。只要沒有堅持到最後,就會前功盡棄。所以一定要有耐心。好了,廢話不多說。現在進入這個章節的主題,細數一下升級過程中踩過的各種坑。
我對webpack的升級之旅是這樣開始的, 直接在webpack4的webpack.config.js添加與提升構建速度有關的配置
module.exports = () => { return { // ... optimization: { // 此設置保證有新增的入口文件時,原有快取的chunk文件仍然可用 moduleIds: "deterministic", // 值為"single"會創建一個在所有生成chunk之間共享的運行時文件 runtimeChunk: "single", splitChunks: { // 設置為all, chunk可以在非同步和非非同步chunk之間共享。 chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all", }, }, }, }, cache: { // 將快取類型設置為文件系統,默認是memory type: "filesystem", buildDependencies: { // 更改配置文件時,重新快取 config: [__filename], }, }, }; };
報如下錯誤,webpack4 optimization.moduleIds不能設置為deterministic。

於是對webpack4進行升級, 從"webpack": "^4.39.1"升級到"webpack": "^5.36.1",,升級後,啟動編譯,報如下錯誤 configuration.devtool should match pattern "^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$"

將devtool的配置 由devtool: 'cheap-module-eval-source-map'改為devtool: 'eval-cheap-module-source-map', 繼續前行,編譯報如下錯誤:

升級 "html-webpack-plugin": "^3.2.0"到"html-webpack-plugin": "^5.3.1",繼續前行,編譯報如下錯誤:Cannot read property 'normal' of undefined

這次既有警告,又有報錯,經查告警和報錯是由於webpack5的API發生改變,而基於webpack4 API開發的一些node工具包還未同步變更, 版本與webpack5不兼容引起的,頭痛醫頭,腳痛醫腳,會事倍功半,不勝其煩。於是決定放大招,升級package.json中所有的開發時依賴到最新版本。
yarn upgrade-interactive --latest

對標紅的開發依賴包進行升級後,繼續前行,編譯報如下錯誤 Cannot find module 'webpack-cli/bin/config-yargs'

經查,是因為webpack-cli4移除了yargs模組,除了要注釋掉項目中對yargs模組的引用,還要修改package.json裡面webpack-dev-server的寫法, 將'webpack-dev-server'改為'webpack serve'。
"start:local": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env local", "start:dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env dev", "start:test": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env test", "start:prod": "cross-env NODE_ENV=development webpack-dev-server --config webpack/dev.js --progress --mode development --current-env prod",
"start:local": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env local", "start:dev": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env dev", "start:test": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env test", "start:prod": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --current-env prod",
改完之後,繼續前行,編譯報如下錯誤 Unknown options

經查是因為webpack-cli的參數寫法不對,於是按照官方文檔修改為
"start:local": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=local", "start:dev": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=dev", "start:test": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=test", "start:prod": "cross-env NODE_ENV=development webpack serve --config webpack/dev.js --progress --mode development --env currentEnv=prod",
獲取命令行自定義參數的寫法改為
module.exports = (env) => { const currentEnv = env.currentEnv; //... }
改完之後,繼續前行,編譯報如下錯誤:TypeError: merge is not a function

經查是最新版本的webpack-merge的merge導出方式有問題,修改merge的導出方式為
const { merge } = require('webpack-merge');
改完之後,繼續前行,編譯報如下錯誤: this.getOptions is not a function

經查是less-loader的配置寫法導致的, 按照最新版本的配置寫法,修改less和module.less載入器的配置
const lessLoader = [ "css-loader", "postcss-loader", { loader: "less-loader", options: { lessOptions: { javascriptEnabled: true } }, }, ]; module.exports = () => { return { // ... module: { rules: [ { test: lessReg, exclude: lessModuleReg, use: isDev ? ["style-loader", ...lessLoader] : [MiniCssExtractPlugin.loader, "happypack/loader?id=less"], }, { test: lessModuleReg, exclude: path.resolve(__dirname, "./node_modules"), // include: [path.resolve(__dirname, '../src')], use: isDev ? ["style-loader", ...lessLoader] : [ MiniCssExtractPlugin.loader, "happypack/loader?id=lessWithModule", ], }, ], }, }; };
繼續前行,編譯有如下警告: consider using [chunkhash] or [contenthash]

將項目配置中用到hash的地方,修改成contenthash
module.exports = () => { return { // ... output: { path: path.resolve(rootPath, "./dist"), filename: isDev ? "js/[name].[contenthash:8].js" : "js/[name].[chunkhash:8].js", publicPath, }, module: { rules: [ { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.ico$/], loader: "url-loader", options: { limit: 10000, name: isDev ? "image/[name][contenthash:8].[ext]" : "image/[name].[contenthash:8].[ext]", }, }, { // 添加otf字體支援 test: /\.(woff|svg|eot|ttf|otf)\??.*$/, loader: "url-loader", options: { limit: 10000, name: isDev ? "font/[name][contenthash:8].[ext]" : "font/[name].[contenthash:8].[ext]", }, }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: isDev ? "css/[name][contenthash:8].css" : "css/[name].[chunkhash:8].css", chunkFilename: isDev ? "css/[id][contenthash:8].css" : "css/[id].[chunkhash:8].css", ignoreOrder: false, }), ], }; };
修改完之後,本地開發環境終於不報錯了。可是發現修改程式碼之後頁面不自動刷新。經查是webpack5的bug, 如果在 package.json 裡面寫了 browserslist,會導致熱更新失效,解決方案是在 webpack 配置中設置 target 欄位,在開發階段使得 browserslist 失效
module.exports = (env) => { return { // ... target: process.env.NODE_ENV === "development" ? "web" : "browserslist", }; };
再看看生產編譯打包是否正常。

執行yarn build:prod之後,報如下錯誤 MainTemplate.hooks.hashForChunk is deprecated,這個報錯前面遇到過,一看就是生產模式用到的不同於開發模式的插件,與webpack5不兼容導致的。

經查解決方案是用 terser-webpack-plugin替換原來的js壓縮插件uglifyjs-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin'); // 對js進行壓縮
module.exports = () => {
return {
// ...
optimization: {
minimize: true,
minimizer: [
// terserPlugin是webpack推薦及內置的壓縮插件,cache與parallel默認為開啟狀態
// 快取路徑在node_modules/.cache/terser-webpack-plugin
new TerserPlugin({
terserOptions: {
// //github.com/terser/terser#minify-options
compress: {
warnings: false, // 刪除無用程式碼時是否給出警告
drop_debugger: true, // 刪除所有的debugger
// drop_console: true, // 刪除所有的console.*
pure_funcs: [''],
// pure_funcs: ['console.log'], // 刪除所有的console.log
},
},
}),
new CssMinimizerPlugin(),
],
},
};
};
改完之後,編譯報許多如下錯誤: You forgot to add 'mini-css-extract-plugin'

經查是因為webpack5中,happypack不再支援less-loader,修改配置文件,less-loader不開啟多進程編譯
module.exports = () => { return { // ... module: { rules: [ { test: lessReg, exclude: lessModuleReg, // use: isDev ? ['style-loader', ...lessLoader] : ['happypack/loader?id=less'], use: isDev ? ["style-loader", ...lessLoader] : [MiniCssExtractPlugin.loader, ...lessLoader], }, { test: lessModuleReg, exclude: path.resolve(__dirname, "./node_modules"), // include: [path.resolve(__dirname, '../src')], // use: isDev // ? ['style-loader', ...lessLoader] // : ['happypack/loader?id=lessWithModule'], use: isDev ? ["style-loader", ...lessLoader] : [MiniCssExtractPlugin.loader, ...lessLoader], }, ], }, plugins: [ // new Happypack({ // id: 'less', // threadPool: happyThreadPool, // use: [MiniCssExtractPlugin.loader, ...lessLoader], // }), // new Happypack({ // id: 'lessWithModule', // threadPool: happyThreadPool, // use: [MiniCssExtractPlugin.loader, ...lessLoader], // }), ], }; };
修改之後,繼續編譯,報如下錯誤:Module not found: Error: Can't resolve 'crypto'

經查webpack4 引入crypto-js模組會自動引入polyfill: crypto-browserify, webpack5默認會自動將path、crypto、http、stream、zlib、vm的node polyfill剔除,為了不影響之前的業務,我們手動添加這個工具包
yarn add -D crypto-browserify
module.exports = () => { return { // ... resolve: { fallback:{ "stream": false, "buffer": false, "crypto": require.resolve("crypto-browserify") } }, }; };
改完之後,編譯報如下警告: Conflicting values for 'process.env'

經查是webpack5 定義全局變數的寫法改變了,按照最新的語法修改如下:
module.exports = () => { return { // ... plugins: [ // webpack5 定義環境變數的寫法變了 new webpack.DefinePlugin({ "process.env.WX_JS_SDK_ENABLED": WX_JS_SDK_ENABLED, "process.env.CURRENT_ENV": JSON.stringify(currentEnv), "process.env.RELEASE_VERSION": JSON.stringify(RELEASE_VERSION), }), // webpack4的寫法 // new webpack.DefinePlugin({ // "process.env": { // WX_JS_SDK_ENABLED: WX_JS_SDK_ENABLED, // 是否真機調試SDK模式 // CURRENT_ENV: JSON.stringify(currentEnv), // RELEASE_VERSION: JSON.stringify(RELEASE_VERSION), // }, // }), ], }; };
修改完之後,編譯報如下錯誤:optimizeChunkAssets is deprecated

經查是optimize-css-assets-webpack-plugin插件與webpack5不兼容引起的警告,webpack5中同等功能的插件是css-minimizer-webpack-plugin,安裝並修改配置
yarn add -D css-minimizer-webpack-plugin
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); // 對CSS進行壓縮
module.exports = () => {
return {
// ...
optimization: {
minimize: true,
minimizer: [
// ...
// new OptimizeCSSAssetsPlugin(),
new CssMinimizerPlugin(),
],
},
};
};
改好之後,編譯報如下錯誤 complier.plugin is not a function

經查是webpack-cos-plugin插件報的錯, Webpack5 發布後,各大主流 plugin 都已經相繼適配webpack5新的plugin api, 而webpack-cos-plugin最新的版本是兩年前的,近期沒有做過維護,看完官網文檔後,手動修復一下
compiler.hooks.emit.tap('WebpackQcloudCOSPlugin', (compilation) => {
var files = _this.pickupAssetsFiles(compilation);
log('' + green('\nCOS 上傳開始......'));
_this
.uploadFiles(files, compilation)
.then(function () {
log('' + green('COS 上傳完成\n'));
})
.catch(function (err) {
log(red('COS 上傳出錯') + '::: ' + red(err.code) + '-' + red(err.name) + ': ' + red(err.message));
_this.config.ignoreError || compilation.errors.push(err);
});
});
然後在Linux機器上部署打包編譯時,用修改之後的文件替換node_modules下的同名文件
\cp -rf webpack/cos/index.js node_modules/webpack-cos-plugin/lib
運行打包命令,這次終於可以正常打包上傳了,可是發現,打包之後的文件,頁面中有些圖片展示不出來,經查,未載入出來的圖片,src的值是[object Module]

通過樣式名查找,發現程式碼中凡是通過require給圖片的src屬性賦值的圖片都載入不出來
<img src="require('assets/xxx.png')"/>
原因是url-loader最新版本默認情況下會把require引入的內容當做esModules去處理,而不是解析內容本身,所以要關閉默認解析方式。
module.exports = (env) => { return { // ... module: { rules: [ { test: /\.(png|jpe?g|gif|ico|bmp)$/i, use: [ { loader: 'url-loader', options: { esModule: false, // 增加這一句 limit: 10000, name: isDev ? 'image/[name][hash:8].[ext]' : 'image/[name].[contenthash:8].[ext]', }, }, ], }, ], }, } }
至此,大功告成。本地開發和生產打包所有的升級報錯問題都已解決。可以愉快地享受webpack5帶來全新打包體驗。
參考文章
- //stackoverflow.com/questions/59070216/webpack-file-loader-outputs-object-module
- //stackoverflow.com/questions/64557638/how-to-polyfill-node-core-modules-in-webpack-5
- //webpack.js.org/api/cli/#env
- //webpack.docschina.org/blog/2020-10-10-webpack-5-release/
- //www.npmjs.com/package/webpack-cos-plugin
- //blog.csdn.net/qq_36741436/article/details/78732201
- //webpack.js.org/api/plugins/#plugin-types

