一. 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的主要作用是将引用的第三方庫拆分出來。從下面的分包結果就可以看出,node_modules中的第三方引用被分離了出來,放在了vendors-main.[hash].js中。
-
多頁面應用
多頁面應用的情形稍顯複雜,以《webpack4:連奏中的進化》一文中的例子進行代碼分割處理,源碼的依賴關系為:
經過代碼分割後得到的包如下圖所示:entryA.js: vue vuex component10k entryB.js: vue axios component10k entryC.js: vue vuex axios component10k複制代碼
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插件就可以看到分包前後對于公共代碼的抽取帶來的影響(圖檔來自參考文獻的博文):
五. 參考及附件說明
- webpack.spa.config.js——單頁面應用代碼分割配置執行個體
- main.js——單頁面應用入口檔案
- webpack.multi.config.js——多頁面應用代碼分割配置執行個體
- entryA.js,entryB.js,entryC.js——多頁面應用的3個入口