webpack开发打包分析

627 阅读7分钟

从立即执行函数开始

首先定义一个函数,这个函数的入参为打包的入口./src/index.js,webpack是用相对资源地址作为模块的键值。

  (function(modules) {})(
  {
    "./src/index.js":
    (function(module, exports, __webpack_require__) {
    const page = () => {
     return __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.t.bind(null,       "./src/page.js", 7))
}
 })
}
  )

代码首先执行立即执行函数,传入的参数modules为一个键值对形式。每个模块的加载函数接收三个参数:

  • module是webpack对模块的抽象,它的数据结构有模块id(i),模块是否加载(l),模块的导出对象(exports)。在webpack中万物皆模块
  • exports,为模块的导出。
  • __webpack__require__为模块加载函数,异步的或同步的。

__webpack_require__

// 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;
/******/ 	}

这个函数接收一个moduleId,模块id参数。

  • 检查模块是否在installedModules的缓存里
  • 如果没有对应moduleId的模块,则创建一个模块。将这个模块的外包装对象分别赋值给module变量和installedModules[moduleId],这样缓存里就存在了这个模块id的缓存。
  • i表示这个模块的id,
  • l表示模块是否被加载了
  • exports表示这个模块 modules['./src/index.js']就会执行,上下文为module.exports,第一个参数模块就是module(模块的包装对象), 第二个参数为真正的模块,第三个参数为__webpack_require__函数本身。由于page.js是一个异步模块,会调用__webpack_require__的异步模块加载程序。

异步模块加载requireEnsure

/******/ 	// object to store loaded and loading chunks
/******/ 	// undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ 	// Promise = chunk loading, 0 = chunk loaded
/******/ 	var installedChunks = {
/******/ 		"main": 0
/******/ 	};

异步加载模块,首先定义一个安装的块集合变量,默认main.js模块是加载完成的状态,异步模块加载有四个过程:

  • undefined表示异步模块未被加载
  • null表示异步模块被预加载
  • Promise表示模块正在加载中
  • 0表示模块已加载完成 在Promise状态,我们可以在模块渲染的地方显示一个模块加载的动画,目前webpack模块加载的机制还没整明白,我们继续往下看。

chunkId

每个异步模块有一个chunkId。比如./src/page.js。通过__webpack_require__的异步模块加载函数,我们异步加载了page.js内容。

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/page.js":
 (function(module, exports) {
function component() {
    const element = document.createElement('div');
    element.innerHTML =['Hello', 'webpack'].join(',')
  
    return element;
  }
  document.body.appendChild(component());
})

}]);

看看这个异步模块的内容,webpackJsonp是一个全局变量,它是一个数组,数组的元素是数组(用moduleX表示),moduleX的第一个元素是一个数组,第二个元素是一个模块表,存储各个模块。 具体为什么这样的结构,还需要看后续的代码。webpackJsonp维护着一个模块表。

// This file contains only the entry chunk.
/******/ 	// The chunk loading function for additional chunks
/******/ 	__webpack_require__.e = function requireEnsure(chunkId) {
/******/ 		var promises = [];
/******/
/******/
/******/ 		// JSONP chunk loading for javascript
/******/
/******/ 		var installedChunkData = installedChunks[chunkId];
/******/ 		if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ 			// a Promise means "currently loading".
/******/ 			if(installedChunkData) {
/******/ 				promises.push(installedChunkData[2]);
/******/ 			} 
/******/ 		}
/******/ 		return Promise.all(promises);
/******/ 	};
  • 这个函数使用了Promise异步api
  • installedChunkData表示异步模块的加载状态。如果加载状态不等于0,则执行模块加载的流程。根据上述所说的四个状态进行处理。
  • 0, null,undefined均为falsy的值,因此只有Promise的状态下才进入if(installedChunkData) 分支。

创建加载js异步Promise的流程

else {
/******/ 				// setup Promise in chunk cache
/******/ 				var promise = new Promise(function(resolve, reject) {
/******/ 					installedChunkData = installedChunks[chunkId] = [resolve, reject];
/******/ 				});

首先创建一个promisepromise执行立即变更installedChunkData状态的值为Promise状态 ,并将缓存模块状态的值变为一个包含resolve,reject的数组。这里有一个疑问,为什么不将promise直接赋值给installedChunkData呢?继续看代码:

promises.push(installedChunkData[2] = promise);

这里直接将安装模块的索引为2的元素变为promise。这里的问号更大了。

script.onerror = script.onload = onScriptComplete;

统一处理script加载错误或成功。

	var timeout = setTimeout(function(){
/******/ 					onScriptComplete({ type: 'timeout', target: script });
/******/ 				}, 120000);

如果onScriptComplete没有调用,则会在未来的一个时间段爆出错误。超时时间2分钟。也就是说一个异步模块如果2分钟内没有加载完成,则视为这个模块加载失败。那么我们来看看onScriptComplete这个函数。

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;
/******/ 					}
/******/ 				};

首先清空onerroronload,避免ie的内存泄露,同时清除定时器。 如果chunk不是加载完成的状态,则表示异步模块未加载成功。会抛出ChunkLoadError错误。这个错误一般是jscss404。 那么疑问是,怎样的状态才会执行installedChunks[chunkId] = 0呢?

	// script path function
/******/ 	function jsonpScriptSrc(chunkId) {
/******/ 		return __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".js"
/******/ 	}

jsonScriptSrc这个函数用于获取chunk的路径。仅仅是获取路径。 异步模块加载完成后,比如page.js。都会执行一个叫webpackJsonpCallback的函数,为什么,因为webpackJson数组的push方法被webpackJsonpCallback劫持。在想webpackJsonp这个变量推入更多模块时将会执行更多操作。

/******/ 	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;
/******/ 	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;
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/index.js");

首先定义了一个jsonpArray数据,同时将值赋值给windowwebpackJsonp属性。 更改数组的push方法为webpackJsonpCallback,用以在push模块时执行更多的操作。 jsonpArry[i]返回一个一个数组,敲重点。这就是为什么异步模块push的时候数据结构是一个数组,数组第一个元素是数组,第二个是模块表。看下面代码回忆一下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
"./src/page.js":
 (function(module, exports) {
function component() {
    const element = document.createElement('div');
    element.innerHTML =['Hello', 'webpack'].join(',')
  
    return element;
  }
  document.body.appendChild(component());
})

}]);

就是webpackJsonpCallback需要用到这种数据结构。 首先数组的第一个元素表示chunkIds。 第二个参数为一个对象,表示更多的模块。 然后把所有chunkId的加载状态改为0,已加载状态。 将解析出的模块放入解析数组。并一个个执行。 我们这里要理解几个变量是干嘛的:

  • installedChunks,前面说过,保存模块加载状态。
  • resolves,每个模块的执行函数
  • modules,用来保存所有的模块。
  • parentJsonpFunction,数组的push方法,就是将模块推入webpackJsonp这个数组变量。 读到这里,我们对整个运行流程可能还是模糊的。不过,前面的阅读是为了后面的断点调试做准备。
/******/ 	function webpackJsonpCallback(data) {
/******/ 		var chunkIds = data[0];
/******/ 		var moreModules = data[1];
/******/
/******/
/******/ 		// 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()();
/******/ 		}
/******/
/******/ 	};

断点解读

真正理解源码的最好办法是断点运行。

image.png 我们看到,代码主入口,立即执行函数传入了一个modules变量。这个模块表只有一个注册值,'./src/index.js'也就是项目的打包入口,这算是一个总的模块。接着初始化了普通变量和函数变量,直接执行到return语句。

return __webpack_require__(__webpack_require__.s = "./src/index.js");

这里代码项__webpack_require__函数传入了一个moduleId,'./src/index.js'。 这个installedModules还是一个空对象,这样就进入了创建一个包装模块的流程。执行入口模块的加载函数 这就是index.js的代码执行。 当我们需要请求一个异步模块page.js时,我们创建了一个script标签用于加载0.js0就是这个异步块的chunkId。 这样我们就执行到了webpackJsonpCallback,这个劫持操作就是把异步模块放入modules变量。 当resolves执行后,Promise的then操作就会执行。

 __webpack_require__.t.bind(null, /*! ./page */ "./src/page.js", 7)

这操作就是执行了异步模块的内容。这样,这个异步模块的内容就展示在页面上了。 感谢阅读。