前幾天 webpack 作者 Tobias Koppers 釋出了一篇新的文章 webpack 4.0 to 4.16: Did you know?(需翻牆),總結了一下webpack 4
釋出以來,做了哪些調整和優化。
並且說自己正在著手開發 webpack 5
。
Oh you are still on webpack 3. I’m sorry, what is blocking you? We already working on webpack 5, so your stack might be outdated soon…
翻譯成中文就是:
正好我也在使用一個文件生成工具 docz(安利一波) 也最低需要webpack 4+
,新版webpack
效能提高了不少,而且webpack 4
都已經發布五個多月了,想必應該已經沒什麼坑了,應該可以安心的按照別人寫的升級攻略升級了。之前一直遲遲不升級完全是被去年被 webpack 3
坑怕了。它在 code splitting
的情況下 CommonsChunkPlugin
會完全失效。過了好一段時間才修復,欲哭無淚。
所以這次我等了快大半年才準備升級到webpack 4
但萬萬沒想到還是遇到了不少的問題! 有很多之前遺留的問題還是沒有很好地解決。但最主要的問題還是它的文件有所欠缺,已經廢除了的東西如commonsChunkPlugin
還在官方文件中到處出現,很多重要的東西卻一筆帶過,甚至沒寫,需要使用者自己去看原始碼才能解決。
還比如在v4.16.0
版本中廢除了optimization.occurrenceOrder
、optimization.namedChunks
、optimization.hashedModuleIds
、optimization.namedModules
這幾個配置項,替換成了optimization.moduleIds
和 optimization.chunkIds
,但文件完中全沒有任何體現,所以你在新版本中還按照文件那樣配置其實是沒有任何效果的。
最新最完整的文件還是看他專案的配置WebpackOptions.json,強烈建議遇到不清楚的配置項可以看這個,因為它一定保證是和最新程式碼同步的。
吐槽了這麼多,我們言歸正傳。由於本次手摸手篇幅有些長,所以拆解成了上下兩篇文章:
- 上篇 -- 就是普通的在
webpack 3
的基礎上升級,要做哪些操作和遇到了哪些坑 - 下篇 -- 是在
webpack 4
下怎麼合理的打包和拆包,並且如何最大化利用long term caching
本文章不是手摸手從零教你 webpack 配置,所以並不會講太多很基礎的配置問題。比如如何處理 css 檔案,如何配置 webpack-dev-server,講述 file-loader 和 url-loader 之間的區別等等,有需求的推薦看 官方文件 或者 survivejs 出的一個系列教程。或者推薦看我司的另一篇 wbepack 入門文章,已同步到 webpack4 傳送門。
升級篇
前言
我一直認為模仿和借鑑是學習一個新東西最高效的方法。所以我建議還是通過借鑑一些成熟的 webpack 配置比較好。比如你專案是基於 react 生態圈的話可以借鑑 create-react-app ,下載之後npm run eject
就可以看到它詳細的 webpack 配置了。vue 的話由於新版vue cli
不支援 eject
了,而且改用 webpack-chain來配置,所以借鑑起來可能會不太方便,主要配置 地址。覺得麻煩的話你可以直接借鑑 vue-element-admin
的 配置。或者你想自己發揮,你可以借鑑 webpack 官方的各種 examples,來組合你的配置。
升級 webpack
首先將 webpack 升級到 4 之後,直接執行webpack --xxx
是不行的,因為新版本將命令列相關的東西單獨拆了出去封裝成了webpack-cli
。會報如下錯誤:
The CLI moved into a separate package: webpack-cli. Please install
webpack-cli
in addition to webpack itself to use the CLI.
所有你需要安裝npm install webpack-cli -D -S
。你也可將它安裝在全域性。
同時新版 webpack 需要Node.js 的最低支援版本為 6.11.5
不要忘了升級。如果還需要維護老專案可以使用 nvm 來做一下 node 版本管理。
升級所有依賴
因為webpack4
改了 它的hook
api ,所以所有的loaders
、plugins
都需要升級才能夠適配。
可以使用命令列 npm outdated
,列出所以可以更新的包。免得再一個個去npm
找相對於的可用版本了。
反正把devDependencies
的依賴都升級一下,總歸不會有錯。
帶來的變化
其實這次升級帶來了不少改變,但大部分其實對於普通使用者來說是不需要關注的,比如這次升級帶來的功能SideEffects
、Module Type’s Introduced
、WebAssembly Support
,基本平時是用不到的。我們主要關注那些對我們影響比較大的改動如:optimization.splitChunks
代替原有的CommonsChunkPlugin
(下篇文章會著重介紹),和Better Defaults-mode
更好的預設配置,這是大家稍微需要關注一下的。
如果想進一步瞭解
Tree Shaking
和SideEffects
的可見文末擴充閱讀。 上圖參考 Webpack 4 進階
預設配置
webpack 4 引入了零配置
的概念,被 parcel 刺激到了。 不管效果怎樣,這改變還是值得稱讚的。
最近又新出了 Fastpack 可以關注一下。
言歸正題,我們來看看 webpack 預設幫我們做了些什麼?
development
模式下,預設開啟了NamedChunksPlugin
和NamedModulesPlugin
方便除錯,提供了更完整的錯誤資訊,更快的重新編譯的速度。
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
複製程式碼
production
模式下,由於提供了splitChunks
和minimize
,所以基本零配置,程式碼就會自動分割、壓縮、優化,同時 webpack 也會自動幫你 Scope hoisting
和 Tree-shaking
。
module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
複製程式碼
webpack 一直以來最飽受詬病的就是其配置門檻極高,配置內容極其複雜和繁瑣,容易讓人從入門到放棄,而它的後起之秀如 rollup、parcel 等均在配置流程上做了極大的優化,做到開箱即用,所以webpack 4
也從中借鑑了不少經驗來提升自身的配置效率。願世間再也不需要 webpack 配置工程師。
html-webpack-plugin
用最新版本的的 html-webpack-plugin
你可能還會遇到如下的錯誤:
throw new Error('Cyclic dependency' + nodeRep)
產生這個 bug 的原因是迴圈引用依賴,如果你沒有這個問題可以忽略。
目前解決方案可以使用 Alpha 版本,npm i --save-dev html-webpack-plugin@next
或者加入chunksSortMode: 'none'
就可以了。
但仔細檢視文件發現設定成chunksSortMode: 'none'
這樣是會有問題的。
Allows to control how chunks should be sorted before they are included to the HTML.
這屬性會決定你 chunks 的載入順序,如果設定為none
,你的 chunk 載入在頁面中載入的順序就不能夠保證了,可能會出現樣式被覆蓋的情況。比如我在app.css
裡面修改了一個第三方庫element-ui
的樣式,通過載入順序的先後來覆蓋它,但由於設定為了none
,打包出來的結果變成了這樣:
<link href="/app.8945fbfc.css" rel="stylesheet">
<link href="/chunk-elementUI.2db88087.css" rel="stylesheet">
複製程式碼
app.css
被先載入了,之前寫的樣式覆蓋就失效了,除非你使用important
或者其它 css 權重的方式覆蓋它,但這明顯是不太合理的。
vue-cli
正好也有這個相關 issue,尤雨溪也在不使用@next
版本的基礎上 hack 了它,有興趣的可以自己研究一下,本人在專案中直接使用了@next
版本,也沒遇到其它什麼問題(除了不相容 webpack 的 prefetch/preload
相關 issue)。兩種方案都可以,自行選擇。
其它 html-webpack-plugin
的配置和之前使用沒有什麼區別。
mini-css-extract-plugin
與 extract-text-webpack-plugin 區別
由於webpack4
對 css 模組支援的完善以及在處理 css 檔案提取的方式上也做了些調整,所以之前我們一直使用的extract-text-webpack-plugin
也完成了它的歷史使命,將讓位於mini-css-extract-plugin
。
使用方式也很簡單,大家看著 文件 抄就可以了。
它與extract-text-webpack-plugin
最大的區別是:它在code spliting
的時候會將原先內聯寫在每一個 js chunk bundle
的 css,單獨拆成了一個個 css 檔案。
原先 css 是這樣內聯在 js 檔案裡的:
將 css 獨立拆包最大的好處就是 js 和 css 的改動,不會影響對方。比如我改了 js 檔案並不會導致 css 檔案的快取失效。而且現在它自動會配合optimization.splitChunks
的配置,可以自定義拆分 css 檔案,比如我單獨配置了element-ui
作為單獨一個bundle
,它會自動也將它的樣式單獨打包成一個 css 檔案,不會像以前預設將第三方的 css 全部打包成一個幾十甚至上百 KB 的app.xxx.css
檔案了。
壓縮與優化
打包 css 之後檢視原始碼,我們發現它並沒有幫我們做程式碼壓縮,這時候需要使用 optimize-css-assets-webpack-plugin 這個外掛,它不僅能幫你壓縮 css 還能優化你的程式碼。
//配置
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin()];
}
複製程式碼
如上圖測試用例所示,由於optimize-css-assets-webpack-plugin
這個外掛預設使用了 cssnano 來作 css 優化,
所以它不僅壓縮了程式碼、刪掉了程式碼中無用的註釋、還去除了冗餘的 css、優化了 css 的書寫順序,優化了你的程式碼 margin: 10px 20px 10px 20px;
=>margin:10px 20px;
。同時大大減小了你 css 的檔案大小。更多優化的細節見文件。
contenthash
但使用 MiniCssExtractPlugin
有一個需求特別注意的地方,在預設文件中它是這樣配置的:
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: devMode ? "[name].css" : "[name].[hash].css",
chunkFilename: devMode ? "[id].css" : "[id].[hash].css"
});
複製程式碼
簡單說明一下:
filename
是指在你入口檔案entry
中引入生成出來的檔名,而chunkname
是指那些未被在入口檔案entry
引入,但又通過按需載入(非同步)模組的時候引入的檔案。
在 copy 如上程式碼使用之後發現情況不對!每次改動一個xx.js
檔案,它對應的 css 雖然沒做任何改動,但它的 檔案 hash 還是會發生變化。仔細對比發現原來是 hash
惹的禍。 6.f3bfa3af.css
=> 6.40bc56f6.css
但我這是根據官方文件來寫的!為什麼還有問題!後來在文件的最最最下面發下了這麼一段話!
For long term caching use filename:
[contenthash].css
. Optionally add [name].
非常的不理解,這麼關鍵的一句話會放在 Maintainers
還後面的地方,預設寫在配置裡面提示大家不是更好?有熱心群眾已經開了一個pr
,將文件預設配置為 contenthash
。chunkhash
=> contenthash
相關 issue。
這個真的蠻過分的,稍不注意就會讓自己的 css 檔案快取無效。而且很多使用者平時修改程式碼的時候都不會在意自己最終打包出來的 dist
資料夾中到底有哪些變化。所以這個問題可能就一直存在了。浪費了多少資源!人艱不拆!大家覺得 webpack 難用不是沒道理的。
補充一點:目前MiniCssExtractPlugin
也不是非常完美的,它現在預設會將每個 bundle 的 css 獨立於 js檔案, 單獨拆成一個 css 檔案。但這樣會產生一個新的問題。比如我有一個頁面它只有一行css,但也會被拆成了一個獨立的css檔案,還需要額外的一次 http 請求,非常的不合理。所以我就給官方提了一個 issue,呼籲增加一個minSize
,當 css 的內容小於這個size
的時候還是內聯到 js 檔案中,期待官方增加這個功能。
這裡再簡單說明一下幾種 hash 的區別:
- hash
hash
和每次 build
有關,沒有任何改變的情況下,每次編譯出來的 hash
都是一樣的,但當你改變了任何一點東西,它的hash
就會發生改變。
簡單理解,你改了任何東西,hash
就會和上次不一樣了。
- chunkhash
chunkhash
是根據具體每一個模組檔案自己的的內容包括它的依賴計算所得的hash
,所以某個檔案的改動只會影響它本身的hash
,不會影響其它檔案。
- contenthash
它的出現主要是為了解決,讓css
檔案不受js
檔案的影響。比如foo.css
被foo.js
引用了,所以它們共用相同的chunkhash
值。但這樣子是有問題的,如果foo.js
修改了程式碼,css
檔案就算內容沒有任何改變,由於是該模組的 hash
發生了改變,其css
檔案的hash
也會隨之改變。
這個時候我們就可以使用contenthash
了,保證即使css
檔案所處的模組裡有任何內容的改變,只要 css 檔案內容不變,那麼它的hash
就不會發生變化。
contenthash
你可以簡單理解為是 moduleId
+ content
所生成的 hash
。
熱更新速度
其實相對 webpack 線上打包速度,我更關心的本地開發熱更新速度,畢竟這才是和我們每一個程式設計師每天真正打交道的東西,打包一般都會扔給CI
自動執行,而且一般專案每天也不會打包很多次。
webpack 4
一直說自己更好的利用了cache
提高了編譯速度,但實測發現是有一定的提升,但當你一個專案,路由懶載入的頁面多了之後,50+之後,熱更新慢的問題會很明顯,之前的文章中也提到過這個問題,原以為新版本會解決這個問題,但並沒有。
不過你首先要排斥你的熱更新慢不是,如:
- 沒有使用合理的 Devtool souce map 導致
- 沒有正確使用 exclude/include 處理了不需要處理的如
node_modules
- 在開發環境不要壓縮程式碼
UglifyJs
、提取 css、babel polyfill、計算檔案 hash 等不需要的操作
舊方案
最早的方案是開發環境中不是用路由懶載入了,只線上上環境中使用。封裝一個_import
函式,通過環境變區分是否需要懶載入。
開發環境:
module.exports = file => require("@/views/" + file + ".vue").default;
複製程式碼
生成環境:
module.exports = file => () => import("@/views/" + file + ".vue");
複製程式碼
但由於 webpack import
實現機制問題,會產生一定的副作用。如上面的寫法就會導致@/views/
下的 所有.vue
檔案都會被打包。不管你是否被依賴引用了,會多打包一些可能永遠都用不到 js 程式碼。 相關 issue
目前新的解決方案思路還是一樣的,只在生成模式中使用路由懶載入,本地開發不使用懶載入。但換了一種沒副作用的實現方式。
新方案
使用babel
的 plugins
babel-plugin-dynamic-import-node。它只做一件事就是:將所有的import()
轉化為require()
,這樣就可以用這個外掛將所有非同步元件都用同步的方式引入了,並結合 BABEL_ENV 這個bebel
環境變數,讓它只作用於開發環境下。將開發環境中所有import()
轉化為require()
,這種方案解決了之前重複打包的問題,同時對程式碼的侵入性也很小,你平時寫路由的時候只需要按照官方文件路由懶載入的方式就可以了,其它的都交給babel
來處理,當你不想用這個方案的時候,也只需要將它從babel
的 plugins
中移除就可以了。
具體程式碼:
首先在package.json
中增加BABEL_ENV
"dev": "BABEL_ENV=development webpack-dev-server XXXX"
複製程式碼
接著在.babelrc
只能加入babel-plugin-dynamic-import-node
這個plugins
,並讓它只有在development
模式中才生效。
{
"env": {
"development": {
"plugins": ["dynamic-import-node"]
}
}
}
複製程式碼
之後就大功告成了,路由只要像平時一樣寫就可以了。文件
{ path: '/login', component: () => import('@/views/login/index')}
複製程式碼
這樣能大大提升你熱更新的速度。基本兩百加頁面也能在2000ms
的熱跟新完成,基本做到無感重新整理。當然你的專案本身就不大頁面也不多,完全沒必要搞這些。當你的頁面變化跟不是你寫程式碼速度的時候再考慮也不遲。
打包速度
webpack 4
在專案中實際測了下,普遍能提高 20%~30%的打包速度。
本文不準備太深入的講解這部分內容,詳細的打包優化速度可以參考 slack 團隊的這篇文章,掘金還有譯文.
這裡有幾個建議來幫你加速 webpack 的打包速度。
首先你需要知道你目前打包慢,是慢在哪裡。
我們可以用 speed-measure-webpack-plugin 這個外掛,它能監控 webpack 每一步操作的耗時。如下圖:
可以看出其實大部分打包花費的時間是在Uglifyjs
壓縮程式碼。和前面的提升熱更新的切入點差不多,檢視source map
的正確與否,exclude/include
的正確使用等等。
使用新版的UglifyJsPlugin
的時候記住可以加上cache: true
、parall: true
,可以提搞程式碼打包壓縮速度。更多配置可以參考 文件 或者 vue-cli 的 配置。
編譯的時候還有還有一個很慢的原因是那些第三方庫。比如echarts
、element-ui
其實都非常的大,比如echarts
打包完也還有 775kb。所以你想大大提高編譯速度,可以將這些第三方庫 externals
出去,使用script
的方式引入,或者使用 dll
的方式打包。經測試一般如echarts
這樣大的包可以節省十幾秒到幾十秒不等。
還有可以使用一些並行執行 webpack 的庫:如parallel-webpack、happypack。
順便說一下,升級一下node
可能有驚喜。前不久將CI
裡面的 node 版本依賴從 6.9.2
=> 8.11.3
,打包速度直接提升了一分多鐘。
總之我覺得打包時間控制在差不多的範圍內就可以了,沒必要過分的優化。可能你研究了半天,改了一堆引數發現其實也就提升了幾秒,但維護成本上去了,得不償失。還不如升級 node、升級 webpack、升級你的編譯環境的硬體水平來的實在和簡單。
比如我司CI
使用的是騰訊雲普通的的 8 核 16g 的機器,這個專案也是一個很大的後臺管理專案 200+頁面,引用了很多第三方的庫,但沒有使用什麼happypack
、dll
,只是用了最新版的webpack4
,node@8.11.3
。
編譯速度穩定在兩分多鐘,完全不覺得有什麼要優化的必要。
Tree-Shaking
這其實並不是 webpack 4 才提出來的概念,最早是 rollup 提出來並實現的,後來在 webpack 2 中就實現了,本次在 webpack 4 只是增加了 JSON Tree Shaking
和sideEffects
能讓你能更好的搖。
不過這裡還是要提一下,預設 webpack 是支援Tree-Shaking
的,但在你的專案中可能會因為babel
的原因導致它失效。
因為Tree Shaking
這個功能是基於ES6 modules
的靜態特性檢測,來找出未使用的程式碼,所以如果你使用了 babel 外掛的時候,如:babel-preset-env,它預設會將模組打包成commonjs
,這樣就會讓Tree Shaking
失效了。
其實在 webpack 2 之後它自己就支援模組化處理。所以只要讓 babel 不transform modules
就可以了。配置如下:
// .babelrc
{
"presets": [
["env", {
modules: false,
...
}]
]
}
複製程式碼
順便說一下都 8102 年了,請不要在使用babel-preset-esxxxx
系列了,請用babel-preset-env
,相關文章 再見,babel-preset-2015。