webpack bootstrap原始碼解讀

YDSS發表於2018-06-22

雖然一直在用webpack,但很少去看它編譯出來的js程式碼,大概是因為除錯的時候有sourcemap,可以直接除錯原始碼。一時心血來潮想研究一下,看了一些關於webpack編譯方面的文章都有提到,再結合自己看原始碼的體會,記錄一下自己的理解

說bootstap可能還有點不好理解,看一下webpack編譯出來的js檔案就很好理解了:

// 編譯前的入口檔案index.js的內容
let a = 1;
console.log(a);

// webpack編譯後的檔案內容
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {


let a = 1;
console.log(a);

/***/ })
],[0]);
複製程式碼

編譯後的檔案跟我們的原始檔不太一樣了,原本的內容被放到了一個function(module, exports){}函式裡,而最外層多了一個webpackJsonp的執行程式碼。那麼問題來了:

  1. webpackJsonp是在哪裡定義的,它是幹什麼用的?
  2. 包裹原來程式碼的function(module, exports){}又是幹什麼用的?

這就是bootstrap的作用了。如果不用code split把bootstrap單獨分離出來,它就在編譯出的js檔案最上面,因為需要先執行bootstrap後續的程式碼才能執行。我們可以用CommonChunkPlugin把它單獨提出來,方便我們閱讀。把下面的程式碼寫到你的webpack的plugin配置裡即可:

new webpack.optimize.CommonsChunkPlugin({
    name: "manifest" // 可以叫manifest,也可以用runtime
}),
複製程式碼

配置之後,編譯出來的檔案會多出一個manifest.js檔案,這就是webpack bootstrap的程式碼了。bootstrap和使用者程式碼(就是我們自己寫的部分)編譯後的檔案其實是一個整體,所以後面的分析會引入使用者程式碼一起看

manifest.js

manifest原始碼分為3個部分:

  1. 建立了一個閉包,初始化需要用到的變數
  2. 定義webpackJsonp方法,掛載到window變數下
  3. 定義與編譯相關的輔助函式和變數,如__webpack_require__(也就是我們在自己的程式碼裡用到的require語法)

我們一個一個來看。下面的每個部分,我們都只擷取manifest原始碼的相關部分來看,完整的原始碼放在文章最後了

初始化部分

/******/ (function(modules) { // webpackBootstrap
            // ......

            // The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// objects to store loaded and loading chunks
/******/ 	var installedChunks = {
/******/ 		1: 0
/******/ 	};

            // ......
/******/ })
/************************************************************************/
/******/ ([]);
複製程式碼

我們擷取了manifest最外層的程式碼和初始化部分的程式碼,可以看到整個檔案都被一個閉包括在裡面,而modules的初始值是一個空的Array([])。 這樣做可以隔離作用域,保護內部的變數不被汙染

  • modules 空的Array([]),用來存放每個module的內容
  • installedModules存放module的cache,一個module被執行後(module的執行會在webpackJsonp的原始碼部分提到)的結果被儲存到這裡,之後再用到這個模組就可以直接使用快取而無需再次執行了
  • installedChunks 用來存放chunk的執行情況。若一個chunk已經載入了,在installedChunks裡這個chunk的值會變成0,也就是無需再載入了

如果分不清module和chunk這兩個概念的區別,文章最後一節專門對此作了解釋

webpackJsonp

原始碼分析

在講webpackJsonp的原始碼之前,先回憶一下我們自己的chunk程式碼

// 編譯前的入口檔案index.js的內容
let a = 1;
console.log(a);

// webpack編譯後的檔案內容
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {


let a = 1;
console.log(a);

/***/ })
],[0]);
複製程式碼

執行webpackJsonp,傳了3個引數:

  • chunkIds chunk的id,這裡用了array,但一般一個檔案就是一個chunk

  • moreModules chunk裡所有模組的內容。模組內容可能不是很直觀,再看上面編譯後的程式碼,我們的程式碼被包在function(module, exports) {}裡,其實是變成了一個函式,這就是一個模組內容。這其實是CommonJs規範中一個模組的定義,只是我們在寫模組的時候不用自己寫這個頭尾,工具會幫我們生成。還記得AMD規範嗎?

    moreModules還隱藏了對每個module的id的定義。從編譯後的檔案裡可以看到/* 0 */這樣的註釋,結合程式碼來看,其實module的id就是它在moreModules裡的陣列下標。那麼問題來了,只有一個entry chunk還好說,如果有多個chunk,每個chunk裡的moreModules的Id不會衝突嗎?這裡有個小技巧,如下是一個非同步chunk的部分程式碼:

    webpackJsonp([0],[
    /* 0 */,
    /* 1 */,
    /* 2 */,
    /* 3 */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    // ......
    複製程式碼

    看到了嗎,moreModules的前3個元素是空的,也就是說0-2這三個id已經被別的chunk使用了

  • executeModules 需要執行的module,也是一個array。並不是每一個chunk都有executeModules,事實上只有entry chunk才有,因為entry.js是需要執行的

ok,有了使用webpackJsonp部分的印象,再來看webpackJsonp程式碼會清晰很多

/******/ 	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/ 		// add "moreModules" to the modules object,
/******/ 		// then flag all "chunkIds" as loaded and fire callback
/******/ 		var moduleId, chunkId, i = 0, resolves = [], result;
                // 
/******/ 		for(;i < chunkIds.length; i++) {       // part 1
/******/ 			chunkId = chunkIds[i];
/******/ 			if(installedChunks[chunkId]) {
/******/ 				resolves.push(installedChunks[chunkId][0]);
/******/ 			}
/******/ 			installedChunks[chunkId] = 0;
/******/ 		}
                // 取出每個module的內容
/******/ 		for(moduleId in moreModules) {         // part 2
/******/ 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ 				modules[moduleId] = moreModules[moduleId];
/******/ 			}
/******/ 		}
                // 
/******/ 		if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/ 		while(resolves.length) {                // part 3
/******/ 			resolves.shift()();
/******/ 		}
                // 執行executeModules
/******/ 		if(executeModules) {                    // part 4
/******/ 			for(i=0; i < executeModules.length; i++) {
/******/ 				result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ 			}
/******/ 		}
/******/ 		return result;
/******/ 	};
複製程式碼

首先,webpackJsonp是掛在window全域性變數上的,看看每個chunk的開頭就知道為什麼。我把它分為4塊:

  • part 1 這部分涉及到installedChunks,我們之前瞭解過,如果沒有非同步載入的chunk,這部分是用不到的,我們留到非同步chunk再說

  • part 2 取出這個chunk裡所有module的內容,放到modules裡,這裡並不執行每個module,而是真正用到這個module時再從modules裡取出來執行

  • part 3 與part 1一樣是對installedChunks的操作,放到後面再說

  • part 4 執行executeModules,一般只有入口檔案對應的module是需要執行的。執行module呼叫了__webpack_require__方法。

    還記得我們在程式碼裡怎麼引入別的js嗎? 對,require方法。其實我們的程式碼編譯後會被轉成__webpack_require__,只不過要把引用的路徑換成moduleId,這一步也是webpack處理的。所以__webpack_require__的作用就是執行一個module,把它的exports返回。先來看看它的實現:

                // The require function
    /******/ 	function __webpack_require__(moduleId) {
    /******/
    /******/ 		// Check if module is in cache
    /******/ 		if(installedModules[moduleId]) {   // line 1
    /******/ 			return installedModules[moduleId].exports;
    /******/ 		}
    /******/ 		// Create a new module (and put it into the cache)
    /******/ 		var module = installedModules[moduleId] = {  // line 2
    /******/ 			i: moduleId,
    /******/ 			l: false,
    /******/ 			exports: {}
    /******/ 		};
    /******/
    /******/ 		// Execute the module function
    /******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // line 3
    /******/
    /******/ 		// Flag the module as loaded
    /******/ 		module.l = true;
    /******/
    /******/ 		// Return the exports of the module
    /******/ 		return module.exports;  // line 4
    /******/ 	}
    複製程式碼

    line 1檢查這個module是不是已經執行過,是的話一定在快取installedModules裡,直接把快取裡的exports返回。如果沒有執行過,那就新建一個module,也就是line 2。這裡module有2個額外的屬性,i記錄moduleId,l記錄module是否已經執行。

    line 3執行這個module。我們前面說過,我們的程式碼都被包在一個函式裡了,這個函式提供3個引數:module, exports, require。仔細看這行,是不是這三個引數都被傳進去了。

    line 4返回exports。值得一提的是,line 3的執行結果是傳給了line 2我們新建的module變數,也就是把exports賦值給module了,所以我們直接返回了module.exports

使用場景

webpackJsonp的使用場景跟chunk相關,有非同步chunk的情況會複雜一些

沒有非同步載入chunk的情況

沒有非同步載入chunk的情況是很簡單的,它的執行過程可以簡單歸納為:依次執行每個chunk檔案,也就是執行webpackJsonp,從moreModules裡取出每個module的內容,放到modules裡,然後執行入口檔案對應的module。因為每次執行module,都會快取這個module的執行結果,所以即使你沒有抽取出每個chunk裡的相同module(CommonChunkPlugin),也不會重複執行重複的module

有非同步載入chunk的情況

當我們使用require.ensure或者import()語法時就會產生一個非同步chunk,官方文件傳送門。非同步chunk的js檔案不需要手動寫到html裡,在執行到它時會通過動態載入script的方式引入,非同步載入的函式就是__webpack_require__.e

            // This file contains only the entry chunk.
/******/ 	// The chunk loading function for additional chunks
/******/ 	__webpack_require__.e = function requireEnsure(chunkId) {
/******/ 		var installedChunkData = installedChunks[chunkId];
/******/ 		if(installedChunkData === 0) {   // part 1
/******/ 			return new Promise(function(resolve) { resolve(); });
/******/ 		}
/******/
/******/ 		// a Promise means "currently loading".
/******/ 		if(installedChunkData) {    // part 2
/******/ 			return installedChunkData[2];
/******/ 		}
/******/
/******/ 		// setup Promise in chunk cache
/******/ 		var promise = new Promise(function(resolve, reject) { // part 3
/******/ 			installedChunkData = installedChunks[chunkId] = [resolve, reject]
/******/ 		});
/******/ 		installedChunkData[2] = promise;
/******/
/******/ 		// start chunk loading
/******/ 		var head = document.getElementsByTagName('head')[0];  // part 4
/******/ 		var script = document.createElement('script');
/******/ 		script.type = "text/javascript";
/******/ 		script.charset = 'utf-8';
/******/ 		script.async = true;
/******/ 		script.timeout = 120000;
/******/
/******/ 		if (__webpack_require__.nc) {
/******/ 			script.setAttribute("nonce", __webpack_require__.nc);
/******/ 		}
/******/ 		script.src = __webpack_require__.p + "" + ({"0":"modC","1":"modA"}[chunkId]||chunkId) + ".js"; // line 1
/******/ 		var timeout = setTimeout(onScriptComplete, 120000);
/******/ 		script.onerror = script.onload = onScriptComplete;
/******/ 		function onScriptComplete() { // line 2
/******/ 			// avoid mem leaks in IE.
/******/ 			script.onerror = script.onload = null;
/******/ 			clearTimeout(timeout);
/******/ 			var chunk = installedChunks[chunkId];
/******/ 			if(chunk !== 0) {
/******/ 				if(chunk) {
/******/ 					chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
/******/ 				}
/******/ 				installedChunks[chunkId] = undefined;
/******/ 			}
/******/ 		};
/******/ 		head.appendChild(script);
/******/
/******/ 		return promise;
/******/ 	};
複製程式碼

程式碼有點多~但其實大部分(part 4)都是非同步載入script。我們從頭開始看

  • part 1判斷chunk是否已經載入過了,是的話直接返回一個空的Promise。為什麼在installedChunks裡的記錄為0就表示已經載入過了?這要回到我們之前在講webpackJsonp跳過的部分,單獨截下來看:

    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]); // line 1
        }
        installedChunks[chunkId] = 0; // line 2
    }
    複製程式碼

    載入當前chunk時在installedChunks裡記錄這個chunk已經載入了,也就是置0了(line 1)

  • part 2part 3是一體的,它的作用是在chunk還沒載入好時就被使用了,這時先返回一個promise,等chunk載入好了,這個promise會resolve,通知呼叫者可以使用這個chunk了。因為chunk的js檔案需要通過網路,不能保證什麼時候載入好,才會用到promise。我們先看看是怎麼實現的:

    其實應該倒過來先看part 3再看part 2part 3定義了一個promise,然後把這個promise的resolve放到installedChunks裡了。這一步很關鍵,因為chunk載入時需要執行這個resolve告訴這個chunk的使用者已經可以使用了。part 3執行完成後,installedChunks裡這個chunk對應的記錄應該是一個Array且有3個元素:這個promise的resolve,reject和promise本身。另外需要注意一點,new Promise(function(){})語句的function是立即執行的。

    再來看part 2,如果installedChunks裡有這條記錄,且它又沒有載入完成,那麼就把part 3定義的promise返回給呼叫者。這樣的作用是,當chunk載入完成了,只需要執行這個promise的resolve就能通知呼叫者繼續往下執行

    順帶提一下這個promise的resolve是何時執行的。看part 1 webpackJsonp的程式碼line 1這行,installedChunks[chunkId][0]是不是很眼熟,對,這就是chunk在為載入完成時建立的promise的resolve方法,而後會把所有的使用到這個chunk的resolve方法都執行(如下),因為執行到webpackJsonp就說明這個chunk已經載入完成了

    while(resolves.length) {
        resolves.shift()();
    }
    複製程式碼
  • part 4是動態載入script的程式碼,沒什麼可說的,值得一提的是line 1在拼接script的src時出現的{"0":"modC","1":"modA"},這個是我自己的兩個非同步chunk的id,是webpack分析依賴後插入進來的,如果你有多個非同步chunk,這裡會隨之變化。

    line 2是非同步chunk載入超時和報錯時的處理

ok,有了__webpack_require__.e的理解,我們再來看載入非同步chunk的情況就很輕鬆了。先來看一段示例:

// 編譯前
import(/* webpackChunkName: "modA" */ './mods/a').then(a => {
    let ret = a();
    console.log('ret', ret);
})

// 編譯後
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 0)).then(a => {
    let ret = a();
    console.log('ret', ret);
})
複製程式碼

我們用import()的方式做code spliting,換成require.ensure也類似,區別在import()的返回值是promise形式的,require.ensure是callback形式。對比編譯前後,import被替換成了__webpack_require__.e,在原始碼的.then中間加了一行.then(__webpack_require__.bind(null, 0))

首先,__webpack_require__.e保證chunk非同步載入完成,但是並不返回chunk的執行結果(見上文__webpack_require__.e的原始碼分析),所以加了一個.thenrequire這個chunk裡的module。再然後,就是我們取這個module的程式碼了

注:/* webpackChunkName: "modA" */這個是給chunk起名字的,webpack會讀這段註釋,取modA作為這個chunk的name,在output.chunkFilename可以用[name].js來命名這個chunk,不然webpack會用數字id作為chunk的檔名

其他輔助函式

webpack_require.p

等於output.publicPath的值(publicPath傳送門)。webpack在編譯時會把原始碼中的本地路徑替換成publicPath的值,但是非同步chunk是動態載入的,它的src需要加上publicPath。看個小栗子就明白了:

// webpack.config.js
module.exports = {
    entry: path.resolve("test", "src", "index.js"),
    output: {
        path: path.resolve("test", "dist"),
        filename: "[name].js",
        publicPath: 'http://game.qq.com/images/test', // 這裡定義了publicPath
        chunkFilename: "[name].js"
    },
    // ......
}
複製程式碼

這是配置檔案,我們定義了publicPath

// manifest.js
    // ...

/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "http://game.qq.com/images/test"; // 賦值publicPath的值

    //...

// 
複製程式碼

webpack把publicPath帶進manifest.js

// 還是manifest.js
    // ...

/******/ 		script.src = __webpack_require__.p + "" + ({"0":"modA","1":"modC"}[chunkId]||chunkId) + ".js";

    // ...
複製程式碼

還記得這行程式碼嗎,這是動態載入非同步chunk時拼src的部分。這裡就把__webpack_require__.p拼在非同步chunk的url上了

webpack_require.e

上面已經詳細分析了~

webpack_require.d 和 webpack_require.n

webpack從2.0開始原生支援es6 modules,也就是importexport語法,不需要藉助babel編譯。這會出現一個問題,es6 modules語法的import引入了default的概念,在Commonjs模組裡是沒有的,那麼如果在一個Commonjs模組裡引用es6 modules就會出問題,反之亦然。webpack對這種情況做了相容處理,就是用__webpack_require__.d__webpack_require__.n來實現的,限於篇幅,就不在這裡細講了,大家可以閱讀webpack模組化原理-ES module這篇文章,寫的比較詳細

webpack_require.nc

script屬性nonce的值,如果你有使用的話,會在每個非同步載入的script加上這個屬性

A cryptographic nonce (number used once) to whitelist inline scripts in a script-src Content-Security-Policy . The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.

一些alias

webpack在__webpack_require__上加了一些manifest.js裡的變數引用,應該是給webpack內部js或者plugin加進來的js使用的:

  1. webpack_require.m modules的引用
  2. webpack_require.c installedModules的引用

如果你嘗試在你的程式碼裡使用這些變數或者require本身(不是用require來引入模組),webpack會把它編譯成一個報錯函式

一些工具函式的簡寫

  1. webpack_require.o Object.prototype.hasOwnProperty.call的簡寫
  2. webpack_require.oe 非同步載入chunk報錯的函式

chunk與module的區別

可能很多同學搞不清楚chunk和module的區別,在這裡特別說明一下

module的概念很簡單,未編譯的程式碼裡每個js檔案都是一個module,比如:

// entry.js
import a from './a.js';

console.log(a); // 1

// a.js
module.exports = 1;
複製程式碼

這裡entry.js和a.js都是module

那什麼是chunk呢。先說簡單的,如果你的程式碼既沒有code split,也沒有需要非同步載入的module,這時編譯出的js檔案只有兩個:

  1. manifest.js,也就是bootstrap程式碼
  2. 你的原始碼編譯後的js檔案

它們都是chunk。有圖為證:

webpack bootstrap原始碼解讀

main chunk就是你的原始碼編譯生成的,因為它是以入口檔案為起點生成的,所以也叫entry chunk

還記得在初始化部分installedChunks的初始化值麼

/******/ 	// objects to store loaded and loading chunks
/******/ 	var installedChunks = {
/******/ 		1: 0
/******/ 	};
複製程式碼

這裡已經把id為1的chunk的值置成0了,說明這個chunk已經載入好了。what?這不是才開始初始化嗎! 再看看上面的那張圖,manifest這個chunk的id為1,manifest當然執行了~

再說複雜的,也就是有code split的情況,這時就不止有entry chunk了,還有因為code split產生的chunk。 code split的情形有兩種:

  1. 通過CommonChunkPlugin分離出的chunk
  2. 非同步模組產生的chunk

第2點的非同步模組,指的是通過require.ensure或者import()引入的模組,這些模組因為是非同步載入的,會被單獨打包到一個檔案,在 觸發載入條件時才會載入這個chunk.js

ok,我們總結一下產生chunk的3種情形

  1. entry chunk 也就是入口檔案產生的chunk,這個必有
  2. initial chunk 也就是manifest生成的chunk,這個也是必有
  3. normal chunk 也就是code split產生的chunk,這個得看你是否有用到code split,且他們是非同步載入的

完整的manifest.js

/******/ (function(modules) { // webpackBootstrap
/******/ 	// install a JSONP callback for chunk loading
/******/ 	var parentJsonpFunction = window["webpackJsonp"];
/******/ 	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/ 		// add "moreModules" to the modules object,
/******/ 		// then flag all "chunkIds" as loaded and fire callback
/******/ 		var moduleId, chunkId, i = 0, resolves = [], result;
/******/ 		for(;i < chunkIds.length; i++) {
/******/ 			chunkId = chunkIds[i];
/******/ 			if(installedChunks[chunkId]) {
/******/ 				resolves.push(installedChunks[chunkId][0]);
/******/ 			}
/******/ 			installedChunks[chunkId] = 0;
/******/ 		}
/******/ 		for(moduleId in moreModules) {
/******/ 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ 				modules[moduleId] = moreModules[moduleId];
/******/ 			}
/******/ 		}
/******/ 		if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/ 		while(resolves.length) {
/******/ 			resolves.shift()();
/******/ 		}
/******/ 		if(executeModules) {
/******/ 			for(i=0; i < executeModules.length; i++) {
/******/ 				result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ 			}
/******/ 		}
/******/ 		return result;
/******/ 	};
/******/
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// objects to store loaded and loading chunks
/******/ 	var installedChunks = {
/******/ 		1: 0
/******/ 	};
/******/
/******/ 	// The require function
/******/ 	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;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// on error function for async loading
/******/ 	__webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/ })
/************************************************************************/
/******/ ([]);
複製程式碼

相關文章