blog
blog copied to clipboard
webpack runtime 源码分析
项目基本配置可参考基于 https://github.com/taniarascia/webpack-boilerplate.git。
(一)从打包产物——html 开始
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>webpack-demo</title>
<link rel="icon" href="/favicon.ico">
<script defer="defer" src="/js/runtime.dacd45666f2c5eb35b8d.bundle.js"></script>
<script defer="defer" src="/js/main.7086fc1df0c0b6a0d989.bundle.js"></script>
<link href="/styles/main.64e10d654e2697258d08.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
</body>
</html>
从浏览器角度,html 是应用入口和运行起点。可以看到,编译出的JS通过<script>标签插入了 html。
- 其中
/js/runtime.dacd45666f2c5eb35b8d.bundle.js是 webpack 提供的运行时,是编译出代码能运行的基础; - 而
/js/main.7086fc1df0c0b6a0d989.bundle.js是我们的入口模块(可能因为CONCATENATE MODULE的优化,除了入口module外有其它module的代码)。
这里需额外注意到:
defer 异步加载,不阻塞 html 下载和解析,且保证这些 script 按序执行(保证了runtime先执行);按序执行非常重要,runtime 脚本提供了webpack的全局变量/方法,提供了模块加载执行的能力等等。
(二)打包产物——模块/chunk 是怎么包装的?
简单来说,我们写的js文件(模块/module)被webpack处理后变成了什么?
有最简单的 src/js/info.js:
export const text = '2021/02/01'
编译后就是dist/js/info.be139a6f62da8ba74d4c.chunk.js:
"use strict";
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[996],{
/***/ 67:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "text": () => (/* binding */ text)
/* harmony export */ });
var text = '2021/02/01';
/***/ })
}]);
为了让编译后的代码保持可读性,webpack的配置去除了压缩。我们可以看到:
-
代码被包装在
(__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 我们的真正代码 };- 这个并不是一定的,也可能是
(module) => { 我们的真正代码 },这取决于代码是否 ES Module 形式的等等。 - webpack 一定帮我们提供了
module|exports|require的环境,保证了我们代码能正常运行。当然这三个变量webpack会编译并匹配包装函数。 __webpack_require__上提供了一些工具函数,具体是什么下个章节讨论。
- 这个并不是一定的,也可能是
-
webpack的编译产出是 chunk,是一个或多个module产出一个chunk。所以我们看到
{moduleId: wrappedModuleCode}的形式:- 就是上面的
{67: (__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 代码 })}。
- 就是上面的
-
最终chunk的形式是
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push(参数)。- 参数是数组,
self["webpackChunkwebpack_demo"].push保证了数组里面如果有代码需要运行是可以运行的,而不是静态的数组。 - 参数的形式是
[[chunkId], moreModules, runtime],runtime就是在 push 时可以运行的代码。
- 参数是数组,
看看entry point编译出的 initial chunk 会是什么样?
(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[179],{
/***/ 573:
/***/ ((__unused_webpack_module, __unused_webpack___webpack_exports__, __webpack_require__) => {
"use strict";
;// CONCATENATED MODULE: ./src/js/title.js
var getTitle = function getTitle() {
return '柴门闻犬吠,风雪夜归人。';
};
// EXTERNAL MODULE: ./src/js/log.js
var log = __webpack_require__(967);
var log_default = /*#__PURE__*/__webpack_require__.n(log);
;// CONCATENATED MODULE: ./src/images/logo.svg
const logo_namespaceObject = "data:image/svg+xml;base64,...........";
;// CONCATENATED MODULE: ./src/index.js
// Test import of a JavaScript function
// Test import of an asset
// Test import of styles
var logo = document.createElement('img');
logo.src = logo_namespaceObject;
var heading = document.createElement('h1');
heading.textContent = getTitle();
var app = document.querySelector('#root');
app.append(logo, heading);
setTimeout(function () {
__webpack_require__.e(/* import() | info */ 996).then(__webpack_require__.bind(__webpack_require__, 67)).then(function (v) {
var footer = document.createElement('footer');
footer.textContent = v.text;
app.append(footer);
log_default()();
});
}, 100);
__webpack_require__.e(/* import() */ 232).then(__webpack_require__.bind(__webpack_require__, 232)).then(function (v) {
var div = document.createElement('div');
div.textContent = v.author;
app.append(div);
});
/***/ }),
/***/ 967:
/***/ ((module) => {
module.exports = function () {
console.log('xxxxxxxxx');
};
/***/ })
},
/******/ __webpack_require__ => { // webpackRuntimeModules
/******/ /* webpack/runtime/startup prefetch */
/******/ (() => {
/******/ __webpack_require__.O(0, [179], () => {
/******/ __webpack_require__.E(996);
/******/ }, 5);
/******/ })();
/******/
/******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
/******/ var __webpack_exports__ = (__webpack_exec__(573));
/******/ }
]);
(三)打包产物——webpack runtime 有哪些功能
一切魔法都在 webpack runtime 脚本和模块代码的包装中,从 webpack runtime 开始分析编译出的代码,看看一个简单的项目是怎么跑起来的。而我们写的 import/export 这些模块化的代码,是怎么成功跑在浏览器里的。
这里首先明确一个概念:
- chunk:分为 initial/non-initial 两种类型。
- 其中 initial 是指 main chunk for entry point,包括entry point涉及的所有模块和依赖;
- non-initial 是指可能懒加载的 chunk,由
import()或者SplitChunksPlugin产生。
- module/(webpack module): 组成程序的功能块(chunks of functionality),可能是js/css/img等等,可通过
import/require/<img src=...>等等。
一定程度上,中文“模块”一词可能存在混用以上概念,注意分辨。
然后我们正式来分析 webpack runtime。webpack runtime 代码根据配置/是否有异步模块等不同,稍有不同,但总共不超过几百行代码。
webpack runtime 结构
整个webpack runtime是个IIFE(Immediately Invoked Function Expression),整体结构如下:
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ({});
/************************************************************************/
// 缓存,缓存了所有模块的 exports
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// 检查是不是在缓存里?在直接返回即可。
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 否则创建 module,并放到缓存。
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// 执行对应的模块,执行完成后 epxorts 就有值了
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;
// 下面是一堆自执行函数(IIFE):
// 1. 为__webpack_require__添加各种方法;
// 2. 最终往window上添加 webpackChunkwebpack_demo 数组(最终是否叫webpackChunkwebpack_demo取决于config)。
})()
-
定义了
__webpack_modules__来存储所有模块,定义了__webpack_require__来作为模块中用到的require方法。 -
通过一堆 IIFE 在
__webpack_require__上定义了一堆工具函数,这些函数可以被编译的模块去使用。 -
最后一个IIFE中定义了一个唯一全局变量
'webpackChunkwebpack_demo',该变量为数组,提供 push 方法来加载 chunk。- 变量名由
output.chunkLoadingGlobal等配置确定(默认值是'webpackChunkwebpack'); - 所有的模块最终通过
webpackChunkwebpack_demo.push来执行,或者说所有的模块被它包装:(self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || []).push([[id],{id:code},runtime])
- 变量名由
最终我们的入口 chunk 执行时,通过webpackChunkwebpack_demo.push来最终执行业务代码。
webpack runtime 中的工具函数解析
核心的 'webpackChunkwebpack_demo'(即webpackJsonpCallback)
webpackChunkwebpack_demo.push 是什么?webpackChunkwebpack_demo.push 最终调用 webpackJsonpCallback。
// self["webpackChunkwebpack_demo"]一般为undefined,初始化为空数组;如果不为空则保留。
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
// 假如 self["webpackChunkwebpack_demo"] 之前非空,那么对其每项调用 webpackJsonpCallback,
// 把相关 chunks & modules 存入缓存(module未执行,后面的 startup 真正执行)。
// 这里 0 作为参数传入,即 parentChunkLoadingFunction 是0,防止了 webpackJsonpCallback 执行时元素重复压入chunkLoadingGlobal。
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 设置 chunkLoadingGlobal数组的push方法(这是其它模块的wrapper function);
// webpackJsonpCallback 绑定 parentChunkLoadingFunction 为 chunkLoadingGlobal.push, 元素被压入chunkLoadingGlobal
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
来具体看看 webpackJsonpCallback 作用是什么。请结合上面的 initial chunk 代码来看(看调用chunkLoadingGlobal.push的传参)。
var __webpack_modules__ = ({});
// expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;
// 缓存所有加载完成/加载中的模块,0代表加载完成
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
var installedChunks = {
666: 0
};
/**
* install a JSONP callback for chunk loading
* @params {function|0} parentChunkLoadingFunction,可能是数组的push方法,也可能是0
* @params {Array} data 格式是 [[moduleId], {moduleId: moduleCode}, runtime]
*
* moduleId: 数字;moduleCode:wrapper function包裹的模块代码;runtime: (__webpack_require__)=>any
**/
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
// "moreModules" 就是当前加载的chunk自带的模块,包括自身代码的模块、CONCATENATED过来的模块、其它依赖模块。
var [chunkIds, moreModules, runtime] = data;
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0;
// 当前加载的chunk一般不会是0,所以执行这个if内逻辑,把moreModules添加到__webpack_modules__
if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
for(moduleId in moreModules) {
// moreModules 形式为 {id: wrappedCode},我们直接把它们挂载到 __webpack_modules__
// 方便之后__webpack_require__(id) 可以正确执行对应模块。
if(__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
// 然后看有需要执行的代码就直接执行
if(runtime) var result = runtime(__webpack_require__);
}
// push 到 chunkLoadingGlobal 数组
if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
// 如果这个chunk是loading状态,则resolve掉它,方便通知之前等待该chunk的代码继续执行。
if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0]();
}
// 标记这个chunk加载完成
installedChunks[chunkId] = 0;
}
// 那么__webpack_require__.O在干嘛?下一小节看
return __webpack_require__.O(result);
}
var chunkLoadingGlobal = self["webpackChunkwebpack_demo"] = self["webpackChunkwebpack_demo"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// chunkLoadingGlobal.push.bind(chunkLoadingGlobal) 作为 parentChunkLoadingFunction,此时是 chunkLoadingGlobal 的数组push方法
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
webpackJsonpCallback 作为真正的业务模块调用入口,主要就是把 push 进来的 chunk 指定的模块加载到 __webpack_modules__,并执行 chunk 指定的业务代码(模块)。
__webpack_require__.O 在干什么?
webpackJsonpCallback 中只剩__webpack_require__.O没搞明白作用,这一小节来解释下。
/* webpack/runtime/chunk loaded */
(() => {
var deferred = [];
__webpack_require__.O = (result, chunkIds, fn, priority) => {
if(chunkIds) {
priority = priority || 0;
for(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
deferred[i] = [chunkIds, fn, priority];
return;
}
// 以我们下面的传参为例,这里deferred是 [[179], () => {
// __webpack_require__.E(996);
// }, 5]
var notFulfilled = Infinity;
for (var i = 0; i < deferred.length; i++) {
var [chunkIds, fn, priority] = deferred[i];
var fulfilled = true;
for (var j = 0; j < chunkIds.length; j++) {
// 5 & 1 是1,但是 Infinity >= 5 成立,
// __webpack_require__.O 上只有 j,是检查chunkId对应的chunk是否已加载,这里第一次不成立而第二次成立
if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {
chunkIds.splice(j--, 1);
} else {
fulfilled = false;
if(priority < notFulfilled) notFulfilled = priority;
}
}
// 第一次不成立,而第二次可以执行
if(fulfilled) {
deferred.splice(i--, 1)
var r = fn(); // 第二次其实是执行 __webpack_require__.E(996),即prefetch 996
if (r !== undefined) result = r;
}
}
return result;
};
__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);
})();
多数时候 __webpack_require__.O 接收的参数全部是 undefined,即什么都不干。所以略过它多数时候也不影响理解webpack流程。但我们的入口 chunk 其实会传入不一样的参数,我们来看下:
__webpack_require__ => { // webpackRuntimeModules
/* webpack/runtime/startup prefetch */
(() => {
__webpack_require__.O(0, [179], () => {
__webpack_require__.E(996);
}, 5);
})();
var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
var __webpack_exports__ = (__webpack_exec__(573));
}
这是initial chunk的runtime函数执行时,会调用__webpack_require__.O,并且是在业务代码执行前。然后等webpackJsonpCallback执行时,__webpack_require__.O会执行第二次,此时会开始 prefetch id 为 996 的 chunk。
这符合预期:prefetch 在父 chunk 加载完成后开始。
__webpack_require__.F 和 __webpack_require__.E 怎么配合完成 prefetch/preload
/* webpack/runtime/chunk prefetch function */
(() => {
__webpack_require__.F = {};
__webpack_require__.E = (chunkId) => {
Object.keys(__webpack_require__.F).map((key) => {
__webpack_require__.F[key](chunkId);
});
}
})();
__webpack_require__.F.j = (chunkId) => {
// 如果 chunkId 不是 666(runtime chunk),也没有加载过
if((!__webpack_require__.o(installedChunks, chunkId) || installedChunks[chunkId] === undefined) && 666 != chunkId) {
// null 代表 chunk preloaded/prefetched
installedChunks[chunkId] = null;
var link = document.createElement('link');
if (__webpack_require__.nc) {
link.setAttribute("nonce", __webpack_require__.nc);
}
link.rel = "prefetch";
link.as = "script";
link.href = __webpack_require__.p + __webpack_require__.u(chunkId);
document.head.appendChild(link);
}
};
代码一目了然。
重要的jsonp加载chunk相关 __webpack_require__.l & __webpack_require__.f.j & __webpack_require__.e
这里讲讲 webpack 最基本的怎么加载 chunk 的(jsonp)。
/* webpack/runtime/load script */
(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack-demo:";
// loadScript function to load a script via script tag
__webpack_require__.l = (url, done, key, chunkId) => {
// 如果已经在加载中了,直接返回
if(inProgress[url]) { inProgress[url].push(done); return; }
var script, needAttach;
if(key !== undefined) {
var scripts = document.getElementsByTagName("script");
for(var i = 0; i < scripts.length; i++) {
var s = scripts[i];
if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
}
}
if(!script) {
needAttach = true;
script = document.createElement('script');
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.setAttribute("data-webpack", dataWebpackPrefix + key);
script.src = url;
}
inProgress[url] = [done];
var onScriptComplete = (prev, event) => {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var doneFns = inProgress[url];
// 加载完成就删除inProgress[url]
delete inProgress[url];
script.parentNode && script.parentNode.removeChild(script);
doneFns && doneFns.forEach((fn) => (fn(event)));
if(prev) return prev(event);
};
var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
needAttach && document.head.appendChild(script);
};
})();
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
})();
// import()|require.ensure 的核心实现代码
__webpack_require__.f.j = (chunkId, promises) => {
// JSONP chunk loading for javascript
var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
// 正在加载,那么取出promise放到 promises 即可
promises.push(installedChunkData[2]);
} else {
if(666 != chunkId) {
// setup Promise in chunk cache
var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
promises.push(installedChunkData[2] = promise);
// start chunk loading 构造异步模块的正确加载路径(publicPath + filename)
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
// 只需要处理加载失败的问题(因为chunk代码执行会更新installedChunks[chunkId]为0)
var loadingEnded = (event) => {
if(__webpack_require__.o(installedChunks, chunkId)) {
installedChunkData = installedChunks[chunkId];
// 加入没有加载成功(chunk代码执行会更新installedChunks[chunkId]为0),
// 则置为 undefined(chunk not loaded)
if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
if(installedChunkData) {
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;
installedChunkData[1](error);
}
}
};
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
} else installedChunks[chunkId] = 0;
}
}
};
显而易见的 __webpack_require__.o & __webpack_require__.r & __webpack_require__.d
1. __webpack_require__.o 即 hasOwnProperty
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
2. __webpack_require__.r
给 exports 对象加上 __esModule=true 等标记。
/* webpack/runtime/make namespace object */
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
3. __webpack_require__.d
给 exports 对象加上我们导出的那些属性/方法(其它模块通过 import 来使用)。注意,这里是通过 getter 来导出。
/* webpack/runtime/define property getters */
(() => {
// define getter functions for harmony exports
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
})();