1. 程式人生 > 實用技巧 >webpack4.0各個擊破(4)—— Javascript & splitChunk

webpack4.0各個擊破(4)—— Javascript & splitChunk

一. Js模組化開發

javascript之所以需要打包合併,是因為模組化開發的存在。開發階段我們需要將js檔案分開寫在很多零碎的檔案中,方便除錯和修改,但如果就這樣上線,那首頁的http請求數量將直接爆炸。同一個專案,別人2-3個請求就拿到了需要的檔案,而你的可能需要20-30個,結果就不用多說了。

但是合併指令碼可不是“把所有的碎片檔案都拷貝到一個js檔案裡”這樣就能解決的,不僅要解決名稱空間衝突的問題,還需要相容不同的模組化方案,更別提根據模組之間複雜的依賴關係來手動確定模組的載入順序了,所以利用自動化工具來將開發階段的js指令碼碎片進行合併和優化是非常有必要的。

二. Js檔案的一般打包需求

  • 程式碼編譯(TSES6程式碼的編譯)
  • 指令碼合併
  • 公共模組識別
  • 程式碼分割
  • 程式碼壓縮混淆

三. 使用webpack處理js檔案

3.1 使用babel轉換ES6+語法

babelES6語法的轉換工具,對babel不瞭解的讀者可以先閱讀《大前端的自動化工廠(3)——Babel》一文進行了解,babelwebpack結合使用的方法也在其中做了介紹,此處僅提供基本配置:

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.splitChunksoptimization.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配置項設定為allinitial時,就會有問題,例如上面示例中,通過在html-webpack-plugin中配置excludeChunks可以去除pageabout這兩個chunk,但是卻無法提前排除vendors-about-page這個chunk,因為打包前無法知道是否會生成這樣一個chunk。這個場景筆者並沒有找到現成的解決方案,對此場景有需求的讀者也許可以通過使用html-webpack-plugin事件擴充套件來處理此類場景,也可以使用折中方案,就是第一次打包後記錄下新生成的chunk名稱,按需填寫至html-webpack-pluginchunks配置項裡。

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》

作者:大史不說話
連結:webpack4.0各個擊破(4)—— Javascript & splitChunk
來源:部落格園
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。