Webpack打包文件分析与动态加载原理

我心飞翔 分类:javascript

一、构建结果分析

先从一个简单的模块开始。

假设我们有一个hello模块如下,

function hello() {
  console.log('hello world')
}
hello()
 

使用webpack进行构建后,得到的结果如下,

我们的代码经过webpack构建后,生成的是一个IIFE。该自执行函数先定义了一系列的方法,然后在最后执行了

下面我们来看看__webpack_require__到底做了什么。该函数的定义如下:

通过分析上面的代码,我们可以知道,__webpack_require__的作用其实就是定义了一个module,同时将这个module__webpack_require__本身作为参数传给指定的函数并执行。这样在函数内部给modulemodule.exports赋值时,就能同时修改installedModules里对应的模块。而且在执行函数内部如果使用__webpack_require__方法,也能获取到modules中相应的模块。__webpack_require__(0)表示执行第一个模块,也就是我们的hello模块。hello的代码比较简单,只是简单的执行一下函数。下面我们来看一个稍微复杂一点的🌰。

我们把代码修改一下,定义一个log模块,然后在hello中使用它。

使用webpack重新构建,得到的bundle如下:

通过分析发现,相比第一次构建的代码,modules中多了一个模块,而这个模块正是我们后面添加的log模块。而且hello模块编译出来的代码也有了一些变化:

这里我们看到多了一个__webpack_require__.r的方法,这个方法是初始化的时候定义的,它的功能其实很简单,就是标记一下当前的模块为esModule,我们分析模块执行时,可以忽略这些不会对流程造成影响的逻辑。hello模块中,通过__webpack_require__(1)加载下一个模块的exports,并将其缓存到变量_log__WEBPACK_IMPORTED_MODULE_0__上,然后在hello方法中传递参数给log__WEBPACK_IMPORTED_MODULE_0__["default"]并执行。

接下来我们再看一下log模块的实现:

log模块中使用了一个__webpack_require__.d方法,该方法的定义如下:

不难发现,这个__webpack_require__.d方法的作用就是:检查指定对象上是否具有某个属性,如无,就为这个对象的指定属性绑定一个getter。所以,当执行下面的代码时,其实就是在module.exports添加了一个default的方法,执行该方法会返回log函数。

因此,在hello模块中,就能通过__webpack_require__(1)获取log模块的exports。通过__webpack_require__(1)["default"])()就能调用log方法。

综上所述,我们可以了解到,webpack打包出来的IIFE,其实核心就只有两个,一个是定义的内置方法,另一个是依赖的modules数组,模块之间通过__webpack_require__引用。我们写的模块代码,会作为依赖传递给webpack定义的函数,同时,第一个模块就是入口文件。

二、 动态加载

当我们在编写大型单页vue应用时,一般会用到路由懒加载(当访问到特定路由时,才会去加载相应的模块)。其实这里主要是利用了动态 import这个特性。把上面的例子改一下,看看webpack是如何处理动态 import的。

经过webpack构建后,我们发现,除了app.bundle.js以外,还多了一个1.bundle.js,这就是我们要动态导入的模块。另外,app.bundle.js中还多了几个先前没出现过的方法。

(function (modules) { // webpackBootstrap
// 省略部分代码
// 处理jsonp
function webpackJsonpCallback(data) {  //...}; 
// 构造jsonp的请求地址
function jsonpScriptSrc(chunkId)  {// ...}
// 动态加载方法,返回promise,在promise.then中可以使用__webpack_require__加载动态模块
webpack_require__.e = function requireEnsure(chunkId) {   // ...};
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;
return __webpack_require__(__webpack_require__.s = 0);
})([/* 0 */
(function (module, exports, __webpack_require__) {
function hello() {
__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(res => console.log(res))
}
hello()
})
]);

还是先从入口文件看起,当执行hello方法时,会执行以下逻辑:

__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(res => console.log(res))

但我们发现,webpack打包出来的依赖数组中,就只有hello模块,并没有其他模块了,如果在hello中直接使用__webpack_require__(1),那就会报错。但在__webpack_require__.e(/* import() */ 1)之后就能正常引用了。__webpack_require__.e这个方法到底做了什么呢?我们先来看看它的定义:


// 用来存储已加载和加载中的chunk
// undefined表示chunk未加载,null表示chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
0: 0 // 表示模块0已加载
};
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId]; // 当前chunkId为1,值为undefined,表示未加载,
if (installedChunkData !== 0) { // 0 表示chunk已经安装了
// installedChunkData只有0, undefined, null ,promise4种类型的值。如果installedChunkData布尔值为true,表示installedChunkData为一个数组,chunk加载中
if (installedChunkData) { // 
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise); // installedChunkData = [resolve, reject,promise],例如installedChunkData保存新创建的promise以及他的resolve和reject
// promises => [promise]
// 开始加载chunk
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId]; // [resolve, reject,promise]
if (chunk !== 0) {
if (chunk) { // reject erro
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;
}
};
var timeout = setTimeout(function () {
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete; // load事件会在script加载并执行完才触发
document.head.appendChild(script);
}
}
return Promise.all(promises);
};

__webpack_require__.e的作用是判断模块是否已加载(或加载中),如果都不是,就利用jsonp加载模块。该方法会返回一个promise,当script加载并执行成功时会resolve,当加载失败时reject。这里需要注意的是,scriptload事件会在脚本下载并执行完之后触发

通过jsonp加载的1.bundle.js的实现如下:

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1], [/* 0 */,  /* 1 */
(function (module, __webpack_exports__, __webpack_require__) {
    "use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (function () {
return 'dynamic'
});
})
]]);

不难发现,js加载完成后,调用了 window["webpackJsonp"]push方法。当这个push方法可不是数组原生当push方法,初始化时webpack做了一个小动作,关键代码如下:

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // oldJsonpFunction -> push,代理jsonpArray的push方法
jsonpArray.push = webpackJsonpCallback; 

所以在执行push方法时,实际上是执行webpackJsonpCallback。我们来看一下这个函数的定义:

function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// 把新加载的模块加到modules数组中
// 把模块标记为已加载,然后resolve
var moduleId, chunkId, i = 0, resolves = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]); // 把chunk的resolve方法放到resolves数组中,后面统一resolve
}
installedChunks[chunkId] = 0; // 把模块标记为已加载
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId]; // 把新加载回来的模块添加到之前的模块数组中
}
}
if (parentJsonpFunction) parentJsonpFunction(data); // 把模块数据放到window["webpackJsonp"] 数组中
while (resolves.length) {
resolves.shift()(); // resolve the promise
}
};

这个函数其实就做了几件事:

  • 把模块标记为已加载,并追加到modules数组中
  • 把模块数据放到window["webpackJsonp"] 数组中
  • 调用之前那些promise的resolve

因此,当__webpack_require__.e(/* import() */ 1).then时,就能通过__webpack_require__拿到动态获取的模块了。

__webpack_require__.e(/* import() */ 1).then(__webpack_require__.bind(null, 1)).then(res => console.log(res))

回复

我来回复
  • 暂无回复内容