目錄
- 一. Js模組化開發
- 二. Js檔案的一般打包需求
- 三. 使用webpack處理js檔案
- 3.1 使用babel轉換ES6+文法
- 3.2 指令碼合并
- 3.3 公用模組識別
- 3.4 代碼分割
- 3.5 代碼混淆壓縮
- 四. 細說splitChunks技術
- 4.1 參數說明
- 4.2 參數配置
- 4.3 代碼分割執行個體
- 五. 參考及附件說明
webpack作為前端最火的構建工具,是前端自動化工具鏈最重要的部分,使用門檻較高。本系列是筆者自己的學習記錄,比較基礎,希望通過問題 + 解決方式的模式,以前端構建中遇到的具體需求為出發點,學習webpack工具中相應的處理辦法。(本篇中的參數配置及使用方式均基於webpack4.0版本)
本篇摘要:
本篇主要介紹基於webpack4.0的splitChunks分包技術。
一. Js模組化開發
javascript之所以需要打包合并,是因為模組化開發的存在。開發階段我們需要將js檔案分開寫在很多零碎的檔案中,方便調試和修改,但如果就這樣上線,那首頁的http請求數量將直接爆炸。同一個項目,別人2-3個請求就拿到了需要的檔案,而你的可能需要20-30個,結果就不用多說了。
但是合并指令碼可不是“把所有的片段檔案都拷貝到一個js檔案裡”這樣就能解決的,不僅要解決命名空間衝突的問題,還需要相容不同的模組化方案,更別提根據模組之間複雜的依賴關係來手動確定模組的載入順序了,所以利用自動化工具來將開發階段的js指令碼片段進行合并和最佳化是非常有必要的。
二. Js檔案的一般打包需求
- 代碼編譯(
TS或ES6代碼的編譯)
- 指令碼合并
- 公用模組識別
- 代碼分割
- 代碼壓縮混淆
三. 使用webpack處理js檔案3.1 使用babel轉換ES6+文法
babel是ES6文法的轉換工具,對babel不瞭解的讀者可以先閱讀《大前端的自動化工廠(3)——Babel》一文進行瞭解,babel與webpack結合使用的方法也在其中做了介紹,此處僅提供基本配置:
webpack.config.js:
... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: [ { loader: 'babel-loader' } ] } ] }, ...
.babelrc:
{ "presets":[ ["env",{ "targets":{ "browsers":"last 2 versions" } } ]], "plugins": [ "babel-plugin-transform-runtime" ]}
3.2 指令碼合并
使用webpack對指令碼進行合并是非常方便的,畢竟模組管理和檔案合并這兩個功能是webpack最初設計的主要用途,直到涉及到分包和懶載入的話題時才會變得複雜。webpack使用起來很方便,是因為實現了對各種不同模組規範的相容處理,對前端開發人員來說,理解這種相容性實現的方式比學習如何配置webpack更為重要。webpack預設支援的是CommonJs規範,但同時為了擴充其使用情境,webpack在後續的版本迭代中也加入了對ES harmony等其他規範定義模組的相容處理,具體的處理方式將在下一章《webpack4.0各個擊破(5)—— Module篇》詳細分析。
3.3 公用模組識別
webpack的輸出的檔案中可以看到如下的部分:
/******/ function __webpack_require__(moduleId) {/******//******/ // Check if module is in cache/******/ if(installedModules[moduleId]) {/******/ return installedModules[moduleId].exports;/******/ }/******/ // Create a new module (and put it into the cache)/******/ var module = installedModules[moduleId] = {/******/ i: moduleId,/******/ l: false,/******/ exports: {}/******/ };/******//******/ // Execute the module function/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);/******//******/ // Flag the module as loaded/******/ module.l = true;/******//******/ // Return the exports of the module/******/ return module.exports;/******/ }
上面的__webpack_require__( )方法就是webpack的模組載入器,很容易看出其中對於已載入的模組是有統一的installedModules對象來管理的,這樣就避免了模組重複載入的問題。而公用模組一般也需要從bundle.js檔案中提取出來,這涉及到下一節的“代碼分割”的內容。
3.4 代碼分割
1. 為什麼要進行代碼分割?
代碼分割最基本的任務是分離出第三方依賴庫,因為第三方庫的內容可能很久都不會變動,所以用來標記變化的摘要雜湊contentHash也很久不變,這也就意味著我們可以利用本機快取來避免沒有必要的重複打包,並利用瀏覽器緩衝避免冗餘的用戶端載入。另外當項目發布新版本時,如果第三方依賴的contentHash沒有變化,就可以使用用戶端原來的快取檔案(通用的做法一般是給靜態資源請求設定一個很大的max-age),提升訪問速度。另外一些情境中,代碼分割也可以提供對指令碼在整個載入周期內的載入時機的控制能力。
2. 代碼分割的使用情境
舉個很常見的例子,比如你在做一個資料視覺效果類型的網站,引用到了百度的Echarts作為第三方庫來渲染圖表,如果你將自己的代碼和Echarts打包在一起產生一個main.bundle.js檔案,這樣的結果就是在一個網速欠佳的環境下開啟你的網站時,使用者可能需要面對很長時間的白屏,你很快就會想到將Echarts從主檔案中剝離出來,讓體積較小的主檔案先在介面上渲染出一些動畫或是提示資訊,然後再去載入Echarts,而分離出的Echarts也可以從速度更快的CDN節點擷取,如果載入某個體積龐大的庫,你也可以選擇使用懶載入的方案,將指令碼的下載時機延遲到使用者真正使用對應的功能之前。這就是一種人工的代碼分割。
從上面的例子整個的生命週期來看,我們將原本一次就可以載入完的指令碼拆分為了兩次,這無疑會加重服務端的效能開銷,畢竟建立TCP串連是一種開銷很大的操作,但這樣做卻可以換來對渲染節奏的控制和使用者體驗的提升,非同步模組和懶載入模組從宏觀上來講實際上都屬於代碼分割的範疇。code splitting最極端的狀況其實就是拆分成打包前的原貌,也就是源碼直接上線。
3. 代碼分割的本質
代碼分割的本質,就是在“源碼直接上線”和“打包為唯一的指令碼main.bundle.js”這兩種極端方案之間尋找一種更符合實際情境的中間狀態,用可接受的伺服器效能壓力增加來換取更好的使用者體驗。
4. 配置代碼分割
code-splitting技術的配置和使用方法將在下一小節詳細描述。
5. 更細緻的代碼分割
感興趣的讀者可以參考來自google開發人員社區的文章《Reduce JavaScript Payloads with Code Splitting》自行研究。
3.5 代碼混淆壓縮
webpack4中已經內建了UglifyJs外掛程式,當打包模式參數mode設定為production時就會自動開啟,當然這不是唯一的選擇,babel的外掛程式中也能提供代碼壓縮的處理,具體的效果和原理筆者尚未深究,感興趣的讀者可以自行研究。
四. 細說splitChunks技術4.1 參數說明
webpack4廢棄了CommonsChunkPlugin外掛程式,使用optimization.splitChunks和optimization.runtimeChunk來代替,原因可以參考《webpack4:連奏中的進化》一文。關於runtimeChunk參數,有的文章說是提取出入口chunk中的runtime部分,形成一個單獨的檔案,由於這部分不常變化,可以利用緩衝。google開發人員社區的博文是這樣描述的:
The runtimeChunk option is also specified to move webpack's runtime into the vendors chunk to avoid duplication of it in our app code.
splitChunks中預設的代碼自動分割要求是下面這樣的:
node_modules中的模組或其他被重複引用的模組
就是說如果引用的模組來自node_modules,那麼只要它被引用,那麼滿足其他條件時就可以進行自動分割。否則該模組需要被重複引用才繼續判斷其他條件。(對應的就是下文配置選項中的minChunks為1或2的情境)
分離前模組最小體積下限(預設30k,可修改)
30k是官方給出的預設數值,它是可以修改的,上一節中已經講過,每一次分包對應的都是服務端的效能開銷的增加,所以必須要考慮分包的性價比。
對於非同步模組,產生的公用模組檔案不能超出5個(可修改)
觸發了懶載入模組的下載時,並發請求不能超過5個,對於稍微瞭解過服務端技術的開發人員來說,【高並發】和【壓力測試】這樣的關鍵詞應該不會陌生。
對於入口模組,抽離出的公用模組檔案不能超出3個(可修改)
也就是說一個入口檔案的最大並行請求預設不得超過3個,原因同上。
4.2 參數配置
splitChunks的在webpack4.0以上版本中的用法是下面這樣的:
module.exports = { //... optimization: { splitChunks: { chunks: 'async',//預設只作用於非同步模組,為`all`時對所有模組生效,`initial`對同步模組有效 minSize: 30000,//合并前模組檔案的體積 minChunks: 1,//最少被引用次數 maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: '~',//自動命名串連符 cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, minChunks:1,//敲黑板 priority: -10//優先順序更高 }, default: { test: /[\\/]src[\\/]js[\\/]/ minChunks: 2,//一般為非第三方公用模組 priority: -20, reuseExistingChunk: true } }, runtimeChunk:{ name:'manifest' } } }
4.3 代碼分割執行個體
註:執行個體中使用的demo及設定檔已放在附件中。
splitChunks提供了更精確的分割策略,但是似乎無法直接通過html-webpack-plugin配置參數來動態解決分割後代碼的注入問題,因為分包名稱是不確定的。這個情境在使用chunks:'async'預設配置時是不存在的,因為非同步模組的引用代碼是不需要以<script>標籤的形式注入html檔案的。
當chunks配置項設定為all或initial時,就會有問題,例如上面樣本中,通過在html-webpack-plugin中配置excludeChunks可以去除page和about這兩個chunk,但是卻無法提前排除vendors-about-page這個chunk,因為打包前無法知道是否會產生這樣一個chunk。這個情境筆者並沒有找到現成的解決方案,對此情境有需求的讀者也許可以通過使用html-webpack-plugin的事件擴充來處理此類情境,也可以使用折中方案,就是第一次打包後記錄下新產生的chunk名稱,按需填寫至html-webpack-plugin的chunks配置項裡。
### 4.4 結果分析
通過Bundle Buddy分析工具或webpack-bundle-analyser外掛程式就可以看到分包前後對於公用代碼的抽取帶來的影響(圖片來自參考文獻的博文):
五. 參考及附件說明
【1】附加中檔案說明:
webpack.spa.config.js——單頁面應用代碼分割配置執行個體
main.js——單頁面應用入口檔案
webpack.multi.config.js——多頁面應用代碼分割配置執行個體
entryA.js,entryB.js,entryC.js——多頁面應用的3個入口
【2】參考文獻: 《Reduce JavaScript Payloads with Code Splitting》