記一次對webpack打包後代碼的失敗探究

記得4月新出了webpack4,這個月恰好沒什麼事情,用webpack4又從新去搭了一遍本身的項目。在搭項目的途中,突然對webpack模塊化以後的代碼起了興趣,因而想搞清楚咱們引入的文件究竟是怎麼運行的。javascript

一、基本版——單入口引入一個js文件

所謂的基本版,就是我只引入了一個test.js,代碼只有一行var a = 1。打包以後,發現生成的文件main.js並無多少代碼,只有90行不到。html

截取出真正執行的代碼就更加少了,只有下面4行。咱們接下去就從這幾行代碼中看下打包出來的文件的執行流程是怎麼樣的。java

(function(modules) {
    //新建一個對象,記錄導入了哪些模塊
    var installedModules = {};
    
    // The require function 核心執行方法
    function __webpack_require__(moduleId){/*內容暫時省略*/}
    
    // expose the modules object (__webpack_modules__) 記錄傳入的modules做爲私有屬性
    __webpack_require__.m = modules;
    
    // expose the module cache 緩存對象,記錄了導入哪些模塊
    __webpack_require__.c = installedModules;
    
    
    // Load entry module and return exports 默認將傳入的數組第一個元素做爲參數傳入,這個s應該是start的意思了
    return __webpack_require__(__webpack_require__.s = 0);
})([(function(module, exports, __webpack_require__) {
/* 0 */
    var a = 1;
/***/ })
/******/ ])
複製代碼

首先很明顯,整個文件是個自執行函數。傳入了一個數組參數moduleswebpack

這個自執行函數內部一開始新建了一個對象installedModules,用來記錄打包了哪些模塊。git

而後新建了函數__webpack_require__,能夠說整個自執行函數最核心的就是__webpack_require____webpack_require__有許多私有屬性,其中就有剛剛新建的installedModulesgithub

最後自執行函數return__webpack_require__,並傳入了一個參數0。由於__webpack_require__的傳參變量名稱叫作moduleId,那麼傳參傳進來的也就是*模塊id**。因此我大膽猜想這個0多是某個模塊的id。web

這時候我瞄到下面有一行註釋/* 0 */。能夠發現webpack會在每個模塊導入的時候,會在打包模塊的頂部寫上一個id的註釋。那麼剛纔那個0就能解釋了,就是咱們引入的那個模塊,因爲是第一個模塊,因此它的id是0。json

那麼當傳入了moduleId以後,__webpack_require__內部發生了什麼?數組

__webpack_require__解析

function __webpack_require__(moduleId) {
    // Check if module is in cache 
    // 檢查緩存對象中是否有這個id,判斷是否首次引入
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache) 添加到.c緩存裏面
    var module = installedModules[moduleId] = {
    	i: moduleId,
    	l: false,
    	exports: {}
    };
    // Execute the module function 執行經過moduleId獲取到的函數
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // Flag the module as loaded
    // 表示module對象裏面的模塊加載了
    module.l = true;
    // Return the exports of the module
    return module.exports;
}
複製代碼

首先經過moduleId判斷這個模塊是否引入過。若是已經引入過的話,則直接返回。不然installedModules去記錄下此次引入。這樣子若是別的文件也要引入這個模塊的話,避免去重複執行相同的代碼。瀏覽器

而後經過modules[moduleId].call去執行了引入的JS文件。

看完這個函數以後,你們能夠發現其實webpack打包以後的文件並無什麼很複雜的內容嘛。固然這很大一部分緣由是由於咱們的場景太簡單了,那麼接下來就增長一點複雜性。

二、升級版——單入口引入多個文件

接下來我修改一下webpack入口,單個入口同時下引入三個個文件

entry: [path.resolve(__dirname, '../src/test.js'),path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')],
複製代碼

三個文件的內容分別爲var a = 1,var b = 2,var c = 3。接下來咱們能夠看看打包以後的代碼

打包以後的文件main.js核心內容並無發生變化,和上面如出一轍。可是這個自執行函數傳入的參數卻發生了變化。

(function(modules) {
    /*這部份內容省略,和前面如出一轍*/
})([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
        __webpack_require__(1);
        __webpack_require__(2);
        module.exports = __webpack_require__(3);
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
        var a = 1;
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
        var b = 2;
/***/ })
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
        var c = 3;
/***/ })
/******/ ]);
複製代碼

前面說過,自執行函數默認將傳入的參數數組的第一個元素傳入__webpack_require__執行代碼。

咱們能夠看一下傳入第一個參數的內容,在上一章中是咱們引入的文件內容var a = 1,可是這裏卻不是了。而是按模塊引入順序執行函數__webpack_require__(1),__webpack_require__(2),__webpack_require__(3),經過__webpack_require__函數去執行了咱們引入的代碼。

你們能夠先想一下這裏的1,2,3是怎麼來的,爲何能夠函數調用的時候,直接傳參1,2,3

不過到這裏還不明白,module.exports到底起了什麼做用,若是起做用,爲何又只取最後一個呢?

3.升級版——多入口,多文件引入方式

由於好奇若是多入口多文件是怎麼樣的,接下去我又將入口改了一下,變成了下面這樣

entry: {
    index1: [path.resolve(__dirname, '../src/test1.js')],
    index2: [path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')],
},
複製代碼

打包生成了index1.jsindex2.js。發現index1.js和第一章講的同樣,index2.js和第二個文件同樣。並無什麼讓我很意外的東西。

四、進階版——引入公共模塊

在前面的打包文件中,咱們發現每一個模塊id彷佛是和引入順序有關的。而在咱們平常開發環境中,必然會引入各類公共文件,那麼webpack會怎麼處理這些id呢

因而咱們在配置文件中新增了webpack.optimize.SplitChunksPlugin插件。

webpack2和3版本中是webpack.optimize.CommonsChunkPlugin插件。可是在webpack4進行了一次優化改進,想要了解的能夠看一下這篇文章webpack4:代碼分割CommonChunkPlugin的壽終正寢。因此這裏的代碼將是使用webpack4打包出來的。

而後修改一下配置文件中的入口,咱們開了兩個入口,而且兩個入口都引入了test3.js這個文件

entry: {
        index1: [path.resolve(__dirname, '../src/test.js'),path.resolve(__dirname, '../src/test3.js')],
        index2: [path.resolve(__dirname, '../src/test2.js'),path.resolve(__dirname, '../src/test3.js')],
    },
複製代碼

能夠看到,打包後生成了3個文件。

<script type="text/javascript" src="scripts/bundle.4474bdd2169853ce33a7.js"></script>
<script type="text/javascript" src="scripts/index1.4474bdd2169853ce33a7.js"></script>
<script type="text/javascript" src="scripts/index2.4474bdd2169853ce33a7.js"></script>
複製代碼

首先bundle.js(文件名本身定義的)很明顯是一個公共文件,裏面應該有咱們提取test3.js出來的內容。打開文件後,發現裏面的代碼並很少,只有下面幾行。

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[2],{
/***/ 2:
/***/ (function(module, exports, __webpack_require__) {
var c = 1;
/***/ })
}]);
複製代碼

單純看文件內容,咱們大概能推測出幾點:

  • window全局環境下有一個名爲webpackJsonp的數組
  • 數組的第一個元素仍然是數組,記錄了數字2,應該是這個模塊的id
  • 數組第二個元素是一個記錄了形式爲{模塊id:模塊內容}的對象。
  • 對象中的模塊內容就是咱們test3.js,被一個匿名函數包裹

webpack2中,採用的是{文件路徑:模塊內容}的對象形式。不過在升級到webpack3中優化採用了數字形式,爲了方便提取公共模塊。

注意到一點,這個文件中的2並不像以前同樣做爲註釋的形式存在了,而是做爲屬性名。可是它爲何直接就將這個模塊id命名爲2呢,目前來看,應該是這個模塊是第二個引入的。帶着這個想法,我接下去看了打包出來的index1.js文件

截取出了真正執行而且有用的代碼出來。

// index1.js
(function(modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
    function webpackJsonpCallback(){
        /*暫時省略內容*/
        return checkDeferredModules
    }
    
    function checkDeferredModules(){/*暫時省略內容*/}
    
    // The module cache
    var installedModules = {};
    
    // object to store loaded and loading chunks
    // undefined = chunk not loaded, null = chunk preloaded/prefetched
    // Promise = chunk loading, 0 = chunk loaded
    var installedChunks = {
    	0: 0
    };
    
    var deferredModules = [];   //
    
    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;
    
    
    // add entry module to deferred list
    deferredModules.push([0,2]);
    // run deferred modules when ready
    return checkDeferredModules();
    
})([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
    __webpack_require__(1);
    module.exports = __webpack_require__(2);
    /***/ }),
    /* 1 */
    /***/ (function(module, exports, __webpack_require__) {
        var a = 1;
    /***/ })
/******/ ]);
複製代碼

在引入webpack.optimize.SplitChunksPlugin以後,核心代碼在原來基礎上新增了兩個函數webpackJsonpCallbackcheckDeferredModules。而後在原來的installedModules基礎上,多了一個installedModules,用來記錄了模塊的運行狀態;一個deferredModules,暫時不知道幹嗎,看名字像是存儲待執行的模塊,等到後面用到時再看。

此外,還有這個自執行函數最後一行代碼調用形式再也不像以前同樣。以前是經過調用__webpack_require__(0),如今則變成了checkDeferredModules。那麼咱們便順着它如今的調用順序再去分析一下如今的代碼。

在分析了不一樣以後,接下來就按照運行順序來查看代碼,首先能看到一個熟悉的變量名字webpackJsonp。沒錯,就是剛纔bundle.js中暴露到全局的那個數組。因爲在html中先引入了bundle.js文件,因此咱們能夠直接從全局變量中獲取到這個數組。

前面已經簡單分析過window["webpackJsonp"]了,就不細究了。接下來這個數組進行了一次for循環,將數組中的每個元素傳參給了方法webpackJsonpCallback。而在這裏的演示中,傳入就是咱們bundle.js中一個包含模塊信息的數組[[2],{2:fn}}]

接下來就看webpackJsonpCallback如何處理傳進來的參數了

webpackJsonpCallback簡析

/******/ 	function webpackJsonpCallback(data) {
/******/ 		var chunkIds = data[0]; // 模塊id
/******/ 		var moreModules = data[1];  // 提取出來的公共模塊,也就是文件內容
/******/ 		var executeModules = data[2];   // 須要執行的模塊,但演示中沒有
/******/ 		// add "moreModules" to the modules object,
/******/ 		// then flag all "chunkIds" as loaded and fire callback
/******/ 		var moduleId, chunkId, i = 0, resolves = [];
/******/
/******/ 		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(data);
/******/
/******/ 		while(resolves.length) {
/******/ 			resolves.shift()();
/******/ 		}
/******/
/******/ 		// add entry modules from loaded chunk to deferred list
/******/ 		deferredModules.push.apply(deferredModules, executeModules || []);
/******/
/******/ 		// run deferred modules when all chunks ready
/******/ 		return checkDeferredModules();
/******/ 	};
複製代碼

這個函數中主要乾了兩件事情,分別是在那兩個for循環中。

一是在installedChunks對象記錄引入的公共模塊id,而且將這個模塊標爲已經導入的狀態0

installedChunks[chunkId] = 0;
複製代碼

而後在另外一個for循環中,設置傳參數組modules的數據。咱們公共模塊的id是2,那麼便設置modules數組中索引爲2的位置爲引入的公共模塊函數。

modules[moduleId] = moreModules[moduleId];
//這段代碼在咱們的例子中等同於 modules[2] = (function(){/*test3.js公共模塊中的代碼*/})
複製代碼

其實當看到這段代碼時,內心就有個疑問了。由於index1.js中設置modulesp[2]這個操做並非一個push操做,若是說數組索引爲2的位置已經有內容了呢?暫時保留着心中的疑問,繼續走下去。心中隱隱感受到這個打包後的代碼其實並非一個獨立的產物了。

咱們知道modules是傳進來的一個數組參數,在第二個章節中能夠看到,咱們會在最後執行函數__webpack_require__(0),而後依順序去執行全部引入模塊。

不過此次卻和之前不同了,能夠看到webpackJsonpCallback最後返回的代碼是checkDeferredModules。前面也說了整個自執行函數最後返回的函數也是checkDeferredModules,能夠說它替代了__webpack_require__(0)。接下去就去看看checkDeferredModules發生了什麼

checkDeferredModules簡析

/******/ 	function checkDeferredModules() {
/******/ 		var result;
/******/ 		for(var i = 0; i < deferredModules.length; i++) {
/******/ 			var deferredModule = deferredModules[i];
/******/ 			var fulfilled = true;
/******/ 			for(var j = 1; j < deferredModule.length; j++) {
/******/ 				var depId = deferredModule[j];
/******/ 				if(installedChunks[depId] !== 0) fulfilled = false;
/******/ 			}
/******/ 			if(fulfilled) {
/******/ 				deferredModules.splice(i--, 1);
/******/ 				result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
/******/ 			}
/******/ 		}
/******/ 		return result;
/******/ 	}
複製代碼

這個函數關鍵點彷佛是在deferredModules,可是咱們剛纔webpackJsonpCallback惟一涉及到這個的只有這麼一句,而且executeModules實際上是沒有內容的,因此能夠說是空數組。

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

既然沒有內容,那麼webpackJsonpCallback就只能結束函數了。回到主線程,發現下面立刻是兩句代碼,得,又繞回來了。

// add entry module to deferred list
deferredModules.push([0,2]);
// run deferred modules when ready
return checkDeferredModules();
複製代碼

不過如今就有deferredModules這個數組終於有內容了,一次for循環下來,最後去執行咱們模塊的代碼仍然是這一句

result = __webpack_require__(__webpack_require__.s = deferredModule[0]);
複製代碼

很熟悉,有木有,最後仍是回到了__webpack_require__,而後就是熟悉的流程了

__webpack_require__(1);
module.exports = __webpack_require__(2);
複製代碼

可是當我看到這個內容居然有這行代碼時__webpack_require__(2);仍是有點崩潰的。爲何?由於它代碼明確直接執行了__webpack_require__(2)。可是2這個模塊id是經過在全局屬性webpackJsonp得到的,代碼不該該明確知道的啊。

我原來覺得的運行過程是,每一個js文件經過全局變量webpackJsonp得到到公共模塊id,而後push到自執行函數傳參數組modules。那麼等到真正執行的時候,會按照for循環依次執行數組內的每一個函數。它不會知道有1,2這種明確的id的。

爲何我會這麼想呢?由於我一開始認爲每一個js文件都是獨立的,想交互只能經過全局變量來。既然是獨立的,我天然不知道公共模塊id是2事實上,webpackJsonp的確是驗證了個人想法。

惋惜結果跟我想象的徹底不同,在index1.js直接指定執行哪些模塊。這隻能說明一個事情,其實webpack內部已經將全部的代碼順序都肯定好了,而不是在js文件中經過代碼來肯定的。事實上,當我去查看index2.js文件時,更加肯定了個人想法。

/******/ (function(modules) {/*內容和index1.js同樣*/})
/************************************************************************/
/******/ ([
/* 0 */,
/* 1 */,
/* 2 */,
/* 3 */
/***/ (function(module, exports, __webpack_require__) {
    __webpack_require__(4);
    module.exports = __webpack_require__(2);
/***/ }),
/* 4 */
/***/ (function(module, exports, __webpack_require__) {
    var b = 2;
/***/ })
/******/ ]);
//# sourceMappingURL=index2.19eeab4e90ee99ee1ce4.js.map
複製代碼

仔細查看自執行函數的傳參數組,發現它的第0,1,2位都是undefined。咱們知道這幾個數字其實就是每一個模塊自己的Id。而這幾個id偏偏就是index1.jsbundle.js中的模塊。理論上來講在瀏覽器下運行,index2.js應該沒法得知的,可是事實卻徹底相反。

走到這一步,我對webpack打包後的代碼也沒有特別大的慾望了,webpack內部實現纔是更重要的了。好了,不說了,我先去看網上webpack的源碼解析了,等我搞明白了,再回來寫續集。

文章首發於個人github上,以爲不錯的能夠去點個贊

相關文章
相關標籤/搜索