记一次对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上,以为不错的能够去点个赞

相关文章
相关标签/搜索