webpack源碼學習系列之二:code-splitting(代碼切割)

前言

上一篇以後,咱們今天來看看如何實現 webpack 的代碼切割(code-splitting)功能,最後實現的代碼版本請參考這裏。至於什麼是 code-splitting ,爲何要使用它,請直接參考官方文檔前端

目標

通常說來,code-splitting 有兩種含義:node

  1. 將第三方類庫單獨打包成 vendor.js ,以提升緩存命中率。(這一點咱們不做考慮)
  2. 將項目自己的代碼分紅多個 js 文件,分別進行加載。(咱們只研究這一點)

換句話說,咱們的目標是:將原先集中到一個 output.js 中的代碼,切割成若干個 js 文件,而後分別進行加載。 也就是說:原先只加載 output.js ,如今把代碼分割到3個文件中,先加載 output.js ,而後 output.js 又會自動加載 1.output.js 和 2.output.js 。jquery

切割點的選擇

既然要將一份代碼切割成若干份代碼,總得有個切割點的標誌吧,從哪兒開始切呢?
答案:webpack 使用require.ensure做爲切割點。webpack

然而,我用 nodeJS 也挺長時間了,怎麼不知道還有require.ensure這種用法?而事實上 nodeJS 也是不支持的,這個問題我在CommonJS 的標準中找到了答案:雖然 CommonJS 通俗地講是一個同步模塊加載規範,可是其中是包含異步加載相關內容的。只不過這條內容只停留在 PROPOSAL (建議)階段,並未最終進入標準,因此 nodeJS 沒有實現它也就不奇怪了。只不過 webpack 剛好利用了這個做爲代碼的切割點。git

ok,如今咱們已經明白了爲何要選擇require.ensure做爲切割點了。接下來的問題是:如何根據切割點對代碼進行切割? 下面舉個例子。github

例子

// example.js
var a = require("a");
var b = require("b");
a();
require.ensure(["c"], function(require) {
    require("b")();
    var d = require("d");
    var c = require('c');
    c();
    d();
});

require.ensure(['e'], function (require) {
   require('f')();
});複製代碼

假設這個 example.js 就是項目的主入口文件,模塊 a ~ f 是簡簡單單的模塊(既沒有進一步的依賴,也不包含require.ensure)。那麼,這裏一共有2個切割點,這份代碼將被切割爲3部分。也就說,到時候會產生3個文件:output.js ,1.output.js ,2.output.jsweb

識別與處理切割點

程序如何識別require.ensure呢?答案天然是繼續使用強大的 esprima 。關鍵代碼以下:算法

// parse.js
if (expression.callee && expression.callee.type === 'MemberExpression'
    && expression.callee.object.type === 'Identifier' && expression.callee.object.name === 'require'
    && expression.callee.property.type === 'Identifier' && expression.callee.property.name === 'ensure'
    && expression.arguments && expression.arguments.length >= 1) {

    // 處理require.ensure的依賴參數部分
    let param = parseStringArray(expression.arguments[0])
    let newModule = {
        requires: [],
        namesRange: expression.arguments[0].range
    };
    param.forEach(module => {
        newModule.requires.push({
            name: module
        });
    });

    module.asyncs = module.asyncs || [];
    module.asyncs.push(newModule);

    module = newModule;

    // 處理require.ensure的函數體部分
    if(expression.arguments.length > 1) {
        walkExpression(module, expression.arguments[1]);
    }
}複製代碼

觀察上面的代碼能夠看出,識別出require.ensure以後,會將其存儲到 asyncs 數組中,且繼續遍歷其中所包含的其餘依賴。舉個例子,example.js 模塊最終解析出來的數據結構以下圖所示:
express

image

module 與 chunk

我在剛剛使用 webpack 的時候,是分不清這兩個概念的。如今我能夠說:「在上面的例子中,有3個 chunk,分別對應 output.js、1.output.js 、2.output.js;有7個 module,分別是 example 和 a ~ f。json

因此,module 和 chunk 之間的關係是:1個 chunk 能夠包含若干個 module。
觀察上面的例子,得出如下結論:

  1. chunk0(也就是主 chunk,也就是 output.js)應該包含 example 自己和 a、b 三個模塊。
  2. chunk1(1.output.js)是從 chunk0 中切割出來的,因此 chunk0 是 chunk1 的 parent。
  3. 原本 chunk1 應該是包含模塊 c、b 和 d 的,可是因爲 c 已經被其 parent-chunk(也就是 chunk1)包含,因此,必須將 c 從 chunk1 中移除,這樣方能避免代碼的冗餘。
  4. chunk2(2.output.js)是從 chunk0 中切割出來的,因此 chunk0 也是 chunk2 的 parent。
  5. chunk2 包含 e 和 f 兩個模塊。

好了,下面進入重頭戲。

構建 chunks

在對各個模塊進行解析以後,咱們能大概獲得如下這樣結構的 depTree。

image

下面咱們要作的就是:如何從8個 module 中構建出3個 chunk 出來。 這裏的代碼較長,我就不貼出來了,想看的到這裏的 buildDep.js

其中要重點注意是:前文說到,爲了不代碼的冗餘,須要將模塊 c 從 chunk1 中移除,具體發揮做用的就是函數removeParentsModules,本質上無非就是改變一下標誌位。最終生成的chunks的結構以下:

image

拼接 output.js

經歷重重難關,咱們終於來到了最後一步:如何根據構建出來的 chunks 拼接出若干個 output.js 呢?
此處的拼接與上一篇最後提到的拼接大同小異,主要不一樣點有如下2個:

  1. 模板的不一樣。原先是一個 output.js 的時候,用的模板是 templateSingle 。如今是多個 chunks 了,因此要使用模板 templateAsync。其中不一樣點主要是 templateAsync 會發起 jsonp 的請求,以加載後續的 x.output.js,此處就不加多闡述了。仔細 debug 生成的 output.js 應該就能看懂這一點。
  2. 模塊名字替換爲模塊 id 的算法有所改進。原先我直接使用正則進行匹配替換,可是若是存在重複的模塊名的話,好比此例子中 example.js 出現了2次模塊 b,那麼簡單的匹配就會出現錯亂。由於 repalces 是從後往前匹配,而正則自己是從前日後匹配的。webpack 原做者提供了一種很是巧妙的方式,具體的代碼能夠參考這裏

後話

其實關於 webpack 的代碼切割還有不少值得研究的地方。好比本文咱們實現的例子僅僅是將1個文件切割成3個,並未就其加載時機進行控制。好比說,如何支持在單頁面應用切換 router 的時候再加載特定的 x.output.js?

注:更多系列文章請移步個人博客

-------- EOF -----------


本文對你有幫助?歡迎掃碼加入前端學習小組微信羣:

相關文章
相關標籤/搜索