webpack 的輸出文件居然這麼妙!

序:javascript

5月份的時候個人好朋友(@楊鵬)看了 github 上的博客年少時的孤芳自賞,特地跑來誇獎一番。他期待我更新,我回復到6月會更一篇。可是個人整個 6 月都在忙(懶)着一個項目的重構,致使只要有點時間就去 B 站消遣去了。確實很久不寫了,楊鵬的誇讚當勉勵,也當是督促。這一篇給楊鵬,祝好!html

1. demo 代碼

本篇所用 webpack 爲 v4.x 版本java

  • webpack.config.js
var path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    bundle: './src/a.js'
  },
  devtool: 'none',
  output: {
    path: __dirname + '/dist',
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:8].js'
  },
  mode: 'development',
  plugins: [new HtmlWebpackPlugin()],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/env']
            }
          }
        ]
      }
    ]
  }
};

複製代碼
  • 入口 a.js
import { add } from './b';
import('./c.js').then(m => m.minus(2, 1));

const A_NUM = 1;
let r = add(3, 2 + A_NUM);

console.log(r);

複製代碼
  • 模塊 b.js
export const SOME_VAR = 'SOME_VAR'

export function add(a, b) {
  return a + b
}

複製代碼
  • 模塊 c.js
import('./b.js').then(m => m.add(200, 100));

export function minus(a, b) {
  return a - b;
}
複製代碼
  • 模塊 d.js
export const L = 'Aragaki Yui'

export function times(a, b) {
  return a * b
}
複製代碼

2. 打包輸出文件

注意打包的模式是開發模式,不要混淆代碼,咱們還要讀這些代碼,輸出文件以下node

  • 0.a619de3d.js (下稱 chunk)
  • bundle.b05e.js (下稱 bundle)
  • index.html

3. 刪除空註釋

這一步是下降心理難度的重要手段,不少人都是被這一大堆的註釋勸退的;因此把相似下面的註釋都替換成空,沒錯,用你的IDE,Cmd + R;暫時移除如下注釋:bundle 和 chunk 的處理相同。webpack

  • bundle 裏面的空註釋,示例:
/******/
 
/***/
複製代碼
  • 模塊前的註釋,示例

個註釋是用來提示模塊導出了那些內容,暫時忽略git

/*!******************!*\ !*** ./src/a.js ***! \******************/
/*! no exports provided */
複製代碼

4. bundle (bundle.b05e.js)結構

4.1 總體結構

(function (modules) { 
  // webpack runtime 代碼
})({
  // 這個是模塊對象,下稱爲 modules, 注意提到 modules 就要想到這個對象!!!!
  // key 是模塊的路徑,注意,若是同一個模塊使用了不一樣 loader,webpack 會認爲這是兩個模塊,這個差異會體如今 key 上,key 包含了使用的 loader(若有)
  // value 就是被 webpack 包裝處理事後的模塊
  "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {}),
  "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {})
})
複製代碼

經過上面的代碼塊能夠看出來,這個結構就是一個自執行函數(IIFE),它定義形參 modules,接收實參爲一個對象,這個對象中 key 是模塊路徑,value 則是被 webpack 包裝後的模塊;github

看具體的代碼前,先要了解一個概念———— runtime;咱們來看下中文官方文檔的定義: runtime,.....主要是指:在瀏覽器運行過程當中,webpack 用來鏈接模塊化應用程序所需的全部代碼。它包含:在模塊交互時,鏈接模塊所需的加載和解析邏輯。包括:已經加載到瀏覽器中的鏈接模塊邏輯,以及還沒有加載模塊的延遲加載邏輯。web

簡言之,就是 webpack 用來處理鏈接、加載、執行 webpack 模塊的代碼;這些就是 bundle 中自執行函數的主要內容,這一段信息量有點大,咱們仍是由外入內的介紹一下這些變量、方法、以及方法上的屬性的大體做用;json

4.2 runtime 概覽

一、 webpackJsonpCallback 方法數組

(function(modules) { // webpack 啓動代碼
    // 這個 webpackJsonpCallback 是經過 JSONP 加載那些按需加載(import(some-file.js).then(...))的 chunk 時的 JSONP 的回調 callback;
    // JSONP 就是建立一個 script 標籤去加載 js 文件,而 JOSNOP callback 就是加載回來之後要作的事情
    function webpackJsonpCallback(data) {};
複製代碼

二、 installedModules

// 模塊緩存,已經安裝過的模塊們,若是已經加安裝過了就緩存在這個對象中,下次再訪問這個模塊走緩存就能夠了
// 下面的 __webpack_require__ 就是用來安裝模塊的
var installedModules = {};
複製代碼

三、 installedChunks

// 這個已經安裝過的 chunks,這個就有點複雜了;後面會細說加載異步 chunk 的過程;
// installedChunks 這個對象以 key-value 的形式保存已經安裝的 chunk,key 是 chunk id,關於 value 有如下幾種狀況:
// value = undefined,表示該 chunk 未被加載過
// value = 0,chunk 已經加載完畢
// value = <Array> [Promise resolveFn, Promise rejectFn, Promise] 表示 chunk 正在加載中,關於爲啥搞成這個數組結構後面的加載異步chunk 會細說
// value = null 表示 chunk preload 或者 prefetch 
    var installedChunks = {
            "bundle": 0
    };
複製代碼

四、 jsonpScriptSrc 方法

// 爲 script 標籤 src 屬性拼接 __webpack_require_.p,這個 p 屬性就是 webpack.config.js 中 output.publicPath 
	function jsonpScriptSrc(chunkId) {
		return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + "." + {"0":"a619de3d"}[chunkId] + ".js"
	}
複製代碼

五、 _webpack_require_ 方法

// webpack 運行時的主要方法,其做用建立並緩存 module 對象()後,執行這個 module 中的代碼;
// 建立 module 是啥意思嘞?就是 __webpack_require__ 中的 { i: moduleId, l: false, exports: {} } 對象
function __webpack_require__(moduleId) {}
複製代碼

六、 __webpack_require__.e 靜態屬性

// 用於加載額外 chunk 的函數,好比按需加載的 chunk,這個裏面就會有建立 script 標籤而後去加載代碼的具體邏輯,後面細說
__webpack_require__.e = function requireEnsure(chunkId) {};
複製代碼

七、 __webpack_require__.m 靜態屬性

// 暴露這個 runtime 接收到的 modules 對象(這個自執行函數接收到參數對象,看上面 4.1 )
__webpack_require__.m = modules;
複製代碼

八、 __webpack_require__.c 靜態屬性

// 暴露緩存的已經安裝的模塊們
__webpack_require__.c = installedModules;
複製代碼

九、 __webpack_require__.d 靜態方法

// 在模塊對象(module 上面__webpack_require__ 中建立的 module 對象,下同)的 exports 對象上增長屬性,
// 以 getter 的形式定義導出(就是實現你代碼中的經過 export 導出一個變量/常量/函數等)
__webpack_require__.d = function(exports, name, getter) {};
複製代碼

十、 __webpack_require__.r 靜態方法

// 在模塊對象(module) 增長 __esModule 屬性,用於標識這個模塊是個 ES6 模塊
__webpack_require__.r = function(exports) {};
複製代碼

十一、 __webpack_require__.t 靜態方法

// 這個 t 先忽略掉,暫時用不到
__webpack_require__.t = function(value, mode) {};
複製代碼

十二、 __webpack_require__.n 靜態方法

// 獲得 getDefaultExport 方法,抹平 ES6 的模塊和非 ES6 模塊的默認導出
__webpack_require__.n = function(module) {};
複製代碼

1三、 __webpack_require__.o 靜態方法

// 調用 hasOwnProperty 判斷對象是否有某一屬性
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
複製代碼

1四、 __webpack_require__.p 靜態屬性

// 這個就是 webpack.config.js 中 output.publicPath,上面 jsonpScriptSrc 方法就是拼接的這個值
__webpack_require__.p = "";
複製代碼

1五、 __webpack_require__.oe 靜態方法

// 錯誤處理,忽略
__webpack_require__.oe = function(err) { console.error(err); throw err; };
複製代碼

1六、 JSONP 初始化

// JSONP 初始化,這是個精彩的部分,後面講異步 chunk 加載的時候細說,可是先來看看這幾步驟都在幹啥
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; // 初始化 window[webpackJsonp] 對象
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 暫存數組 push 方法,這個 push 就是 Array.prototype.push
jsonpArray.push = webpackJsonpCallback; // 重寫 jsonpArray.push 方法(注意,這麼重寫不會改寫數組原型)
jsonpArray = jsonpArray.slice(); // 賦值 jsonpArrray,這個複製不帶 push 方法!
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); // 若chunk先於 bundle 這個入口加載,這個 jsonpArray 裏面就不是空的,此時,遍歷並調用 webpackJsonpCallback,至關於手動觸發 jsonp callback
var parentJsonpFunction = oldJsonpFunction; // 舊 push 暫存於 parentJsonpFunciton
複製代碼

1七、 __webpack_require__() 加載入口啓動應用

// 加載入口 module 並返回 webpack_require__ 執行後的 exports 對象
// 從這裏算是真正的開始跑咱們開發的模塊了
    return __webpack_require__(__webpack_require__.s = "./src/a.js");
})
/************************************************************************/
({
// 這就是 webpack runtime 的自執行函數接收到 modules 對象:提到 modules 請聯想到這個對象
    "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {}),
    "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {})
});
複製代碼

4.3 __webpack_require__ 方法

前面的刪除註釋、把函數體裏面的代碼刪除,都是在剔除支線劇情,讓咱們更多注意力放在我想表達的重點上,你能夠更好的跟着做者的寫做思路,若是你還在讀,你會發現我已經由外入內了,從整個 js 文件到 webpack runtime 概覽,從這個小的主題我就要進入到一個方法的代碼塊。

咱們來重複一下 _webpack_require_ 方法的做用:接收指定 moduleId 做爲參數,建立並緩存 module 對象,加載並執行 moduleId 代碼,是 webpack runtime 的重要部分;

在上面 webpack runtime 概覽的最後發現,在 runtime 的最後調用了 __webpack_require__(webpack_require.s = './src/a.js');接下來看看方法裏面發生了什麼:

function __webpack_require__ (moduleId) {

    // 查看緩存,若是緩存中有了即返回緩存便可:installedModules 在上面 4.2 runtime 概覽(2)
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }

    // 建立並緩存 module 對象,後面被賦值的這個對象稱爲 module 對象
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    }

    // 執行 module 中的代碼:
    // modules 就是 webpack runtime 這個自執行函數接收到的模塊對象,
    // moduleId 是什麼?從上面 runtime 最後調用 __webpack_require__ 的時候能夠發現接收的參數即 moduleId,即 ./src/a.js,這就是說 moduleId 能夠認爲是個路徑
    // call 則把模塊中的 this 修改爲 module.exports 並執行,執行時傳入參數:module, module.exports, __webpack_require__ (有沒有 node.js 中 CMD 的感受了)
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)

    // 標記 module 已經加載過,l 我猜猜是 loaded 的縮寫
    module.l = true

    // 把 module 的 exports 導出
    return module.exports
  }
複製代碼

接着咱們看看 modules[moduleId] 即 modules["./src/a.js"] 裏面是什麼:

  • modules
// 1. webpack runtime 接收到的 modules 就是這個樣子:
var modules = ({
    "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js");
  
      __webpack_require__.e(/*! import() */ 0)
        .then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))
        .then(function (m) {return m.minus(2, 1);});
      var A_NUM = 1;
      var r = Object(_b__WEBPACK_IMPORTED_MODULE_0__["add"])(3, 2 + A_NUM);
      console.log(r);
    }),
    "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {})
  });
複製代碼
  • 取 ./src/a.js
// 2. modules[moduleId] 
// 從上面能夠看出 modules[./src/a.js] 獲得的一個函數 

function (module, __webpack_exports__, __webpack__require__) {
 "use strict";
    __webpack_require__.r(__webpack_exports__); // 利用上面的 __webpack_require\__.r 方法將該模塊的 exports 對象上增長 __esModule 屬性,定性爲 ES6 module 對象
    /* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b */ "./src/b.js"); // 實現 import { add } from './b';
    
    // 實現 import('b.js').then(m => m.minus(2, 1);
    __webpack_require__.e(/*! import() */ 0) 
        .then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))
        .then(function (m) {return m.minus(2, 1);});
    var A_NUM = 1;
    var r = Object(_b__WEBPACK_IMPORTED_MODULE_0__["add"])(3, 2 + A_NUM);
    console.log(r);
}
複製代碼
  • 和 src/a.js 源碼對比一下
import { add } from './b';
import('./c.js').then(m => m.minus(2, 1));

const A_NUM = 1;
let r = add(3, 2 + A_NUM);

console.log(r);
複製代碼

咱們能夠輕鬆發現 webpack 都對代碼作了些什麼:

1)給模塊包了一層函數 function (module, __webpack_exports_, __webpack_require_) {};這麼作則是爲了實現模塊化,用閉包作隔離, webpack 本身實現了一個 CommonJS 規範;

2)利用上面的 __webpack_require__.r 方法將該模塊的 exports 對象上增長 __esModule 屬性,定性爲 ES6 module 對象;看看 .r 方法(上面 4.2 runtime 概覽(10)):

__webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
    }
    Object.defineProperty(exports, '__esModule', { value: true })
}
複製代碼

3)實現 import { add } from './b'; 導入 b 模塊上的 add 方法,咱們發現 import 關鍵字被 __webpack_require__ 實現了,咱們發現源碼中的 add() 調用被處理成了 _b__WEBPACK_IMPORTED_MODULE_0__['add'],這說明 add 這個方法被放到了 _b__WEBPACK_IMPORTED_MODULE_0__ 對象上,那麼這裏思考一下,b 模塊的導出怎麼去了這個對象上的呢?

4)實現 import('c.js').then(m => m.minus(2, 1); import(c.js) 變成了 __webpack_require\__.e(/*! import() */ 0),並且增長了一個 then(__webpack_require\__.bind(null, 'c.js'')) 這又是在搞什麼呢?這個後面揭曉;

5)調用獲得的 add 方法,忽略

6)log 輸出,忽略

  • 咱們看看 webpack 處理後的 b.js 的樣子:

b.js 一樣在 modules 中,以下:

// moduels
var modules = ({
    "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {}),
    "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SOME_VAR", function() { return SOME_VAR; });
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; });
      var SOME_VAR = 'SOME_VAR';
      function add(a, b) {
        return a + b;
      }
    })
})
複製代碼

對比一下 b.js 的源碼

export const SOME_VAR = 'SOME_VAR'

export function add(a, b) {
  return a + b
}
複製代碼

咱們能夠輕易看出 webpack 對 b.js 作了些什麼:

1)__webpack_require\__.r 標識該模塊是一個 ES6 的模塊;

2)實現 export const SOME_VAR = 'SOME_VAR' ;咱們發現 webpack 是經過 __webpack_require__.d 實現的 export 關鍵字,即導出,接着咱們看 .d 方法(4.2 runtime 概覽(9));d 方法就是經過在 module.exports 對象(_webpack_require_ 建立的)配置 getter 實現 export;這麼作的好處在哪裏?這種 export 一個變量(包括函數),好處是這個 getter 訪問的都是原來模塊做用域中的變量,自動鏈接到模塊的做用域,若此設計不可謂不機智;

__webpack_require__.d = function (exports, name, getter) {
    // __webpack_require__.o 是 hasOwnProperty 方法,判斷是否有私有屬性的
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter })
    }
}
複製代碼
  • 簡單回顧一下,webpack 怎麼讓代碼跑起來的?

1)webpack 有本身的runtime,其中的 _webpack_require_ 能夠建立、緩存 module,而後加載並運行 module。在 runtime 的末尾傳入入口模塊的路徑並執行 _webpack_require_ 方法;

2)webpack 的 runtime 接受到了一個 modules 對象,其中 key 是模塊路徑,value 則是被 webpack 處理過的函數,這個函數就是模塊主體;

3)webpack 經過 __webpack_require_.r 定義 ES6 模塊,通多 __webpack_require_.d 方法實現 export,經過 _webpack_require_ 實現 import 等措施實現了一個 CommonJS 規範的模塊系統;

5. 非入口 chunk 的加載

5.1 細說 webpack_require.e 方法

就說說 webpack 是如何加載非入口 chunk,非入口 chunk 的加載須要 __webpack_require__.e 方法。在前面的 demo 中,a.js 源碼中經過 import(c.js) 語法實現按需加載,被按需加載的模塊會以一個單獨的 chunk 生成一個文件 ———— 0.a619de3d.js;在看這個 chunk 以前,咱們先看看 a.js 又是如何實現的 import() 語法:

var modules = {
    "./src/a.js": (function(module, __webpack_exports__, __webpack_require__) {
        // ... 
        // 這裏就是實現的 import(c.js).then((m) => ....)
        __webpack_require__.e(/*! import() */ 0)
          .then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))
          .then(function (m) {
            return m.minus(2, 1);
          });
    }),
}
複製代碼

從上面的代碼能夠看出,__webpack_require_.e(/*! import() */ 0).then(__webpack_require_.bind(null, /*! ./c.js */ "./src/c.js")) 實現的 import(c.js);接下來就該看看 e 方法的真實面目:

__webpack_require__.e = function requireEnsure(chunkId) {
    // 1. 參數 chunkId: 用於加載的 chunk 的 chunkId,從上面調用該方法中能夠看出要的加載 c.js 的對應的 chunkId 是 0,這個 chunkId 是 webpack 生成的,這裏不須要關心;細心的朋友發現 chunk 文件除了 chunkId 0 後面還有 hash,這個 hash 哪裏來的呢?這個後面下面揭曉

    // 2. promise 隊列,能夠加載多個 chunk;
    var promises = [];
    
    // 3. 嘗試從已經安裝過的 chunk 中取用參數 chunkId,這個 installedChunks 的做用在上文 4.2 runtime 概覽(3)
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 這裏判斷非0,由於 0 表示已經安裝過了
        // 若是從 installedChunks 中取到的 installedChunkData 是個 promise 則說明正在加載這個 chunk;
        if(installedChunkData) {
           // 這裏有個精巧的設計,回到上面 4.2 runtime 概覽(3)中關於 installedChunk 的 value 的詳述中,
            // 只有一種狀況不是 falsy 的值,其餘的value如 0,undefined、null 都是 falsy,
            // 因此這裏他敢判斷 if(installedChunData) 而不用作細緻判斷
            // 就一個字,絕
            promises.push(installedChunkData[2]);
        } else {
            // else 說明這裏就說明都是些 falsy 的值,說明該去乖乖的加載這個 chunkId 對應的 chunk 文件

            // 這裏就要回達 4.2 runtime 概覽(3)中爲啥 installedChunks 的 value 有一種狀況是一個數組:
            // [Promise resolveFn, Promise, rejectFn, promise] 
            // 就是從 這個 __webpack_require__.e 方法的這裏建立的,就在下面的這幾行代碼
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject]; //就是這裏給數組中加上 resolve, reject~~~
            }); 
            promises.push(installedChunkData[2] = promise); // 這裏給數組加上 promise 對象
    
            // 所謂 JSONP 就是新建個 script 標籤去加載這個 chunkId 對應的文件(你是否是想說:就這?。。。jq 直呼內行)
            var script = document.createElement('script');
            var onScriptComplete;
    
            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }

            // 給 script src 屬性賦值,瀏覽器就會去發起一個 get 請求去加載 src 對應的資源;
            // 還記得這個方法開頭咱們說只有 chunkId 0,還有 hash 來着,hash 從哪兒來?這裏就是在 jsonpScriptSrc 方法裏面拼接的;
            script.src = jsonpScriptSrc(chunkId);
    
            // create error before stack unwound to get useful stacktrace later
            var error = new Error();
            onScriptComplete = function (event) {
                // avoid mem leaks in IE.
                script.onerror = script.onload = null;
                clearTimeout(timeout);
                var chunk = installedChunks[chunkId];
                if(chunk !== 0) {
                    if(chunk) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            // 人肉處理超時, 120s 後手動超時
            var timeout = setTimeout(function(){
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete;

            // 把 script 標籤插入到 dom 中 
            document.head.appendChild(script);
        }
    }
 
    // 調用 Promise.all() 等全部被加載的 chunk 都完成了纔會 resolve
    // 最後返回的是一個 promise
    return Promise.all(promises);
};
複製代碼

總結一下上面這段異步按需加載模塊的操做: 1)首先源碼中 import(c.js).then 被 webpack 處理成了 __webpack_require__.e(chunkId).then(__webpack_require__.bind(null, c.js)) 2)__webpack_require__.e 則會經過 JSONP 加載對應的 chunk 並執行,返回加載 chunk 時建立的 promise 對象; 3)上面 1)中的 then 的回調 __webpack_require\__.bind(null, c.js) 接着執行,這個回調的做用其實就是調用 __webpack_require\__ 去加載 c.js; 4)接着上面的從 chunk 加載按需加載的 c.js,緊接着再次調用 then 方法,這個 then 方法中的回調纔會真正調用 c.js 中的方法,到這裏被按需加載的 c.js 中的方法完成調用;

你覺得到這裏就結束了是麼,怎(我)麼(也)可(想)能(阿)?

咱們來思考一個問題:__webpack_require__ 是從 modules 對象或者 installedModules 緩存中獲取模塊的,那麼這個 chunk 代碼作了什麼,runtime 又作了什麼,才使得 then(__webpack_require__.bind(null, c.js)) 取到預期的方法呢?

這個問題的答案要着眼於兩方面:

  1. chunk 裏面的代碼;
  2. runtime 中 JSONP 處理;

5.2 chunk 概覽及 JSONP

一、chunk 中的代碼

// 1. 確保 window[webpackJsonp] 這個數組的存在,若是沒有就賦值一個,有的話就取用
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
  "./src/c.js": (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "minus", function() { return minus; });
      Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./b.js */ "./src/b.js")).then(function (m) {
        return m.add(200, 100);
      });
      function minus(a, b) {
        return a - b;
      }
  })
}]);
// 2. 接着就是向 window[webpackJsonp] 這個數組中 push 了一項,這裏須要重點關注一下這個數組的 push 方法和被 push 的數據結構:
// 2.1 這個 push 方法不必定是數組原生的 push 方法(Array.prototype.push),這一點在後面的 runtime 處詳述;
// 2.2 這個數組項是個數組,數組第一項又是個數組 [0], 第二項是個對象,對象的結構和 modules 相同,key 是資源路徑,value 是個webpack 包裝函數:
// 因此 window[webpackJsonp] 的大體結構:[[[0], { ./src/c.js: (function(module, __....) {}) }]]
複製代碼

二、runtime 中的 JSONP 處理

在上面 4.2 runtime 概覽(16) 這個小標題,說到了 JSONP 初始化,看看這段代碼:

(function (modules) {
    // ... webpack runtime 代碼
    var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; // 初始化 window[webpackJsonp],若是沒有就初始化
    var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 暫存數組 push 方法,這個 push 就是 Array.prototype.push
    jsonpArray.push = webpackJsonpCallback; // 重寫 jsonpArray.push 方法(注意,這麼重寫不會改寫數組原型)
    jsonpArray = jsonpArray.slice(); // 賦值 jsonpArrray,這個複製不帶 push 方法!
    for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); // 本例子中,執行到這裏時,jsonpArray 中是空的,因此這個 for 循環暫時不執行;若chunk先於 bundle 這個入口加載,這個 jsonpArray 裏面就不是空的,此時,遍歷並調用 webpackJsonpCallback,至關於手動觸發 jsonp callback;
    var parentJsonpFunction = oldJsonpFunction; // 舊 push 暫存於 parentJsonpFunciton
    // ... other runtime processing
})
複製代碼

這裏有必要強調一點,在本文對應的 demo 中,runtime 所在的入口 bundle 的執行順序是先於後面按需加載的 chunk 的。因此執行過這段代碼後,window[webpackJsonp] 已經初始化,同時 jsonpArraypush 方法被改寫成了 webpackJsonpCallback 方法;

因此,等到後面 chunk 被加載回來後,執行 chunk 代碼:(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{.. 時,執行的這個 push 實際上是 webpackJsonpCallback 方法。

你必定在想,爲何會有這個操做?對於我這個搬磚的人來講,只能說:絕!

其實他改寫 push 是配合着下面的 for 循環用的,目的在於無論是入口 bundle 先下載仍是 chunk 先加載,都能讓代碼獲得正確的執行。先加載 bundle,就是我們上面一直說的狀況,這個很少說,chunk 的 push 方法就是 webpackJsonpCallback 方法,調用 push 就是調用 webpackJsonpCallback。

若是先加載 chunk(例如某些預加載之類的技術下),此時 chunk 中的代碼先執行了,這個時候 chunk 會初始化 window[webpackJsonp] 這個數組,而 push 方法就是原生的數組方法 push,很樸素的把數組項添加到 window[webpackJsonp] 數組中,chunk 的使命暫時完成了;接下就等 bundle 加載並執行 runtime 了,等到執行到上面的 runtime 的時候,window[webpackJsonp] 數組已經被初始化,因此後面對 jsonpArray 的 for 循環就能夠工做了,這個時候,對 window[webpackJsonp] 中的每一項調用一次 webpackJsonpCallback

這就保證了,無論 chunk 什麼時候加載,都能讓代碼以預期的方式工做。絕,連城訣!接下來看看這個 webpackJsonpCallback 作了些什麼吧:

function webpackJsonpCallback(data) {
    // 1. 這個 data 就是上面說 chunk 代碼的時 push 的數組項,結構相似:[[[0], { ./src/c.js: (function(module, __....) {}) }]]
    var chunkIds = data[0]; // chunkIds 就是上面的 [0] 
    var moreModules = data[1]; // moreModules 就是上面的 { ./src/c.js: (fun....) } 這個對象

    // 2. 遍歷 chunkIds,若是在 installedChunks 是個非 falsy 的值,說明這個 chunkId 對應的 chunk 正在加載了,
    // 把它的 resolve push 到 resolves 這個數組(注意:installedChunks 中若 chunkId 所對應 chunk 正在加載,其value [Promise resolveFn, Promise rejectFn, promsie ])
    // 把 installedChunks[chunkId] 的值修改成 0,標識該 chunk 已經被加載過!
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }

    // 這裏回答:runtime 作了啥,才能讓 chunk 代碼裏面的 module 能夠被 runtime 中的 __webpack_require__ 獲取到 的問題:
    // 是由於在這裏會把 chunk 中所攜帶的 module 整合到 webpack runtime 接收到 modules 對象中,在下一個事件循環中 __webpack_require__ 從 modules 中取天然就能夠取到了
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    
    // 3. 這一步就是用來保證 bundle 先執行的時候,因爲 runtime 執行,
    // 數組 push 被改寫成 webpackJsonpCallback 後,chunk 加載後,可以正常的把 chunk 相關信息添加到 window[webpackJsonp] 這個數組中
    if(parentJsonpFunction) parentJsonpFunction(data);

    // 4. resolves 中存放的都是加載 chunk時 __webpack_require__.e 方法建立的特殊數組項:
    // (形如[Promise resolve, Promise reject, promise])的第 0 項,即 resolve,
    // 它決定了 __webpack_require__.e 方法最後 return 出去的 Promise.all(promise) 的 promise 對象是否 resolve,
    // 進而決定了編譯後的 a.js 模塊中: __wepack_require__.e(0).then(__webpack_require__.bind(c.js)).then(...) 的執行
    // 若是 resolves 不爲空,則清空這個隊列,使得 __webpack_require__.e 方法最後 return 出去的 Promise.all(promise) promise 得以 resolve 
    while(resolves.length) {
        resolves.shift()();
    }
};
複製代碼

6. splitChunks 以後的輸出文件

6.1 修改項目代碼

  • 修改 webpack.config.js 追加 splitChunk 配置(optimization.splitChunks):
var path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    m1: './src/m1.js',
    m2: './src/m2.js'
  },
  devtool: 'none',
  output: {
    path: __dirname + '/dist',
    filename: '[name].[chunkhash:4].js',
    chunkFilename: '[name].[chunkhash:8].js'
  },
  mode: 'development',
  plugins: [new HtmlWebpackPlugin()],
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: ['@babel/env']
            }
          }
        ]
      }
    ]
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 0,
      maxSize: 0,
      minChunks: 1,
      name: true,
      automaticNameDelimiter: '~',
      cacheGroups: {
        default: {
          chunks: 'all',
          minChunks: 2,
          priority: -10
        }
      }
    },
    // runtimeChunk: { name: 'runtime' }
  }
};

複製代碼
  • 在 src 下追加兩個文件m1.js 、m2.js,做爲多入口:

一、 m1.js

import { times } from './d';
console.log(times(10, 4));

複製代碼

二、 m2.js

import { times } from './d';
console.log(times(2, 12));
複製代碼

6.2 打包輸出文件

這兩個入口都依賴了 d.js 這個模塊,這個模塊將會被拆成一個單獨的 chunk —— dd~m1~m2.a7ba197d.js;下列就是修改配置後的文件輸出:

  1. index.html
  2. m1.33c1.js
  3. m2.50e9.js
  4. ddm1m2.a7ba197d.js

咱們使用的 html-webpack-plugin,它會幫我你把 js 按順序插入到 html 文件裏;這裏須要先看下 index.html 代碼,關注一下這些 js 被引入的順序;

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>Webpack App</title>
</head>
<body>
    <script type="text/javascript" src="dd~m1~m2.a7ba197d.js"></script>
    <script type="text/javascript" src="m1.33c1.js"></script>
    <script type="text/javascript" src="m2.50e9.js"></script>
</body>
</html>
複製代碼

很明顯的看出這個公共的 chunk 被最早引入,代碼執行的時候也是先去加載並執行它;這和咱們前面的例子略有區別,主要體如今:

  1. chunk 被優先引入,而入口 bundle 被後置;

  2. 在新生成的 bundle m1.xxx.js 和 m2.xxx.js 的 runtime 的最後再也不返回 __webpack_require__(入口),而是返回了 checkDeferredModules([m1.33c1.js, dd~m1....js])

  3. runtime 新增 checkDeferredModules 方法,同時 webpackJsonpCallback 中也做出了相應的調整,在末尾增長了兩行代碼:

deferredModules.push.apply(deferredModules, executeModules || []);
return checkDeferredModules();
複製代碼

6.3 執行過程分析

一、首先加載並執行 dd~m1~m2.a7ba197d.js 文件,代碼以下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["dd~m1~m2"],{
  "./src/d.js": (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
      __webpack_require__.r(__webpack_exports__);
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "L", function() { return L; });
      /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "times", function() { return times; });
      var L = 'Aragaki Yui';
      function times(a, b) {
        return a * b;
      }
  })
}]);
複製代碼

分析發現,這個 chunk 和前面例子中按需加載的 chunk 沒有特別大的差異,仍然是經過向 window[webpackJsonp] 中添加數組項;值得強調的是,這裏由於 chunk 先於入口加載並執行,因此這個 window[webpackJsonp] 是在 chunk 中初始化,同時這個 push 方法也是數組原生的 push 方法;

二、加載入口文件 m1.33c1.js ,代碼以下:

咱們簡化了這個入口文件,把註釋和與前面未進行 splitChunks 相同的方法和變量刪除掉,咱們聚焦於不一樣點:

  • 變量 deferredModules
  • webpackJsonpCallback 方法
  • checkDeferredModules 方法
(function(modules) { // webpackBootstrap
	// install a JSONP callback for chunk loading
	function webpackJsonpCallback(data) {
		// ... 同前 webpackJsonpCallback 邏輯,略去
        var executeModules = data[2];

		// 這一行代碼目前還沒看出啥做用,看起來是這個 webpackJsonpCallback 被調用時,好比 window[webpackJsonp].push 或者 
        // webpackJsonpCallback(jsonpArray[i]) 的時候能夠傳入一個 defferedModule 項;
        // webpack 的原文註釋字面意思是"從已經加載過 chunk 中把 entry modules 加入到 deferredModules"
		deferredModules.push.apply(deferredModules, executeModules || []);

		// webpackJsonpCallback 是加載 chunk 後執行的,意在每次加載 chunk 後都 check 一下 defrredModules 中的依賴們是否加載完了,加載完了就調用入口 module 執行;
        // 這樣有個好處就是無論加載入口仍是先加載依賴,最後的代碼執行順序都能得正確執行;
		return checkDeferredModules();
	};
	function checkDeferredModules() {
	    // 方法做用:
        // deferredModules 中的每一項都是一個數組形如:[入口 chunk1, 依賴chunk2, 依賴 chunk3...]
        // checkDeferredModules 的做用就是檢查 defferredModules 中的每一項,的依賴部分(從第二項開始)是否都已經安裝完成(installedChunks[chunkIed] === 0)
        // 若是都安裝完成了,則調用數組項的入口 chunk


        // 變量 result,用於接收入口 chunk 執行後的結果,最後返回它
		var result;

        // 遍歷 deferredModules,這個 deferredModules 能夠有不少項,因此須要遍歷一下
		for(var i = 0; i < deferredModules.length; i++) {
			var deferredModule = deferredModules[i]; // defferredModule 形如: [入口 chunk1, 依賴chunk2, 依賴 chunk3...]
			var fulfilled = true; // 標識符,標識全部的依賴 chunk 都已經被安裝
			for(var j = 1; j < deferredModule.length; j++) {
				var depId = deferredModule[j];
				if(installedChunks[depId] !== 0) fulfilled = false; // installedChunks 中對應這一項的值不爲 0 說明沒有安裝完,安裝完這個值就是 0 了,關於 installedChunks 在上文 4.2 runtime 概覽(三、installedChunks)
			}
			// 經歷了上面的 for 循環,若是 fulfilled 仍是 true 說明全部的依賴 chunk 都已經加載完成了,就能夠執行入口 chunk 了
			if(fulfilled) {
                // 此時從 deferredModules(注意有 s,不是 deferredMdoule) 中刪除掉這一項,由於這一項已經 check 過了,
                // 下面一行代碼馬上就會執行這一項的入口,因此 checkDeferredModules 對它的工做已經完成了;
                // 若是不刪會怎樣?後面只要有新的入口,好比 m2.js,添加到 defferedModules 數組就會觸發從新 check,若是不刪除就會讓已經執行過 chunk 再執行一遍,至關於同一段邏輯執行兩次;
				deferredModules.splice(i--, 1);
                // __webpack_require__ 調用入口模塊,並將其返回結果賦值給 result 
				result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
			}
		}
        // 開開心心拿着入口 module 返回結果返回
		return result;
	}

    // 這個變量時由於 slitChunks 多出來的,這個變量是一個數組,其第一項表示要加載的 module,從第二項開始後面的都是第一項依賴的模塊;
    // 從這個例子上來講,第一項是入口 chunk(例如本例中的 m1.xxx.js),後面的是入口依賴的 chunk(如 dd~m1~m2.....js) 
    // 如今這個數組是空的,何時裏面纔會值呢?在 runtime 結尾處,會把入口 ./src/m1.js 和 依賴的 dd~m1~m2 push 進去;
    var deferredModules = [];

	function __webpack_require__(moduleId) { /* 同前 __webpack_require__ 略去 */ }
    
	// JSONP 初始化.....
	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
	jsonpArray.push = webpackJsonpCallback;
	jsonpArray = jsonpArray.slice();
	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
	var parentJsonpFunction = oldJsonpFunction;

	// 這裏!把入口 ./src/m1.js 和 依賴的 chunk pushu 到 deferredModules 中;
    // 緊接着調用 checkDeferredModules 方法,接着去這個方法裏面看看這個方法都發生了啥
	deferredModules.push(["./src/m1.js","dd~m1~m2"]);
	return checkDeferredModules();
})
/************************************************************************/
({
  // 這個對象也是 modules 
  "./src/m1.js": (function(module, __webpack_exports__, __webpack_require__) {
 "use strict";
    __webpack_require__.r(__webpack_exports__);
    /* harmony import */ var _d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./d */ "./src/d.js");
    
    console.log(Object(_d__WEBPACK_IMPORTED_MODULE_0__["times"])(10, 4));
  })

});
複製代碼

另外一個入口 m2.js 同理,在此再也不贅述。

  • TIPS: 關於先加載 chunk 仍是先加載入口,能夠去 index.html 中調整一下 chunk 和 入口的 script 標籤順序,就能夠發先控制檯的輸出仍然是正常的;

7. 總結

本篇詳述了 webpack 打包輸出文件,又賞析了他設計的精妙:

1)巧妙實現 CMD 規範 __webpack_require____webpack_require__.d,shim 瀏覽器的模塊系統;

2)經過 runtime 的 改寫 push 方法 並結合 for 循環調用 webpackJsonpCallback 實現不管入口和chunk的順序如何,最後代碼都能順利執行

3)webpack 實現細節的精妙,好比 installedChunked 的數據結構的設計,只有一種正在加載的是數組,其他都是 falsy 值;

4)本篇篇幅有點長,建議閱讀 2-3 遍;

5)碼字不容易,看完了,點個讚唄

相關文章
相關標籤/搜索