webpack bootstrap源碼解讀

雖然一直在用webpack,但不多去看它編譯出來的js代碼,大概是由於調試的時候有sourcemap,能夠直接調試源碼。一時心血來潮想研究一下,看了一些關於webpack編譯方面的文章都有提到,再結合本身看源碼的體會,記錄一下本身的理解javascript

說bootstap可能還有點很差理解,看一下webpack編譯出來的js文件就很好理解了:html

// 編譯前的入口文件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的執行代碼。那麼問題來了:java

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

這就是bootstrap的做用了。若是不用code split把bootstrap單獨分離出來,它就在編譯出的js文件最上面,由於須要先執行bootstrap後續的代碼才能執行。咱們能夠用CommonChunkPlugin把它單獨提出來,方便咱們閱讀。把下面的代碼寫到你的webpack的plugin配置裏便可:webpack

new webpack.optimize.CommonsChunkPlugin({
    name: "manifest" // 能夠叫manifest,也能夠用runtime
}),
複製代碼

配置以後,編譯出來的文件會多出一個manifest.js文件,這就是webpack bootstrap的代碼了。bootstrap和用戶代碼(就是咱們本身寫的部分)編譯後的文件實際上是一個總體,因此後面的分析會引入用戶代碼一塊兒看es6

manifest.js

manifest源碼分爲3個部分:web

  1. 建立了一個閉包,初始化須要用到的變量
  2. 定義webpackJsonp方法,掛載到window變量下
  3. 定義與編譯相關的輔助函數和變量,如__webpack_require__(也就是咱們在本身的代碼裏用到的require語法)

咱們一個一個來看。下面的每一個部分,咱們都只截取manifest源碼的相關部分來看,完整的源碼放在文章最後了bootstrap

初始化部分

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

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

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

咱們截取了manifest最外層的代碼和初始化部分的代碼,能夠看到整個文件都被一個閉包括在裏面,而modules的初始值是一個空的Array([])。 這樣作能夠隔離做用域,保護內部的變量不被污染segmentfault

  • modules 空的Array([]),用來存放每一個module的內容
  • installedModules存放module的cache,一個module被執行後(module的執行會在webpackJsonp的源碼部分提到)的結果被保存到這裏,以後再用到這個模塊就能夠直接使用緩存而無需再次執行了
  • installedChunks 用來存放chunk的執行狀況。若一個chunk已經加載了,在installedChunks裏這個chunk的值會變成0,也就是無需再加載了

若是分不清module和chunk這兩個概念的區別,文章最後一節專門對此做了解釋數組

webpackJsonp

源碼分析

在講webpackJsonp的源碼以前,先回憶一下咱們本身的chunk代碼promise

// 編譯前的入口文件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。有圖爲證:

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; };
/******/ })
/************************************************************************/
/******/ ([]);
複製代碼
相關文章
相關標籤/搜索