乐趣区

webpack启动代码源码解读

前言
虽然每天都在用 webpack,但一直觉得隔着一层神秘的面纱,对它的工作原理一直似懂非懂。它是如何用原生 JS 实现模块间的依赖管理的呢?对于按需加载的模块,它是通过什么方式动态获取的?打包完成后那一堆 /******/ 开头的代码是用来干什么的?本文将围绕以上 3 个问题,对照着源码给出解答。
如果你对 webpack 的配置调优感兴趣,可以看看我之前写的这篇文章:webpack 调优总结
模块管理
先写一个简单的 JS 文件,看看 webpack 打包后会是什么样子:
// main.js
console.log(‘Hello Dickens’);

// webpack.config.js
const path = require(‘path’);
module.exports = {
entry: ‘./main.js’,
output: {
filename: ‘bundle.js’,
path: path.resolve(__dirname, ‘dist’)
}
};
在当前目录下运行 webpack,会在 dist 目录下面生成打包好的 bundle.js 文件。去掉不必要的干扰后,核心代码如下:
// webpack 启动代码
(function (modules) {
// 模块缓存对象
var installedModules = {};

// webpack 实现的 require 函数
function __webpack_require__(moduleId) {
// 检查缓存对象,看模块是否加载过
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}

// 创建一个新的模块缓存,再存入缓存对象
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

// 执行模块代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// 将模块标识为已加载
module.l = true;

// 返回 export 的内容
return module.exports;
}

// 加载入口模块
return __webpack_require__(__webpack_require__.s = 0);
})
([
/* 0 */
(function (module, exports) {
console.log(‘Hello Dickens’);
})
]);
代码是一个立即执行函数,参数 modules 是由各个模块组成的数组,本例子只有一个编号为 0 的模块,由一个函数包裹着,注入了 module 和 exports2 个变量(本例没用到)。
核心代码是__webpack_require__这个函数,它的功能是根据传入的模块 id,返回模块 export 的内容。模块 id 由 webpack 根据文件的依赖关系自动生成,是一个从 0 开始递增的数字,入口文件的 id 为 0。所有的模块都会被 webpack 用一个函数包裹,按照顺序存入上面提到的数组实参当中。
模块 export 的内容会被缓存在 installedModules 中。当获取模块内容的时候,如果已经加载过,则直接从缓存返回,否则根据 id 从 modules 形参中取出模块内容并执行,同时将结果保存到缓存对象当中(将在下文讲解)。
我们再添加一个文件,在入口文件处导入,再来看看生成的启动文件是怎样的。
// main.js
import logger from ‘./logger’;

console.log(‘Hello Dickens’);
logger();

//logger.js
export default function log() {
console.log(‘Log from logger’);
}
启动文件的模块数组:
[
/* 0 */
(function (module, __webpack_exports__, __webpack_require__) {

“use strict”;
Object.defineProperty(__webpack_exports__, “__esModule”, {
value: true
});
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__logger__ = __webpack_require__(1);

console.log(‘Hello Dickens’);

Object(__WEBPACK_IMPORTED_MODULE_0__logger__[“a” /* default */])();
}),
/* 1 */
(function (module, __webpack_exports__, __webpack_require__) {

“use strict”;
/* harmony export (immutable) */
__webpack_exports__[“a”] = log;

function log() {
console.log(‘Log from logger’);
}
})
]
可以看到现在有 2 个模块,每个模块的包裹函数都传入了 module, __webpack_exports__, __webpack_require__三个参数,它们是通过上文提到的__webpack_require__注入的:
// 执行模块代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
执行的结果也保存在缓存对象中了。
按需加载
再对代码进行改造,来研究 webpack 是如何实现动态加载的:
// main.js
console.log(‘Hello Dickens’);

import(‘./logger’).then(logger => {
logger();
});
logger 文件保持不变,编译后比之前多出了 1 个 chunk。
bundle_asy 的内容如下:
(function (modules) {
// 加载成功后的 JSONP 回调函数
var parentJsonpFunction = window[“webpackJsonp”];

// 加载成功后的 JSONP 回调函数
window[“webpackJsonp”] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0,
resolves = [],
result;

for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];

// installedChunks[chunkId] 不为 0 且不为 undefined,将其放入加载成功数组
if (installedChunks[chunkId]) {
// promise 的 resolve
resolves.push(installedChunks[chunkId][0]);
}

// 标记模块加载完成
installedChunks[chunkId] = 0;
}

// 将动态加载的模块添加到 modules 数组中,以供后续的 require 使用
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()();
}
};

// 模块缓存对象
var installedModules = {};

// 记录正在加载和已经加载的 chunk 的对象,0 表示已经加载成功
// 1 是当前模块的编号,已加载完成
var installedChunks = {
1: 0
};

// require 函数,跟上面的一样
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}

var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

module.l = true;

return module.exports;
}

// 按需加载,通过动态添加 script 标签实现
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];

// chunk 已经加载成功
if (installedChunkData === 0) {
return new Promise(function (resolve) {
resolve();
});
}

// 加载中,返回之前创建的 promise(数组下标为 2)
if (installedChunkData) {
return installedChunkData[2];
}

// 将 promise 相关函数保持到 installedChunks 中方便后续 resolve 或 reject
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;

// 启动 chunk 的异步加载
var head = document.getElementsByTagName(‘head’)[0];
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 + “” + chunkId + “.bundle_async.js”;
script.onerror = script.onload = onScriptComplete;
var timeout = setTimeout(onScriptComplete, 120000);

function onScriptComplete() {
script.onerror = script.onload = null;

clearTimeout(timeout);

var chunk = installedChunks[chunkId];

// 正常的流程,模块加载完后会调用 webpackJsonp 方法,将 chunk 置为 0
// 如果不为 0,则可能是加载失败或者超时
if (chunk !== 0) {
if (chunk) {
// 调用 promise 的 reject
chunk[1](new Error(‘Loading chunk ‘ + chunkId + ‘ failed.’));
}
installedChunks[chunkId] = undefined;
}
};

head.appendChild(script);

return promise;
};

// 加载入口模块
return __webpack_require__(__webpack_require__.s = 0);
})
([
/* 0 */
(function (module, exports, __webpack_require__) {

console.log(‘Hello Dickens’);

// promise resolve 后,会指定加载哪个模块
__webpack_require__.e /* import() */(0)
.then(__webpack_require__.bind(null, 1))
.then(logger => {
logger();
});
})
]);
挂在到 window 下面的 webpackJsonp 函数是动态加载模块代码下载后的回调,它会通知 webpack 模块下载完成并将模块加入到 modules 当中。
__webpack_require__.e 函数是动态加载的核心实现,它通过动态创建一个 script 标签来实现代码的异步加载。加载开始前会创建一个 promise 存到 installedChunks 对象当中,加载成功则调用 resolve,失败则调用 reject。resolve 后不会传入模块本身,而是通过__webpack_require__来加载模块内容,require 的模块 id 由 webpack 来生成:
__webpack_require__.e /* import() */(0)
.then(__webpack_require__.bind(null, 1))
.then(logger => {
logger();
});
接下来看下动态加载的 chunk 的代码,0.bundle_asy 的内容如下:
webpackJsonp([0], [
/* 0 */
,
/* 1 */
(function (module, __webpack_exports__, __webpack_require__) {

“use strict”;
Object.defineProperty(__webpack_exports__, “__esModule”, {
value: true
});
/* harmony export (immutable) */
__webpack_exports__[“default”] = log;

function log() {
console.log(‘Log from logger’);
}
})
]);
代码非常好理解,加载成功后立即调用上文提到的 webpackJsonp 方法,将 chunkId 和模块内容传入。这里要分清 2 个概念,一个是 chunkId,一个 moduleId。这个 chunk 的 chunkId 是 0,里面只包含一个 module,moduleId 是 1。一个 chunk 里面可以包含多个 module。
总结
本文通过分析 webpack 生成的启动代码,讲解了 webpack 是如何实现模块管理和动态加载的,希望对你有所帮助。
如果你对 webpack 的配置调优感兴趣,可以看看我之前写的这篇文章:webpack 调优总结

退出移动版