( 2017-05-03 寫作當下使用的是 webpack 2.3.3)
Webpack 是個神奇且好用的東西,但 webpack2 的 document 目前不盡完善,有許多狀況與細節並沒有解釋或解釋詳盡。在這裡簡單的介紹使用 Webpack 完成 Code splitting ,以及使用 Chunkhash 達成解決 File Caching 的過程。
打包 (Bundle)
打包是最基本的 Webpack 功能,假設我們有個檔案叫index.js
,裡面長得像是
|
|
那麼,基本的webpack.config.js
大概定義如下
|
|
當我們執行webpack --config webpack.config.js
之後,webpack 大概做了以下的事情:
- 根據
entry
的定義,知道要打包一個叫做app
的檔案,從./src/index.js
作為起始文件,開始讀取檔案 index.js
裡面require('axios')
,因此在執行環境中尋找axios
插件,一般會從entry
的相對路徑以及node_modules
裡面找。- 把
index.js
和axios
打包成一份檔案輸出為app.js
,output
的[name].js
就是指檔案名稱用entry
所指定的,然後檔案放置的位置以path
指定。其中__dirname
是指執行webpack
指令的根目錄
也就是說,我們開發可以結構化地把檔案分拆,像一般寫程式那樣。但我們不需要在 template 中用許多個<script>
將所有js
檔案載入,並還要注意先後順序。執行完webpack
之後,只需要載入app.js
就可以了。
多個檔案打包成一個
|
|
webpack 會按照陣列的順序,一一打包檔案,然後整合為一包app.js
打包成多個檔案
|
|
這樣設定的話,webpack 最終就會打包出三個檔案,分別是app.js
,auth.js
,message.js
拆分 (Code Splitting)
每次執行webpack
,webpack就會幫我們將檔案重新打包、更新。但隨著架構愈來愈大時,我們自己的檔案、使用的第三方套件增多,我們打包出來的檔案也會愈來愈肥。這對頁面載入將是個很大的負荷。有時候我們可能只是改個一兩行,但整個app.js
就會需要更新,使用者端就需要重新載入新的一整大包。
但其實第三方套件並不像我們自己的 Code 那麼經常變動,通常是我們自己想升版才會更動。而且寫js
我們會使用的第三方套件通常挺多。如果能將這些部分與我們自己的 Code 分開打包成不同的一包,在我們更新自己的 Code 的時候,只會更新我們 Code 整合的那包,不會更動到第三方套件的那一包,可以減少一定程度的 Loading 負荷。
首先我們將webpack.config.js
中的 config
更新成
|
|
執行webpack
,axios
這個第三方套件的會被打包進vendor.js
,但我們會發現app.js
裡面還是有axios
,這是因為不同的entry
,webpack 會分開打包,各自的 dependencies 各自處理。
為了讓共同的第三方套件都打包成同一包,我們將webpack.config.js
更改為
|
|
CommonsChunkPlugin
會將各個entry
中共同的套件包裝到指定的name
檔案。所以當我們執行webpack
之後,我們會發現這次axios
只出現在vendor.js
了。
我們可以進一步將webpack.config.js
再改為
|
|
其中minChunks
是指定判定套件為共同套件的條件,將其打包進指定檔案中。若是將minChunks
指定為 2
,那麼就是說在各entry
中,總共出現2
次以上的套件就將其打包進vendor.js
這的檔案。而我們在這邊寫的 callback,是判斷套件若是有出現在node_modules
中的,就視為共同套件打包進vendor.js
中。
值得特別注意的是在這次的webpack.config.js
中,我們的entry
沒有指定vendor
,僅僅靠CommonsChunkPlugin
完成第三方套件的打包。這在 webpack 稱為 Implicit Common Vendor Chunk。
版本 (Chunkhash)
瀏覽器多有 Cache 的機制,static file (image, css, js…etc)會留有一份快取在 client 端。在一定時間內,若是瀏覽器發現這網站要求app.js
,而app.js
在本地端有一份 copy ,那瀏覽器會使用本地的app.js
copy。這個機制是為了優化網頁的讀取,但卻造成了一個麻煩的情況。當我們更新了app.js
,但因為檔名沒變,瀏覽器不會知道檔案內容改變了,會繼續使用本地端較舊的 copy 版本。
為了解決這個問題,通常的辦法是在檔案名稱加上版本號:app.v1.js
。當我們更新app.js
時,就更新版本號:app.v2.js
,讓瀏覽器認為是要求一份不同的 js ,達到即時更新的效果。
身為一位工程師,改 code 才是我們的本職。需要在每次改 code 之後去注意版本號、手動更新,是一件麻煩且浪費我們腦內記憶體的事情。透過 webpack ,我們可以輕鬆達成這件事。
將webpack.config.js
中的output
改為
|
|
webpack 打包檔案之後,會在檔案名自動添上一個chunkhash
,所以我們會得到一個像是app.3c75dbb16437c09415ac.js
的檔案名稱。每次更改 code,重新打包,webpack 就會產生一個新的 chunkhash
,也就達成了更改版本的效果。
每個打包出的檔案的chunkhash
都是 unique 的,這在打包成不同檔案的時候,可以幫助我們達到一個目的:更新app.js
時,若vendor.js
不需要更新,他可以維持舊的chunkhash
,進而達到讓瀏覽器「只更新需要更新的套件」這件事。
但實際執行webpack
後,我們會發現當我們更改了一小段index.js
的 code,產出的 app.[chunkhash].js
和 vendor.[chunkhash].js
的[chunkhash]
部分都更新了,並沒有達到預期的只更新app.js
的效果。
這是因為每次 webpack 在執行打包時,會將一些 runtime code 帶進打包出的 Common Chunk 之中,也就是我們的 vendor.js
,來幫助打包後的檔案處理引入插件。為了解決這個問題,我們再引入一個叫做 manifest
的檔案來處理這個問題。
manifest
|
|
這樣的設定下,首先 webpack 會把屬於 node_modules
中的第三方套件通通打包進 vendor
,然後再把 app
和 vendor
共同的 modules
打包進 manifest
。但因為 vendor
和 app
並沒有共同套件了,所以 manifest
最後只剩下 runtime code。
之後 html 只要按順序把 manifest.[chunkhash].js
, vendor.[chunkhash].js
和 app.[chunkhash].js
載入,就可以讓我們的 scripts 動起來了。雖然這樣我們多載入了一個檔案,但少變動 vendor
所省下的 loading 總的來說還是有益的。
HTML template
一切都已逼近完美,只差當每次執行完webpack
,我們就要更新一次我們的 template,把新的<script>
部分的檔案名中的chunkhash
更新。官方的教學是透過ChunkManifestPlugin
產出一個manifest.json
,會帶有檔案對應的資訊,然後把它直接在 template inline 引入。這種人工的作法實在令人覺得功虧一簣。另外還有一種方法是透過HtmlWebpackPlugin
幫我們產生一個帶有chunkhash
版本號的<script>
的 html template。但使用方式略複雜,而且對於 template 有需多限制,若要客製化還要再學習許多插件。
基於我們只是想要簡單的讓 webpack 幫我們自動換版本號而已,小魚自己實在不想為了這件事再多加入這些東西。經過一段時間的 google + stackoverflow 之後,先寫出了以下的暴力做法。
在webpack.config.js
的plugin
處多加一個自己寫的 Plugin
|
|
我們把<scripts>
部分的 html 獨立成一個 template,然後直接使用node
的fs
來進行檔案的 I/O,讀入<script>
的 template ,然後將檔案中的檔案名稱置換成帶有chunkhash
的字串,再輸出成production
用的 template。
我們設定this.plugin
只在NODE_ENV === 'production'
的時候執行,並且是在 webpack 已經打包完的done
的階段,把結果的資訊讀出來,然後進行操作。而在開發模式下,不用多帶chunkhash
可以省下一些麻煩,所以就略過這步驟。也可以把這段 code 寫成一個插件,然後在webpack.config.js
引入,會讓程式碼看起來乾淨些。這邊只是一個簡單的快速介紹。
要注意的是,雖然官方文件說:
Running
webpack -p
(or equivalentlywebpack --optimize-minimize --define process.env.NODE_ENV="'production'"
).
但這裡的process.env.NODE_ENV
並不是指定你執行時的NODE_ENV
,而是對於 webpack 而言的變數。因此若我們就這麼執行webpack -p --config webpack.config.js
,我們寫的那段 plugin 所認得的process.env.NODE_ENV
是執行webpack
指令的 node 環境,而非 webpack 自己設定的變數,webpack 還是只會跑開發部分的程式碼。(小魚還沒深入去了解其中的原因)
要解決這個問題,你可以使用官方的EnvironmentPlugin
來處理,或者簡單地把你的指令改成NODE_ENV=production webpack -p --config webpack.config.js
就可以了。
如此一來,在 production 環境下執行 NODE_ENV=production webpack -p --config webpack.config.js
,webpack 就會幫我們把檔案打包好,加上chunkhash
,然後再幫我們更改<script>
的 template 了。
結語
這只是最、最、最基本的 webpack 功能,完全還無法體現 webpack 的強大之處。webpack 的各種插件、各類型檔案的花式打包、各種玄妙的 bundle 設定⋯⋯才是 webpack 強大且非同凡響的地方。不然其實以上這些功能,使用其他的打包工具也是能達到相同效果的。
這次只是簡單介紹了一下 webpack 最基本的使用與設定,若要深入感受 webpack 的強大,還是要多去瞭解與研究。