大家好,我是若川。我持續組織了近一年的源碼共讀活動,感興趣的可以?點此掃碼加我微信?lxchuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列。另外:目前建有江西|湖南|湖北
籍前端群,可加我微信進群。
在嘗試升級 webpack5 之前,建議大家盡量先把官方文檔[1]通讀一遍,可以少走很多彎路,本文是在結合具體業務場景后,對官方文檔的歸納和補充。
背景
music-musician-web-node 是音樂人場景的核心應用,覆蓋的頁面多,代碼量大,在使用 webpack4 的情況下,本地開發的編譯效率一直不是很好,以筆者使用的 MacBook Pro M1 為例,音樂人 + 版權總共 45 個頁面,啟動一次全量構建需要花費 140s 左右 (已經加了諸如 happypack 之類的并行打包策略)
增量構建一般是 4-5s,但是一旦修改引用次數比較多的比如公用的 ts 定義,則增量編譯時間一度能達到 80s 以上
因此,隨著業務需求的增多,為了提升需求交付效率,解決開發體驗的問題,就變得非常急迫。在解決開發體驗方面,升級 webpack 是最快捷且直接能帶來巨大收益的手段。
webpack5 帶來的提升
webpack4 到 webpack5 畢竟有很多的 break change,為了讓大家能有升級的熱情,那么還是必須先展示升級帶來的提升。我們就以創作者中心的數據來說明。在上面的背景里面我們已經講了原先開發時 webpack 啟動和增量構建的時間分別是140s
和平均4-5s
,這里我們直接列出升級后的成果
啟動時間降低
webpack 啟動時間縮短到55s
左右,減少了近60%
增量時間降低
增量構建時間非常夸張,直接到了1s
左右,即使是原先可能會導致構建時間達到 80s 以上的 ts 定義修改,也被降低到3s
左右,平均減少80% 以上
生產包構建時間
之前在本地大約是 240s 左右,升級后為 200s。注意:一定要升級 terser-webpack-plugin 到最新版,否則生產包構建時間相較于 webpack4 可能還會降低。
包體積變化
因為創作者中心的項目頁面非常多,不太好比較 webpack4 和 webpack5 打包結果體積的變化,但是從官方優化策略(這個優化策略還導致了很多 break change,這個在下文會細講)和很多實踐結果看,對包體積都是有優化的。
升級前置準備
從這里開始,我們就正式著手 webpack 的升級。在正式升級前,需要先做一些前置工作,這些工作可以幫我們規避之后升級過程中一些奇怪的問題。
將 webpack 升級到 v4 的最新版本:如果你當前使用的版本就是 v4,升級應該是無痛的,但如果 webpack 是 v3 甚至更早的版本,請參考官方文檔[2]先升級到 v4
將 webpack-cli 升級到最新版
將使用的 loader 和 plugin 都升級到兼容 v4 的最新版本:這里要留意每個 plugin 和 Loader 對于 webpack 版本的要求,因為有些 loader/plugin 的最新版是只兼容 webpack5 的,我們這里還暫時不需要升級到這一步
因為每個項目所使用的 loader 和 plugin 有一些差異,因此這里實在不方便列出所有對應的版本,只能以筆者自身的項目為例,把常見的 plugin 和 loader webpack4 對應的最新版本列出來,其他的請根據自己項目的情況,到 npm 或者 github 查找 readme 或者翻閱 release 記錄,幾乎所有符合標準的工具庫都會給出
terser-webpack-plugin: webpack4 請升級到 4.x, webpack5 升級到最新
babel-loader: 無論 v4 或者 v5, 升級到 7.x 或者 8.x 都可以
extract-text-webpack-plugin: v4 請升級到最新版,v5 之后用 mini-css-extract-plugin 進行替換
optimize-css-assets-webpack-plugin: v4 升級到最新版,v5 用 css-minimizer-webpack-plugin 替換
ts-loader: v4 升級到 8.x,v5 升級最新版
less-loader: 7.x 為兼容 webapck4 的最后版本
sass-loader: 10.x 為兼容 webpack4 的最后版本
css-loader: 5.x 為兼容 webpack4 的最后版本
postcss-loader: 4.x 為兼容 webpack4 的最后版本
...
修復 warning 和 errors
做完上述的前置準備以后,不出意外,運行編譯流程,會有一些報警甚至是錯誤,請修復這些問題。
更改 v4 版本過時的寫法
v4 版本就在告警的過時寫法在 v5 版本一般都會直接拋棄,所以我們需要對照官方給出的指引,進行更改,否則升級 webpack5 后 webpack 是無法啟動的
optimization.hashedModuleIds: true → optimization.moduleIds: 'hashed'
optimization.namedChunks: true → optimization.chunkIds: 'named'
optimization.namedModules: true → optimization.moduleIds: 'named'
NamedModulesPlugin → optimization.moduleIds: 'named'
NamedChunksPlugin → optimization.chunkIds: 'named'
HashedModuleIdsPlugin → optimization.moduleIds: 'hashed'
optimization.noEmitOnErrors: false → optimization.emitOnErrors: true
optimization.occurrenceOrder: true → optimization: { chunkIds: 'total-size', moduleIds: 'size' }
optimization.splitChunks.cacheGroups.vendors → optimization.splitChunks.cacheGroups.defaultVendors
optimization.splitChunks.cacheGroups.test(module, chunks) → optimization.splitChunks.cacheGroups.test(module, { chunkGraph, moduleGraph })
Compilation.entries → Compilation.entryDependencies
serve → serve is removed in favor of DevServer
Rule.query (deprecated since v3) → Rule.options/UseEntry.options
測試 webpack5 的兼容性
這一步非常重要,webpack5 相比于 webpack4 一個非常顯著的差異,就是 webpack5 為了優化 bundle size,不再默認支持 node polyfill,所以一旦一些 node 與 browser 環境公用的包內部使用了 node 的全局變量,則會引發非常嚴重的 runtime 階段的報錯,為了盡量提前發現問題,我們需要在去掉 node 全局變量的情況下先測試下代碼的運行情況 注意:這里的寫法僅用于 webpack4 測試兼容性階段,在升級 webpack5 之后記得去掉
module.exports?=?{//?...node:?{Buffer:?false,process:?false,},
};
在補充完上述代碼后,先運行自己代碼看看,是否會遇到頁面報錯,注意,這些報錯是 runtime 階段的,并不是編譯階段就會出現的,所以得實際運行頁面才能看出來。如果你的代碼直接或者間接依賴了 node 的 process 和 Buffer 變量,那么你肯定會遇到下面的報錯
ReferenceError:?Buffer?is?not?defined
或者
ReferenceError:?process?is?not?defined
在實際的業務場景中,我們一個工程往往有非常多的頁面,且每個頁面狀態繁多,無法簡單的看出是否有 runtime 報錯,所以我建議讀者直接就當做代碼中確實就是有 node 全局變量的引用,然后在升級 webpack5 的時候手動進行 polyfill,這樣可以將問題概率降到最低,具體怎么做 polyfill,接下來就會講到。
開始 webpack5 升級
在上述前置準備完成后,我們可以開始進行正式的升級了。
安裝最新 webpack 版本
nenpm install -D webpack@latest
更改配置
移除
optimization.moduleIds
和optimization.chunkIds
的配置,webpack5 優化了 chunkId 和 moduleId 的默認生成策略,直接用默認的會更高效使用
[hash]
占位符的地方都可以換成[contenthash]
,后者會更高效IgnorePlugin
在入參為 regexp 的時候,寫法改成new IgnorePlugin({ resourceRegExp: /regExp/ })
對于 webpack4 中如
node.fs: 'empty'
的寫法,在 webpack5 中,node 屬性只支持三個字段,global
,__dirname
,__filename
,參考[3],所以除這三個屬性外的 node 屬性,需要放到resolve.fallback
中,因此node.fs: 'empty'
需要改為resolve.fallback.fs: false
url-loader
,raw-loader
,file-loader
建議直接用Assets Module[4]替換,雖然不換也暫時不影響,但是在后面的版本,這三個 loader 可能被移除
針對optimization.splitChunks
,在 v5 版本盡量參考下列配置
splitChunks 推薦使用默認配置,或者直接
optimization.splitChunks: { chunks: 'all' }
如果原先有這種寫法
optimization.splitChunks.cacheGroups: { default: false, vendors: false }
,需要改成optimization.splitChunks.cacheGroups: { default: false, defaultVendors: false }
需要更正或者省略的寫法
/* webpackChunkName: '...' */
之前的代碼邏輯中,我們會使用這種注釋寫法給 code split 的 chunk 命名,但是在 v5 版本,可以不用這么做,當 mode 為 development 的時候,webpack 會自動以文件名來命名 chunk
引用 Json 的變化
原先的這種寫法
import { version } from './package.json';
console.log(version);
需要改成
import pkg from './package.json';
console.log(pkg.version);
處理升級過程中的錯誤
一般來說,在升級過程中,會遇到三類問題
schema 錯誤
webpack 的配置語法錯誤,這種錯誤很好解決,在報錯信息中 webpack 會詳細標明錯誤原因和改進方法,按照提示進行修復即可。
編譯時報錯
這類問題也不難解決,最常見的就是因為 webpack5 升級后不再做 node 的 polyfill,會報module not found
,這里列出官方給出的 webpack4 默認的 polyfill,大家根據業務場景的報錯,自行下載對應的 npm 模塊,然后添加配置即可(不需要全部加上,根據報錯內容按需添加)
module.exports?=?{//...resolve:?{fallback:?{assert:?require.resolve('assert'),buffer:?require.resolve('buffer'),console:?require.resolve('console-browserify'),constants:?require.resolve('constants-browserify'),crypto:?require.resolve('crypto-browserify'),domain:?require.resolve('domain-browser'),events:?require.resolve('events'),http:?require.resolve('stream-http'),https:?require.resolve('https-browserify'),os:?require.resolve('os-browserify/browser'),path:?require.resolve('path-browserify'),punycode:?require.resolve('punycode'),process:?require.resolve('process/browser'),querystring:?require.resolve('querystring-es3'),stream:?require.resolve('stream-browserify'),string_decoder:?require.resolve('string_decoder'),sys:?require.resolve('util'),timers:?require.resolve('timers-browserify'),tty:?require.resolve('tty-browserify'),url:?require.resolve('url'),util:?require.resolve('util'),vm:?require.resolve('vm-browserify'),zlib:?require.resolve('browserify-zlib'),},},
};
還有一種可能的情況是之前能引用的 npm 包,在升級以后引用不到了,需要變更寫法,這種比較少見,但是創作者中心這里遇到過。應用依賴的二方包依賴了 uuid 這個庫,之前的寫法是
import?uuidv4?from?'uuid/dist/v4';//?...
然而升級之后,這種寫法在編譯時就報錯了,需要改成
import?{?v4?as?uuidv4?}?from?'uuid';
這種錯誤就很難預料,但好在是編譯時的報錯,還是有跡可循的,遇到后再 google 其實問題也不大。
runtime 階段報錯
以創作者中心升級的經驗來看,會有兩種引發 runtime 階段報錯的原因
node 全局變量 polyfill
最常見的也是 node polyfill 的丟失,上面也提到過,node 與 browser 公用的一些包中會使用一些 node 的全局變量 (process,Buffer),這些變量是直接使用的,并沒有 require,因此在編譯階段不會暴露。為了解決這類問題,需要這么配置
{
plugins:?[new?webpack.ProvidePlugin({Buffer:?['buffer',?'Buffer'],process:?'process/browser',}),],
}
使用 ProvidePlugin 將變量注入到全局,這里注意要下載對應的 buffer 和 process/browser 的包,這里我推薦大家都將 buffer 和 process 的 polyfill 加上,以防萬一
import 后變量丟失
這個也是升級過程中實際遇到過的。部分 npm 包導出的寫法并不是特別規范,在 webpack4 的版本會自動被兼容掉,但是 webpack5 因為默認支持 tree shaking,對于 npm 包的導出寫法會更嚴格,因此存在一種情況,即
import?watermark?from?'watermark-dom'
這種寫法在升級后,引用到的變量是 undefined,同時也沒有警告和編譯錯誤 需要改成
import?{?watermark?}?from?'watermark-dom';
這種 runtime 階段的報錯處理起來就非常棘手,關鍵在于難以預料,無法防范,上面只是列舉了創作者中心升級過程中的 runtime 報錯,不同的業務場景可能還會遇到其他的報錯,所以千萬不要有僥幸心理,唯一穩妥的辦法是將升級的影響范圍告知 qa,讓 qa 做一次完整回歸。
部分 loader 的優化
去掉 cache 相關的 loader 和 plugin
webpack5 內部對于 cache 的優化和利用已經非常好了,不需要再使用 webpack4 階段用來優化緩存的 loader 和 plugin 來提升性能,只會引發未知的問題,建議全部去掉。
用 thread-loader 替換 happypack
happypack 的作者本人已經不再維護該模塊,而 thread-loader 是官方推出的多線程編譯優化方案,性能據說要好不少,推薦在升級完成后進行替換。
使用 css-minimizer-webpack-plugin 替換 optimize-css-assets-webpack-plugin
該 loader 用于 css 代碼壓縮,官方給的建議是升級后進行替換
用 @babel/preset-typescript 替換 ts-loader
推薦嘗試,不僅僅在配置上簡化很多,一套 babel 配置吃遍所有 js,同時在性能方面,個人體驗后感覺確實會快不少。不過 @babel/preset-typescript 因為沒有再走 tsc,所以編譯時不會再像 ts-loader 那樣將編譯錯誤暴露出來,這里需要單獨裝一個插件 fork-ts-checker-webpack-plugin 來進行編譯過程中的語法校驗
后記
webpack5 升級最難解決的問題還是 runtime 階段的異常報錯,這些報錯都是跟具體的業務場景掛鉤,無法枚舉。本文的目的也是以當前業務場景的升級經驗,將一些問題處理思路提供出來供大家參考。以上就是本次創作者中心升級 webpack 的總結,希望對大家有所幫助。
本文發布自網易云音樂技術團隊,文章未經授權禁止任何形式的轉載。我們常年招收各類技術崗位,如果你準備換工作,又恰好喜歡云音樂,那就加入我們 grp.music-fe (at) corp.netease.com!
參考資料
[1]
官方文檔: https://webpack.js.org/migrate/5/
[2]官方文檔: https://webpack.js.org/migrate/4/
[3]參考: https://webpack.js.org/configuration/node/
[4]Assets Module: https://webpack.js.org/guides/asset-modules/