webpack之程式碼拆分

瀟湘待雨發表於2019-03-04

作為當前風頭正盛的打包工具,webpack風靡前端界。確實作為引領了一個時代的打包工具,很多方面都帶來了顛覆性的改進,讓我們更加的感受到自動化的快感。不過最為大家詬病的一點就是用起來太難了。
要想愉快的使用,要使用n多的配置項,究其原因在於文件的不夠詳細、本身預設整合的不足。
也不能說這是缺點吧,更多的主動權放給使用者就意味著配置工作量的增加,這裡就不過多探討了。
當歷盡千辛萬苦,你的專案跑起來之後,可能會發現有一些不太美好的問題的出現,編譯慢、打包檔案大等。那麼,我們還要花些時間來看看怎麼優化相關配置了。 下面一起看下code splitting

code splitting出現的背景

對於前端資源來說,檔案體積過大是很影響效能的一項。特別是對於移動端的裝置而言簡直是災難。
此外對於某些只要特定環境下才需要的程式碼,一開始就載入進來顯然也不那麼合理,這就引出了按需載入的概念了。

為了解決這些情況,程式碼拆分就應運而生了。程式碼拆分故名思意就是將大的檔案按不同粒度拆分,以滿足解決生成檔案體積過大、按需載入等需求。
具體到webpack而言有下面幾種方式來達到我們的目的。

webpack實現程式碼拆分的方式

webpack通過下面三種方式來達到以上目的

  1. Entry Points: 多入口分開打包
  2. Prevent Duplication:去重,抽離公共模組和第三方庫
  3. Dynamic Imports:動態載入 這裡不去扒文件上的定義了,我們從一個例子中來逐步體會他們不同的作用。

假設我們有這麼個專案,有下面幾個檔案

webpack之程式碼拆分

程式碼很簡單(示例而已,直接用commonjs的語法來寫了):

//a.js
var react = require('react')
var tool = require('./tool')
var b = require('./b')
function load(){
    b()
    tool()
    console.log('全部檔案都從一個入口打包')
}
load()
//b.js
var react = require('react')
var tool = require('./tool')
function b(){
    tool()
    console.log('這是bjs檔案')
}
module.exports = b;
//tool.js
var react = require('react')
function tool(){
    console.log('這是tooljs檔案')
}
module.exports = tool;
複製程式碼

配置很簡單:

var webpack = require('webpack');module.exports = {
    entry: './codesplitting/c1/a.js',
    output: {
        path: __dirname,
        filename: '/dist/index.js'
    }
    //*****
}
複製程式碼

直接打包:可以看到檔案大小有2047行,體積也變大了

webpack之程式碼拆分

目前只引入了react,並且業務程式碼幾乎沒有的情況下。大家可以猜到實際專案中的情況了。來讓我們進行第一優化

Entry Points

如果業務中的專案不是單頁面應用,這一步可以忽略了,直接是多入口打包。這裡是為了演示效果,強行分一個模組出來打包,假設我們的檔案也很大,需要將b.js單獨打個包出來:

    entry: {
        index:'./codesplitting/c1/a.js',
        other:'./codesplitting/c1/b.js'
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js'
    },
    //***
複製程式碼

這裡a.js也需要修改,去掉對b的引用。入口檔案之間不能相互引用的。不然,問題就大了,到底以誰為主呢,這樣就陷入了迴圈引用的問題。 此時的生成檔案如下:

webpack之程式碼拆分

看來檔案竟然只小了那麼一點了吧?第一步的優化這裡就完成了,顯然你會認為我在開玩笑。
當然這只是萬里長征第一步,看一下dist下的檔案不難發現兩個檔案中都把react這個第三方庫和tool.js這個可複用模組打進去了,顯然這樣重複打包有點沒必要。
是不是可以把這些複用性強的模組拿出來單獨打包呢?
這樣瀏覽器第一次請求之後就會將該檔案快取起來,從服務端請求的只有體積縮小之後的業務檔案了,這樣的話載入速度顯然會有所提升。
如果你也是這麼想的,來一起繼續看下去。

Prevent Duplication

webpack去除重複引用是通過CommonsChunkPlugin外掛來實現的。該外掛的配置項如下:

{
    //被抽離為公共檔案的chunk名,例如common,可以是string或者陣列
    //顯然如果是單個的模組,就是name多個就是names
    name:string,
    names:[],
    //打包之後公共模組的名稱模板
    //例如'[name].js'
    //如果省略,則和name名稱一致
    filename:string,
     //模組被引的最小次數,也就是說至少有幾個元件引用了該模組。
    //如果是Infinity,則表明單純的建立,並不做任何事情
    minChunks:2  
}
複製程式碼

具體在webpack中去重對於第三方庫顯示宣告vendor,公共模組宣告common的方式來處理

entry: {
        index:'./codesplitting/c1/a.js',
        other:'./codesplitting/c1/b.js',
        //第三方庫顯示宣告
        vendor:['react'],
        //公共元件宣告為common
        common:['./codesplitting/c1/tool']
    },
    //***
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            names:["common", "vendor"],
            filename: "[name].js"
        })  
    ]
複製程式碼

打包結果如下:

webpack之程式碼拆分

可以看到index和other兩個業務包已經很小了,react被抽離到單獨的包中。
這樣還有一個問題,對於某些程式碼可能只有在特定條件下才執行,或者可能就不執行。
我不希望在首屏就去載入它,也就是我們常說的按需載入是要怎麼做呢。一起看下去。

Dynamic Imports

webpack建議如下兩種方式使用動態載入。

1)、ECMAScript中出於提案狀態的import()

2)、webpack 特定的 require.ensure

我們這裡就是用第二種來看下效果(畢竟偷懶沒用babel...),在ajs中動態引入di.js

    //雖然始終會載入,大家能明白就行
    if(true){
        require.ensure([],function(require){
            var di = require('./di')
        })
    }
    //新增動態載入的js
    function di(){
        tool()
        console.log('這是動態引入的檔案')
    }
    module.exports = di;
複製程式碼

執行之後可以發現多了個2.2.js,開啟可以發現就是我們新建的動態引入的di.js

webpack之程式碼拆分

大家可能會問怎麼確定就是動態引入的呢,雖然本示例只能看打包之後的例子(就不引入dev server了,畢竟是懶。。。)我們依然可以從程式碼裡看到結果。 首先、檢視index.js檔案,可以看到下面的程式碼:

      var react = __webpack_require__(2)
	   var tool = __webpack_require__(1)  
	   /****省略8*****/
      //雖然始終會載入
	    if(true){
	        __webpack_require__.e/* nsure */(2, function(require){
	            var di = __webpack_require__(13)
	        })
	    }
複製程式碼

與直接require的模組不同,require.ensure被轉化為了 webpack_require.e方法,來繼續看一下該方法有什麼用。

   	__webpack_require__.e = function requireEnsure(chunkId, callback) {
		// "0" is the signal for "already loaded"
		if(installedChunks[chunkId] === 0)
			return callback.call(null, __webpack_require__);

		// an array means "currently loading".
		if(installedChunks[chunkId] !== undefined) {
			installedChunks[chunkId].push(callback);
		} else {
			// start chunk loading
			installedChunks[chunkId] = [callback];
			var head = document.getElementsByTagName('head')[0];
			var script = document.createElement('script');
			script.type = 'text/javascript';
			script.charset = 'utf-8';
			script.async = true;

			script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"common","1":"index","3":"other"}[chunkId]||chunkId) + ".js";
			head.appendChild(script);
		}
	};
複製程式碼

結合註釋直接從原始碼中可以看出來,最後面的條件語句來建立script標籤進而實現動態載入的。所謂動態載入本質還是要建立script標籤來實現的。

結束語

至此程式碼分割部分的優化已經完成了,以上是個人關於程式碼分割的簡單理解,拋磚引玉,共同學習進步。更多請移步github檢視

相關文章