Webpack5源码解读系列4 - 运行时、代码生成原理
背景
Code Generation即代码生成,Webpack
在经过make
阶段模块分析建立ModuleGraph
以及在seal阶段前半段通过构建ChunkGraph
描述产物输出组织形式,接下来在seal
的下半段会根据ChunkGraph
将应用转为运行时代码,本阶段Webpack
会把Module
封装进Chunk
,并最终输出为产物文件。
为了更好地了解代码生成原理,在讲解代码生成流程前会讲解打包时插入的应用运行时能力,所以本文包含两方面内容:
Webpack
在浏览器上以什么机制管理应用模块、打包出来分割产物如何加载,以及其搜集原理- 模块、文件代码生成流程,更深入一点涉及到
Webpack
把所有资源都视为模块底层实现原理。
Webpack Runtime
Runtime运行时原理
Runtime是指提供应用在浏览器运行时模块管理基础库代码,是应用运行时的基石。在ESM标准出来之前,浏览器上运行js代码是没有原生模块管理机制,应用需要依靠AMD
、System
、UMD
等模块管理库才能在浏览器上实现模块管理。为了使应用在浏览器上能够有模块管理机制且能够支持Webpack
特性,Webpack
提供了一套运行时基础库,该基础库不仅有模块管理能力,还能够提供异步加载、Module Federation
等强大功能。
假设如下应用,其中A为入口文件,引用了B模块
// A模块
import { B } from './b';
export function A() {
B();
}
// B模块
export function B() {}
打包出来的产物经过精简后我们可以得到下面这份代码:
(() => { // webpackBootstrap
"use strict";
var __webpack_modules__ = ({
"./src/dependOn/b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"B": () => (B)
});
// b.js
function B() {
}
})
});
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
/* 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]});
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
})();
/* 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});
};
})();
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"A": () => (A)
});
var _b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/dependOn/b.js");
// a.js
function A() {
(0, _b__WEBPACK_IMPORTED_MODULE_0__.B)();
}
})();
})();
仔细观察产物代码,可以发现上面是应用模块管理代码,并且可以发现两个特征:
-
整体上使用IIFE包裹创造作用域避免影响全局以及受全局环境影响;
-
除了业务代码之外,还有一些辅助闭包缓存、工具方法支持模块管理能力:
__webpack_module_cache__
:模块缓存,所有被加载的模块都会被缓存起来;__webpack_require__
:模块引用代码,通过该方法可直接导入代码;__webpack_exports__
:记录模块导出的对象;__webpack_require__.r
:定义导出模块为esModule的工具方法;__webpack_require__.d
:定位导出模块的getter方法的工具方法;__webpack_require__.o
:hasOwnProperty方法;
- 这些
__webpack_
开头的代码内容是webpack根据代码需要生成的webpack运行时代码,上面代码逻辑为: -
- 内部通过IIFE执行A模块,在A模块中使用
__webpack_require__.r
和__webpack_require__.d
将A模块的导出处理为符合ES模块标准。 - 接着通过
__webpack_require__
方法加载B模块代码,同样的会用__webpack_require__.r
和__webpack_require__.d
将B模块的导出处理为符合ES模块标准。 - 加载完毕后会将加载结果缓存起来,下次加载时不用重新执行B模块代码,保证B模块全局只有一个实例(内部可能存在闭包缓存共享状态)。
- 内部通过IIFE执行A模块,在A模块中使用
上面case中只使用到了ES模块管理能力,如果代码中使用到了异步加载能力,则会插入与异步加载能力相关代码,下面是将B模块改为异步导入后编译产物,产物有两个文件,下面是主文件:
(() => { // webpackBootstrap
"use strict";
// ...省略模块管理代码
/* 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) => {
// 调用__webpack_require_f.l方法
return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
})();
/* webpack/runtime/load script */
(() => {
var inProgress = {};
var dataWebpackPrefix = "webpack-test:";
// 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];
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/publicPath */
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript)
scriptUrl = document.currentScript.src
if (!scriptUrl) {
var scripts = document.getElementsByTagName("script");
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src
}
}
// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
// or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/?.*$/, "").replace(//[^/]+$/, "/");
__webpack_require__.p = scriptUrl;
})();
/* webpack/runtime/jsonp chunk loading */
(() => {
// 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 = {
"A": 0
};
// 提供模块加载入口方法
__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) {
promises.push(installedChunkData[2]);
} else {
if (true) { // all chunks have JS
// setup Promise in chunk cache
var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
promises.push(installedChunkData[2] = promise);
// start chunk loading
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
// create error before stack unwound to get useful stacktrace later
var error = new Error();
var loadingEnded = (event) => {
if (__webpack_require__.o(installedChunks, chunkId)) {
installedChunkData = installedChunks[chunkId];
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;
}
}
};
// install a JSONP callback for chunk loading
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
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;
if (chunkIds.some((id) => (installedChunks[id] !== 0))) {
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) var result = runtime(__webpack_require__);
}
if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
installedChunks[chunkId][0]();
}
installedChunks[chunkId] = 0;
}
}
// 全局定义注册模块方法,其他模块通过该方法注册模块
var chunkLoadingGlobal = self["webpackChunkwebpack_test"] = self["webpackChunkwebpack_test"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
})();
// 以下是A模块内容
var __webpack_exports__ = {};
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"A": () => (A)
});
function A() {
__webpack_require__.e("src_dependOn_b_js").then(__webpack_require__.bind(__webpack_require__, /*! ./b */ "./src/dependOn/b.js"));
}
})();
另外一个是异步加载文件,由于没有runtime代码,所以比较简单:
"use strict";
// 使用上述
(self["webpackChunkwebpack_test"] = self["webpackChunkwebpack_test"] || []).push([["src_dependOn_b_js"], {
"./src/dependOn/b.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
"B": () => (B)
});
function B() {}
})
}]);
相比于普通模块管理,采用异步模块管理会多出三个主要的工具方法:
-
__webpack_require__.l
:通过script
标签加载并运行其他模块方法 -
__webpack_require__.e
:调用__webpack_require__.f.l
方法 -
__webpack_require__.f.j
:提供异步模块加载功能 -
webpackJsonpCallback
:注册模块加载回调能力,可能同时存在一个模块多次加载情况,该方法会搜集回调并统一执行- 异步加载整体流程如下:
上述两个例子可以看出Webpack在编译时会通过分析并搜集模块使用到的运行时能力,往产物代码中按需插入运行时代码,那么内部是如何实现呢?
Runtime代码实现原理
Webpack5源码解读系列3 - 分析产物组织形式 中讲到每个模块资源在Webpack中都会处理为NormalModule,与NormalModule类似,Webpack编译时将运行时能力都视为模块,由与NormalModule平级的RuntimeModule进行抽象,具体的能力实现继承于RuntimeModule,Module类继承关系如下:
每个RuntimeModule都会实现generate方法,用于代码生成时插入对应功能代码,如下面是HasOwnPropertyRuntimeModule
:
class HasOwnPropertyRuntimeModule extends RuntimeModule {
constructor() {
super("hasOwnProperty shorthand");
}
/**
* @returns {string} runtime code
*/
generate() {
const { runtimeTemplate } = this.compilation;
return Template.asString([
`${RuntimeGlobals.hasOwnProperty} = ${runtimeTemplate.returningFunction(
"Object.prototype.hasOwnProperty.call(obj, prop)",
"obj, prop"
)}`
]);
}
}
Webpack在代码一共进行两轮模块代码生成,两次目的各不相同:
- 第一轮模块代码生成时将所有的NormalModule放进执行队列中,目的在于获取模块所使用的Runtime能力,在代码生成结束后会根据模块使用的Runtime能力创建RuntimeModule,用于下一轮代码生成。
- 第二轮模块代码生成时将所有NormalModule和RuntimeModule放进执行队列中,此阶段获取用于输出的代码。
在经过第一轮代码生成之后,所有模块都会得到一份runtimeRequirement
集合,里面存储着每个模块使用到的运行时能力。比如下面这个模块解析结果:
import {c} from "./c";
import {d} from "./d";
export function a() {
d();
c();
}
经过模块代码生成之后会得到如下runtimeRequirement
集合:
[ "__webpack_require__", "__webpack_require__.r", "__webpack_exports__", "__webpack_require__.d"]
接下来会分别将Module
的runtimeRequirement
与关联的Chunk
绑定起来、Chunk
的runtimeRequirement
与Entry Chunk
绑定起来:
一个应用入口只需要一份Runtime代码,上述操作目的是将Runtime代码打包到入口文件中,这样能够保证从该入口进入的所有代码都能够使用到Runtime能力。另外,如果在入口选项配置了runtime字段,那么runtime代码会从入口Chunk抽离出去单独处理成一个Chunk:
module.exports = {
entry: {
A: {
import: './src/a.js',
runtime: 'runtime-chunk',
},
},
}
Entry Chunk
拿到runtimeRequirement
之后,会通过RuntimePlugin
注册的钩子回调中往Chunk
插入RuntimeModule
:
class RuntimePlugin {
apply(compiler) {
compiler.hooks.compilation.tap("RuntimePlugin", compilation => {
// ...省略不必要代码
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.definePropertyGetters)
.tap("RuntimePlugin", chunk => {
compilation.addRuntimeModule(
chunk,
new DefinePropertyGettersRuntimeModule()
);
return true;
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.makeNamespaceObject)
.tap("RuntimePlugin", chunk => {
compilation.addRuntimeModule(
chunk,
new MakeNamespaceObjectRuntimeModule()
);
return true;
});
});
}
}
代码生成原理
在Webpack中Chunk对应一个输出资源文件,Chunk内部封装了多个Module,应用经过Webpack打包之后会产生两部分代码:
- Module代码:包括业务代码和运行时代码,如下图的黄色和蓝色部分。
- Chunk代码:将Module代码整合起来,并提供运行的基本框架,如下图中的红色部分。
Module代码转译
Webpack将所有的资源都视为模块,所有资源在内部以NormalModule形式存在,但NormalModule在代码生成时并不会直接抹平所有资源差异,而是通过创建NormalModule时传入的generator处理差异,如JavaScriptGenerator、CssGenerator、WebAssemblyGenerator等解决各种资源代码生成差异。
下面以JavaScript文件类型讲解代码生成流程。
在讲解模块代码生成逻辑之前,需要讲解代码生成过程中的数据结构:
- Source:在代码转译时需要直接对源码进行操作,Webpack中使用了Source以及其子类对源码内容进行领域模型封装,允许生成器对Source进行修改,具体有ReplaceSource用于源码字符串替换、ConcatSource用于源码和代码碎片合并。
- InitFragment:提供插入代码碎片能力,如添加条件判断语句。
两者均是修改源码的方式,在转译代码之后,JavaScriptGenerator会通过InitFragment.addToSource静态方法将两者合并,最终返回Source实例。
Generator在创建ReplaceSource实例之后,接下来会将模块代码进行转义。Webpack是静态模块打包工具,会针对模块的导入导出语法做转义,在前面模块分析时,每个模块的导入导出语句都会处理为Dependency,所以这里会获取模块的Dependency并逐一处理。
每个Dependency都会有对应的Template去处理语法转译,Dependency对应语法转译为Webpack运行时代码,如下面是cjs导出语句的Template,由于代码比较多,所以省略部分代码:
const handleDependencyBase = (depBase, module, runtimeRequirements) => {
let base = undefined;
let type;
switch (depBase) {
case "exports":
// 往module.runtimeRequirement添加运行时全局能力标识,标识本模块使用到该能力
runtimeRequirements.add(RuntimeGlobals.exports);
base = module.exportsArgument;
type = "expression";
break;
case "module.exports":
runtimeRequirements.add(RuntimeGlobals.module);
base = `${module.moduleArgument}.exports`;
type = "expression";
break;
// ...省略其他代码
default:
throw new Error(`Unsupported base ${depBase}`);
}
return [type, base];
};
class CommonJsExportsDependencyTemplate extends (
NullDependency.Template
) {
/**
* @param {Dependency} dependency the dependency for which the template should be applied
* @param {ReplaceSource} source the current replace source which can be modified
* @param {DependencyTemplateContext} templateContext the context object
* @returns {void}
*/
apply(
dependency,
source,
{module, moduleGraph, initFragments, runtimeRequirements, runtime}
) {
const dep = /** @type {CommonJsExportsDependency} */ (dependency);
const used = moduleGraph
.getExportsInfo(module)
.getUsedName(dep.names, runtime);
// 解析模块依赖语法
const [type, base] = handleDependencyBase(
dep.base,
module,
runtimeRequirements
);
// 根据语法类型对ReplaceSource进行替换或者添加代码碎片
switch (type) {
case "expression":
if (!used) {
// 往模块生成代码中插入代码碎片
initFragments.push(
new InitFragment(
"var __webpack_unused_export__;\n",
InitFragment.STAGE_CONSTANTS,
0,
"__webpack_unused_export__"
)
);
// 替换源码内容
source.replace(
dep.range[0],
dep.range[1] - 1,
"__webpack_unused_export__"
);
return;
}
source.replace(
dep.range[0],
dep.range[1] - 1,
`${base}${propertyAccess(used)}`
);
return;
// 其他代码
}
}
};
从上面代码中可以看到Template中做了两件事情:
- 根据语法往runtimeRequirement中添加元素,用于RuntimeModule的生成。
- 根据语法对source进行修改,以及往模块转译产物中添加代码碎片。
上面是针对于cjs导出语句的处理逻辑,而js中还有amd、systemjs以及esm模块管理方案,这些模块的导入导出语句会由专门的Dependency实现类处理,每个Dependency会有Template进行代码转义为符合webpack模块管理能力代码,从而抹平了这些模块管理能力的差异。
Chunk代码生成
Chunk封装了多个Module,在代码生成时提供了Module代码运行容器并将NormalModule、RuntimeModule渲染到运行的容器中,最终输出为产物文件。Chunk能够支持多种类型文件代码生成,如js、css、wasm,webpack通过Plugin方式拓展Chunk代码生成能力。
Compilation进入Chunk代码生成时,会通过tapable触发代码生成钩子,由事先注册好的各种类型文件插件实现具体类型文件代码生成逻辑,如JavaScriptModulePlugin、CssModulePlugin、WasmModulePlugin等等。与Module代码生成类似,Chunk代码生成时由Source及其子类管理代码生成产物,通过InitFragment向Source插入代码碎片。所以Webpack生成多类型文件的底层实现如下:
以JavaScriptModulePlugin为例子讲解Chunk代码生成,Webpack5源码解读系列3 - 分析产物组织形式 文章中了解到异步导入会产生Chunk,与原有Chunk具有父子关系,这种关系会在生成代码时处理为引用顺序关系。除此之外,在生成代码时会分别为含有Runtime和不含Runtime能力两类Chunk做代码生成:
- 含有Runtime Chunk:入口Chunk或者特别配置runtime属性将runtime抽离出去的Chunk,这一类Chunk会含有RuntimeModule代码生成产物。
- 不含有Runtime Chunk:异步导入Chunk,只含有业务代码。
由于Runtime能力全局只需要一份,且需要所有代码均能够访问到Runtime能力,所以一般情况下都会将代码生成在入口Chunk文件中,除非有特殊说明需要单独抽离Chunk:
-
含有Runtime Chunk:代码分为三部分:
- Chunk容器代码,创建作用域并提供存放模块代码
- runtime代码:根据模块代码时获取所有RuntimeModule生成的代码
- module代码:NormalModule生成的代码
- 不含Runtime Chunk:将本Chunk包含所有NormalModule代码生成存放于容器中即可
以上代码并不是简单地堆叠存放,而是在JavaScriptModulePlugin内部会利用Source和InitFragment根据模块特性对代码进行组装,涉及到具体功能实现这里逐一讲解,我们只需要了解Chunk代码生成流程即可:
小结
本文以JavaScript为例子讲解Webpack代码运行时以及代码生成原理:
-
运行时能力:Webpack在运行时提供一套模块管理、代码分割产物加载等功能基础能力代码,业务代码运行在这一套基础能力之上。
-
在代码生成阶段,webpack分为模块代码生成和Chunk代码生成:
-
模块代码生成分为两次代码生成,其目的各有不同:
- 第一次获取应用中所有模块进行代码生成,目的是为了搜集模块所使用的运行时能力,以便后续生成RuntimeModule;
- 第二次会获取应用中所有模块进行代码生成,用于最终代码封装。
-
Chunk分为是否含有runtime能力两类,其中含有runtime能力会包含runtime代码,Chunk会将代码封装成文件并用于输出。
-
暂时无法在飞书文档外展示此内容
转载自:https://juejin.cn/post/7231816037812060218