本文内容基于
webpack 5.74.0
版本进行剖析因为
webpack5
整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于js
文件类型进行剖析,不会对其它类型(css
、image
)进行剖析,所举的例子也都是基于js
类型
为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码
前言
本文是 webpack5 外围流程
解析的最初一篇文章,共有 5 篇,应用流程图的模式从头到尾剖析了webpack5 外围流程
:
- 「Webpack5 源码」make 阶段(流程图)剖析
- 「Webpack5 源码」enhanced-resolve 门路解析库源码剖析
- 「Webpack5 源码」seal 阶段(流程图)剖析(一)
- 「Webpack5 源码」seal 阶段剖析(二)-SplitChunksPlugin 源码
- 「Webpack5 源码」seal 阶段剖析(三)- 生成代码 &runtime
在上一篇文章「Webpack5 源码」seal 阶段剖析 (二)-SplitChunksPlugin 源码中,咱们进行了hooks.optimizeChunks()
的相干逻辑剖析,选取了 SplitChunksPlugin
进行具体的剖析
在上篇文章完结剖析 SplitChunksPlugin
之后,咱们将开始 codeGeneration()
的相干逻辑剖析
源码整体概述
在上一篇文章中,咱们曾经剖析了 buildChunkGraph()
和hooks.optimizeChunks
相干逻辑,当初咱们要开始剖析代码生成和文件打包输入相干的内容
如上面代码所示,咱们会执行:
codeGeneration()
:遍历modules
数组,实现所有module
代码转化,并将后果存储到compilation.codeGenerationResults
中createChunkAssets()
:合并runtime
代码(包含立刻执行函数,多种工具函数)、modules
代码、其它chunk
相干的桥接代码,并调用emitAsset()
输入产物
seal() {
//... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
// ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
// 为 module 设置深度标记
this.assignDepths(entryModules);
buildChunkGraph(this, chunkGraphInit);
this.createModuleHashes();
this.codeGeneration(err => {this.createChunkAssets(err => {});
});
}
function codeGeneration() {
// ...... 解决 runtime 代码
const jobs = [];
for (const module of this.modules) {const runtimes = chunkGraph.getModuleRuntimes(module);
for (const runtime of runtimes) {
//... 省略 runtimes.size>0 的逻辑
// 如果有多个 runtime, 则取第一个 runtime 的 hash
const hash = chunkGraph.getModuleHash(module, runtime);
jobs.push({module, hash, runtime, runtimes: [runtime] });
}
}
this._runCodeGenerationJobs(jobs, callback);
}
function _runCodeGenerationJobs(jobs, callback) {
//... 省略十分多的细节代码
const {dependencyTemplates, ...} = this;
//... 省略遍历 jobs 的代码,伪代码为:for(const job of jobs)
const {module} = job;
const {hash, runtime, runtimes} = job;
this._codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates});
}
function createChunkAssets(callback) {
asyncLib.forEachLimit(
this.chunks,
(chunk, callback) => {
let manifest = this.getRenderManifest({
chunk,
...
});
// manifest=this.hooks.renderManifest.call([], options);
//... 遍历 manifest,调用(manifest[i]=fileManifest)fileManifest.render()
asyncLib.forEach(
manifest,
(fileManifest, callback) => {
//....
source = fileManifest.render();
this.emitAsset(file, source, assetInfo);
}
);
}
)
}
codeGeneration():module 生成代码
1.1 整体流程图
联合
1.3.3 具体例子
的流程图一起看成果更佳
1.2 codeGeneration()
遍历 this.modules
,而后获取对应的运行时runtimes
,生成hash
,将这三个值都放入到job
数组中,调用this._runCodeGenerationJobs()
在 _runCodeGenerationJobs()
中,遍历 jobs
数组,一直拿出对应的 job(蕴含 module、hash、runtime)
调用 _codeGenerationModule()
进行模块代码的生成
seal() {
//... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
// ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
// 为 module 设置深度标记
this.assignDepths(entryModules);
buildChunkGraph(this, chunkGraphInit);
this.createModuleHashes();
this.codeGeneration(err => {this.createChunkAssets(err => {});
});
}
function codeGeneration() {
// ...... 解决 runtime 代码
const jobs = [];
for (const module of this.modules) {const runtimes = chunkGraph.getModuleRuntimes(module);
for (const runtime of runtimes) {
//... 省略 runtimes.size>0 的逻辑
// 如果有多个 runtime, 则取第一个 runtime 的 hash
const hash = chunkGraph.getModuleHash(module, runtime);
jobs.push({module, hash, runtime, runtimes: [runtime] });
}
}
this._runCodeGenerationJobs(jobs, callback);
}
function _runCodeGenerationJobs(jobs, callback) {
//... 省略十分多的细节代码
const {dependencyTemplates, ...} = this;
//... 省略遍历 jobs 的代码,伪代码为:for(const job of jobs)
const {module} = job;
const {hash, runtime, runtimes} = job;
this._codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates});
}
1.3 _codeGenerationModule
从上面代码块能够晓得
- 调用
module.codeGeneration()
进行代码的生成,将生成后果放入到result
- 还会遍历
runtimes
,生成运行时代码,放入到results
中
function _codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates}) {
//... 省略十分多的细节代码
this.codeGeneratedModules.add(module);
result = module.codeGeneration({
chunkGraph,
moduleGraph,
dependencyTemplates,
runtimeTemplate,
runtime,
codeGenerationResults: results,
compilation: this
});
for (const runtime of runtimes) {results.add(module, runtime, result);
}
callback(null, codeGenerated);
}
1.3.1 NormalModule.codeGeneration
// NormalModule.js
// 上面的办法对应下面的 module.codeGeneration 办法
function codeGeneration(..., dependencyTemplates, codeGenerationResults) {
//... 省略十分多的细节代码
const sources = new Map();
for (const type of sourceTypes || chunkGraph.getModuleSourceTypes(this)) {const source = this.generator.generate(this, { ..., dependencyTemplates, codeGenerationResults});
if (source) {sources.set(type, new CachedSource(source));
}
}
const resultEntry = {
sources,
runtimeRequirements,
data
};
return resultEntry;
}
由下面的代码能够晓得,最终触发的是
this.generator.generate
,而这个generator
到底是什么呢?
由 make
阶段的 NormalModuleFactory.constructor-this.hooks.resolve
的代码能够晓得,之前就曾经生成对应的 parser
解决类以及 generator
解决类
会依据不同的 type
,比方javascript/auto
、css
等不同类型的 NormalModule
进行不同 parser
和generator
的初始化
getGenerator
实质是触发this.hooks.createGenerator
,实际上是由各个Plugin
注册该 hooks 进行各种generator
返回,也就是说对于不同的文件内容,会有不同的genrator
解决类来进行代码的生成
// lib/NormalModuleFactory.js
const continueCallback = () => {
// normalLoaders = this.resolveRequestArray 进行 resolve.resolve 的后果
const allLoaders = postLoaders;
if (matchResourceData === undefined) {for (const loader of loaders) allLoaders.push(loader);
for (const loader of normalLoaders) allLoaders.push(loader);
} else {for (const loader of normalLoaders) allLoaders.push(loader);
for (const loader of loaders) allLoaders.push(loader);
}
for (const loader of preLoaders) allLoaders.push(loader);
Object.assign(data.createData, {
...
loaders: allLoaders,
...
type,
parser: this.getParser(type, settings.parser),
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator),
generatorOptions: settings.generator,
resolveOptions
});
// 为了减少可读性,将外部函数提取到内部,上面的 callback 实际上是 this.hooks.resolve.tapAsync 注册的 callback()
callback();}
getGenerator(type, generatorOptions = EMPTY_GENERATOR_OPTIONS) {if (generator === undefined) {generator = this.createGenerator(type, generatorOptions);
cache.set(generatorOptions, generator);
}
return generator;
}
createGenerator(type, generatorOptions = {}) {
const generator = this.hooks.createGenerator
.for(type)
.call(generatorOptions);
}
如上面截图所示,hooks.createGenerator
会触发多个 Plugin
执行,返回不同的 return new xxxxxGenerator()
类
其中最常见的是 javascript 类型对应的
genrator
解决类:JavascriptGenerator
1.3.2 示例:JavascriptGenerator.generate
整体流程图
概述
从以下精简代码中能够发现,JavascriptGenerator.generate()
执行的程序是:
new ReplaceSource(originalSource)
初始化源码this.sourceModule()
进行依赖的遍历,一直执行对应的template.apply()
,将后果放入到initFragments.push(fragment)
中- 最初调用
InitFragment.addToSource(source, initFragments, generateContext)
合并 source 和 initFragments
generate(module, generateContext) {const originalSource = module.originalSource();
const source = new ReplaceSource(originalSource);
const initFragments = [];
this.sourceModule(module, initFragments, source, generateContext);
return InitFragment.addToSource(source, initFragments, generateContext);
}
this.sourceModule
遍历 module.dependencies
和module.presentationalDependencies
,而后调用 sourceDependency()
解决依赖文件的代码生成
如果存在异步依赖的模块,则应用 sourceBlock()
解决,实质就是递归一直调用 sourceDependency()
解决依赖文件的代码生成
sourceModule(module, initFragments, source, generateContext) {for (const dependency of module.dependencies) {this.sourceDependency();
}
for (const dependency of module.presentationalDependencies) {this.sourceDependency();
}
for (const childBlock of module.blocks) {this.sourceBlock()
}
}
sourceBlock(module, block, initFragments, source, generateContext) {for (const dependency of block.dependencies) {this.sourceDependency();
}
for (const childBlock of block.blocks) {this.sourceBlock();
}
}
sourceDependency(module, dependency, initFragments, source, generateContext) {
const constructor = dependency.constructor;
const template = generateContext.dependencyTemplates.get(constructor);
template.apply(dependency, source, templateContext);
const fragments = deprecatedGetInitFragments(
template,
dependency,
templateContext
);
if (fragments) {for (const fragment of fragments) {initFragments.push(fragment);
}
}
}
在下面
sourceDependency()
的代码中,又呈现了依据不同类型实例化的对象template
,接下来咱们将剖析template
、dependencyTemplates
到底是什么?
dependencyTemplates 跟 dependency 的提前绑定
通过提前绑定不同类型对应的 template,为
template.apply()
做筹备
在初始化 webpack
时,webpack.js
中进行new WebpackOptionsApply().process(options, compiler)
在 WebpackOptionsApply.js
中,进行了 HarmonyModulesPlugin
的初始化
new HarmonyModulesPlugin({topLevelAwait: options.experiments.topLevelAwait}).apply(compiler);
在 HarmonyModulesPlugin.js
的apply
办法中,提前注册了多个 xxxxDependency
对应的 xxxx.Template()
的映射关系,比方上面代码中的
HarmonyCompatibilityDependency
对应HarmonyCompatibilityDependency.Template()
// HarmonyModulesPlugin.js
apply(compiler) {
compiler.hooks.compilation.tap(
"HarmonyModulesPlugin",
(compilation, { normalModuleFactory}) => {
// 绑定 Dependency 跟 Dependency.Template 的关系
compilation.dependencyTemplates.set(
HarmonyCompatibilityDependency,
new HarmonyCompatibilityDependency.Template());
// 绑定 Dependency 跟 NormalModuleFactory 的关系
compilation.dependencyFactories.set(
HarmonyImportSideEffectDependency,
normalModuleFactory
);
...
}
);
}
为什么咱们要通过
xxxxDenependency
绑定对应的xxxx.Template()
映射关系呢?
从上一篇文章「Webpack5 源码」make 整体流程浅析,咱们能够晓得,咱们在 make 阶段
会进行 AST
的解析,比方上一篇文章中呈现的具体例子,咱们会将呈现的 import {getC1}
解析为 HarmonyImportSideEffectDependency
依赖
前面咱们就能够依据这个依赖进行对应 Template
代码的生成
template.apply()
template.apply()
的实现也是依据理论类型进行辨别的,比方template.apply
可能是ConstDependency.Template.apply
,也可能是HarmonyImportDependency.Template.apply
次要有两种模式:
- 间接更改
source
源码 - 应用
initFragments.push()
减少一些代码片段
上面取一些 xxxxxDependency 进行剖析,它会先拿到对应的
Template
,而后执行apply()
ConstDependency.Template.apply()
间接操作 source,间接扭转源码
if (typeof dep.range === "number") {source.insert(dep.range, dep.expression);
return;
}
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
HarmonyImportDependency.Template.apply()
将代码增加到templateContext.initFragments
最终收集的
initFragments
数组会执行InitFragment.addToSource
,在下一个阶段执行
HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends (ModuleDependency.Template) {apply(dependency, source, templateContext) {const importStatement = dep.getImportStatement(false, templateContext);
templateContext.initFragments.push(
new ConditionalInitFragment(importStatement[0], // 同步
InitFragment.STAGE_HARMONY_IMPORTS,
dep.sourceOrder,
key,
runtimeCondition
)
);
templateContext.initFragments.push(
// await
new AwaitDependenciesInitFragment(new Set([dep.getImportVar(templateContext.moduleGraph)])
)
);
templateContext.initFragments.push(
new ConditionalInitFragment(importStatement[1], // 异步
InitFragment.STAGE_ASYNC_HARMONY_IMPORTS,
dep.sourceOrder,
key + "compat",
runtimeCondition
)
);
}
}
InitFragment.addToSource
通过十分繁冗的数据收集后,this.sourceModule()
执行结束
回到 JavascriptGenerator.generate()
代码,在 this.sourceModule()
执行结束,会执行 InitFragment.addToSource()
逻辑,而这个 InitFragment
就是下面 template.apply()
所收集的数据
generate(module, generateContext) {const originalSource = module.originalSource();
const source = new ReplaceSource(originalSource);
const initFragments = [];
this.sourceModule(module, initFragments, source, generateContext);
return InitFragment.addToSource(source, initFragments, generateContext);
}
依照上面程序拼接最终生成代码concatSource
:
header
局部代码:fragment.getContent()
middle
局部代码:module
文件理论的内容(可能通过一些革新)bottom
局部代码:fragment.getEndContent()
最终返回ConcatSource
static addToSource(source, initFragments, context) {
// 排序
const sortedFragments = initFragments
.map(extractFragmentIndex)
.sort(sortFragmentWithIndex);
const keyedFragments = new Map();
//... 省略依据 initFragments 拼接 keyedFragments 数据的逻辑
const endContents = [];
for (let fragment of keyedFragments.values()) {
// add fragment Content
concatSource.add(fragment.getContent(context));
const endContent = fragment.getEndContent(context);
if (endContent) {endContents.push(endContent);
}
}
// add source
concatSource.add(source);
for (const content of endContents.reverse()) {
// add fragment endContent
concatSource.add(content);
}
return concatSource;
}
1.3.3 具体例子
_codeGenerationModule()
流程波及的文件较为简单,上面应用一个具体例子进行整个流程再度解说,可能跟下面的内容会有所反复,然而为了可能真正明确整个流程,笔者认为肯定的反复是有必要的
咱们应用一个入口文件entry4.js
,如上面代码块所示,有两个同步依赖,以及对应的调用语句,还有一个异步依赖chunkB
import {getG} from "./item/common_____g.js";
import voca from 'voca';
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule=> {bModule.default();
});
console.info("getA2E", getG());
1.3.3.1 整体流程图
1.3.3.2 codeGeneration()
codeGeneration()
会遍历所有 this.modules
,而后通过一系列流程调用module.codeGeneration()
,也就是NormalModule.codeGeneration()
生成代码放入 result
中
function codeGeneration() {
// ...... 解决 runtime 代码
const jobs = [];
for (const module of this.modules) {const runtimes = chunkGraph.getModuleRuntimes(module);
for (const runtime of runtimes) {
//... 省略 runtimes.size>0 的逻辑
// 如果有多个 runtime, 则取第一个 runtime 的 hash
const hash = chunkGraph.getModuleHash(module, runtime);
jobs.push({module, hash, runtime, runtimes: [runtime] });
}
}
this._runCodeGenerationJobs(jobs, callback);
}
function _runCodeGenerationJobs(jobs, callback) {
//... 省略十分多的细节代码
const {dependencyTemplates, ...} = this;
//... 省略遍历 jobs 的代码,伪代码为:for(const job of jobs)
const {module} = job;
const {hash, runtime, runtimes} = job;
this._codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates});
}
function _codeGenerationModule({module, runtime, runtimes, ... , dependencyTemplates}) {
//... 省略十分多的细节代码
this.codeGeneratedModules.add(module);
result = module.codeGeneration({
chunkGraph,
moduleGraph,
dependencyTemplates,
runtimeTemplate,
runtime,
codeGenerationResults: results,
compilation: this
});
for (const runtime of runtimes) {results.add(module, runtime, result);
}
callback(null, codeGenerated);
}
1.3.3.3 NormalModule.codeGeneration()
外围代码也就是调用 generator.generate()
,咱们的示例都是JS
类型,因而等同于调用JavascriptGenerator.generate()
function codeGeneration(..., dependencyTemplates, codeGenerationResults) {
//... 省略十分多的细节代码
const sources = new Map();
for (const type of sourceTypes || chunkGraph.getModuleSourceTypes(this)) {const source = this.generator.generate(this, { ..., dependencyTemplates, codeGenerationResults});
if (source) {sources.set(type, new CachedSource(source));
}
}
const resultEntry = {
sources,
runtimeRequirements,
data
};
return resultEntry;
}
1.3.3.4 JavascriptGenerator.generate()
generate(module, generateContext) {const originalSource = module.originalSource();
const source = new ReplaceSource(originalSource);
const initFragments = [];
this.sourceModule(module, initFragments, source, generateContext);
return InitFragment.addToSource(source, initFragments, generateContext);
}
originalSource
实质就是 entry4.js
的原始内容
sourceModule()
在下面的剖析中,咱们晓得应用 sourceModule()
就是
- 遍历所有
module.dependency
,调用sourceDependency()
解决 - 遍历所有
module.presentationalDependencies
,调用sourceDependency()
解决
而 sourceDependency()
就是触发对应 dependency
的template.apply()
进行:
- 文件内容
source
的革新 initFragments.push(fragment)
收集,initFragments
数组在InitFragment.addToSource()
流程为source
插入header
代码和bottom
代码
在 entry4.js
这个 module
中
module.dependencies=[HarmonyImportSideEffectDependency("./item/common_____g.js"),
HarmonyImportSideEffectDependency("voca"),
HarmonyImportSpecifierDependency("./item/common_____g.js"),
HarmonyImportSpecifierDependency("voca")
]
module.presentationalDependencies=[HarmonyCompatibilityDependency, ConstDependency, ConstDependency]
InitFragment.addToSource()
通过 sourceModule()
的解决,咱们当初就能拿到 originalSource
,以及initFragments
数组
generate(module, generateContext) {const originalSource = module.originalSource();
const source = new ReplaceSource(originalSource);
const initFragments = [];
this.sourceModule(module, initFragments, source, generateContext);
return InitFragment.addToSource(source, initFragments, generateContext);
}
在下面的剖析中,咱们晓得 InitFragment.addToSource()
就是依照上面程序拼接最终生成代码concatSource
:
header
局部代码:fragment.getContent()
middle
局部代码:module
文件代码(可能通过一些革新)bottom
局部代码:fragment.getEndContent()
最终返回ConcatSource
而对于 entry4.js
来说,sourceModule()
会触发什么类型的 template.apply()
进行代码的收集呢?
import {getG} from "./item/common_____g.js";
import voca from 'voca';
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule=> {bModule.default();
});
console.info("getA2E", getG());
最终 entry4.js
生成的代码如下所示
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _item_common_g_js__WEBPACK_IMPORTED_MODULE_1__ =
__webpack_require__(/*! ./item/common_____g.js */ \"./src/item/common_____g.js\");
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! voca */ \"./node_modules/voca/index.js\");
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0___default =
/*#__PURE__*/__webpack_require__.n(voca__WEBPACK_IMPORTED_MODULE_0__);
voca__WEBPACK_IMPORTED_MODULE_0___default().kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
__webpack_require__.e(/*! import() | B */ \"B\").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ \"./src/async/async_B.js\")).then(bModule=> {bModule.default();
});
console.info(\"getA2E\", (0,_item_common_g_js__WEBPACK_IMPORTED_MODULE_1__.getG)());
//# sourceURL=webpack://webpack-5-image/./src/entry4.js?
咱们接下来就要剖析
entry4.js
的每一句代码会造成什么dependency
,从而触发的template
的类型是什么,template.apply()
又做了什么?
import voca from 'voca';
造成HarmonyImportSideEffectDependency
,触发HarmonyImportSideEffectDependencyTemplate.apply()
在 HarmonyImportSideEffectDependencyTemplate.apply()
中,如代码所示,触发initFragments.push()
,其中content
=importStatement[0]
+ importStatement[1]
const importStatement = dep.getImportStatement(false, templateContext);
templateContext.initFragments.push(
new ConditionalInitFragment(importStatement[0] + importStatement[1],
InitFragment.STAGE_HARMONY_IMPORTS,
dep.sourceOrder,
key,
runtimeCondition
)
);
最终在拼接最终生成代码时,ConditionalInitFragment.content
也就是 importStatement[0]
+ importStatement[1]
的内容如下所示
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! voca */ "./node_modules/voca/index.js");
/* harmony import */ var voca__WEBPACK_IMPORTED_MODULE_0___default =
/*#__PURE__*/__webpack_require__.n(voca__WEBPACK_IMPORTED_MODULE_0__);
问题:咱们晓得,
initFragments.push()
做的事件是收集插入的代码,不是替换,那么原来的import voca from 'voca';
是如何删除的呢?
波及到 HarmonyImportSideEffectDependency
的增加相干逻辑,当 AST
解析失去 HarmonyImportSideEffectDependency
时,如上面代码所示,也会触发对应的 ConstDependency
的增加,即
module.addPresentationalDependency(clearDep)
module.addDependency(sideEffectDep)
// CompatibilityPlugin.js
parser.hooks.import.tap(
"HarmonyImportDependencyParserPlugin",
(statement, source) => {
parser.state.lastHarmonyImportOrder =
(parser.state.lastHarmonyImportOrder || 0) + 1;
const clearDep = new ConstDependency(parser.isAsiPosition(statement.range[0]) ? ";" : "",
statement.range
);
clearDep.loc = statement.loc;
parser.state.module.addPresentationalDependency(clearDep);
parser.unsetAsiPosition(statement.range[1]);
const assertions = getAssertions(statement);
const sideEffectDep = new HarmonyImportSideEffectDependency(
source,
parser.state.lastHarmonyImportOrder,
assertions
);
sideEffectDep.loc = statement.loc;
parser.state.module.addDependency(sideEffectDep);
return true;
}
);
因而在 entry4.js
这个 module
中,生成对应的HarmonyImportSideEffectDependency
,也会生成对应的ConstDependency
module.dependencies=[HarmonyImportSideEffectDependency("./item/common_____g.js"),
HarmonyImportSideEffectDependency("voca"),
HarmonyImportSpecifierDependency("./item/common_____g.js"),
HarmonyImportSpecifierDependency("voca")
]
module.presentationalDependencies=[HarmonyCompatibilityDependency, ConstDependency, ConstDependency]
ConstDependency
的作用就是记录对应的 import
语句的地位,而后应用 source.replace()
进行替换
source.replace(dep.range[0], dep.range[1] - 1, dep.expression)
在这个示例中,dep.expression
=""
,因而替换后为:
import {getG} from "./item/common_____g.js"; ===> ""
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
造成HarmonyImportSpecifierDependency
,触发HarmonyImportSpecifierDependencyTemplate.apply()
在 HarmonyImportSpecifierDependencyTemplate.apply()
中,如代码所示,触发initFragments.push()
,其中content
=importStatement[0]
+ importStatement[1]
class HarmonyImportSpecifierDependencyTemplate extends (HarmonyImportDependency.Template) {apply(dependency, source, templateContext, module) {const dep = /** @type {HarmonyImportSpecifierDependency} */ (dependency);
const {moduleGraph, runtime} = templateContext;
const ids = dep.getIds(moduleGraph);
const exportExpr = this._getCodeForIds(dep, source, templateContext, ids);
const range = dep.range;
if (dep.shorthand) {source.insert(range[1], `: ${exportExpr}`);
} else {source.replace(range[0], range[1] - 1, exportExpr);
}
}
}
source
实质就是 ReplaceSource
,insert()
是将要替换的代码的范畴以及内容都放在 _replacements
属性中
insert(pos, newValue, name) {this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
this._isSorted = false;
}
最终在拼接最终生成代码时,替换的数据为:
voca.kebabCase('goodbye blue sky') ===> "voca__WEBPACK_IMPORTED_MODULE_0___default().kebabCase"
同理,
import {getG} from "./item/common_____g.js";
和对应的调用办法的生成代码流程如下面voca
的剖析一样
import {getG} from "./item/common_____g.js";
↓
↓
↓
/* harmony import */ var _item_common_g_js__WEBPACK_IMPORTED_MODULE_1__ =
__webpack_require__(/*! ./item/common_____g.js */ \"./src/item/common_____g.js\");
getG() ===> "(0,_item_common_g_js__WEBPACK_IMPORTED_MODULE_1__.getG)"
当初咱们曾经处理完毕 module.dependencies
的所有依赖,接下里咱们要解决module.presentationalDependencies
module.dependencies=[HarmonyImportSideEffectDependency("./item/common_____g.js"),
HarmonyImportSideEffectDependency("voca"),
HarmonyImportSpecifierDependency("./item/common_____g.js"),
HarmonyImportSpecifierDependency("voca")
]
module.presentationalDependencies=[HarmonyCompatibilityDependency, ConstDependency, ConstDependency]
HarmonyCompatibilityDependency
是一个非凡的依赖,在 AST
解析的过程中,咱们就会判断 ast.body
是否蕴含
ImportDeclaration
ExportDefaultDeclaration
ExportNamedDeclaration
ExportAllDeclaration
如果蕴含,则module.addPresentationalDependency(HarmonyCompatibilityDependency)
// JavascriptParser.js
parse(source, state) {if (this.hooks.program.call(ast, comments) === undefined) {this.detectMode(ast.body);
this.preWalkStatements(ast.body);
this.prevStatement = undefined;
this.blockPreWalkStatements(ast.body);
this.prevStatement = undefined;
this.walkStatements(ast.body);
}
return state;
}
// HarmonyDetectionParserPlugin.js
parser.hooks.program.tap("HarmonyDetectionParserPlugin", ast => {
const isStrictHarmony = parser.state.module.type === "javascript/esm";
const isHarmony =
isStrictHarmony ||
ast.body.some(
statement =>
statement.type === "ImportDeclaration" ||
statement.type === "ExportDefaultDeclaration" ||
statement.type === "ExportNamedDeclaration" ||
statement.type === "ExportAllDeclaration"
);
if (isHarmony) {
const module = parser.state.module;
const compatDep = new HarmonyCompatibilityDependency();
module.addPresentationalDependency(compatDep);
}
});
而在 HarmonyExportDependencyTemplate.apply()
的解决也很简略,就是减少一个 new InitFragment()
数据到initFragments
const exportsInfo = moduleGraph.getExportsInfo(module);
if (exportsInfo.getReadOnlyExportInfo("__esModule").getUsed(runtime) !==
UsageState.Unused
) {
const content = runtimeTemplate.defineEsModuleFlagStatement({
exportsArgument: module.exportsArgument,
runtimeRequirements
});
initFragments.push(
new InitFragment(
content,
InitFragment.STAGE_HARMONY_EXPORTS,
0,
"harmony compatibility"
)
);
}
最终这个 InitFragment
造成的代码为:
__webpack_require__.r(__webpack_exports__);
sourceModule()
解决实现所有 module.dependency
和module.presentationalDependencies
之后,开始解决 module.blocks
异步依赖
sourceModule(module, initFragments, source, generateContext) {for (const dependency of module.dependencies) {this.sourceDependency();
}
for (const dependency of module.presentationalDependencies) {this.sourceDependency();
}
for (const childBlock of module.blocks) {this.sourceBlock()
}
}
sourceBlock(module, block, initFragments, source, generateContext) {for (const dependency of block.dependencies) {this.sourceDependency();
}
for (const childBlock of block.blocks) {this.sourceBlock();
}
}
从 entry4.js
的源码中能够晓得,存在着惟一一个异步依赖async_B.js
import {getG} from "./item/common_____g.js";
import voca from 'voca';
voca.kebabCase('goodbye blue sky'); // => 'goodbye-blue-sky'
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule=> {bModule.default();
});
console.info("getA2E", getG());
异步依赖 async_B.js
造成ImportDependency
,触发ImportDependencyTemplate.apply()
在 ImportDependencyTemplate.apply()
中,如代码所示,触发 source.replace
,source
实质就是 ReplaceSource
,replace()
是将要替换的代码的范畴以及内容都放在 _replacements
属性中
const dep = /** @type {ImportDependency} */ (dependency);
const block = /** @type {AsyncDependenciesBlock} */ (moduleGraph.getParentBlock(dep)
);
const content = runtimeTemplate.moduleNamespacePromise({
chunkGraph,
block: block,
module: moduleGraph.getModule(dep),
request: dep.request,
strict: module.buildMeta.strictHarmonyModule,
message: "import()",
runtimeRequirements
});
source.replace(dep.range[0], dep.range[1] - 1, content);
// ReplaceSource.js
replace(start, end, newValue, name) {this._replacements.push(new Replacement(start, end, newValue, name));
this._isSorted = false;
}
最终在拼接最终生成代码时,替换的数据为:
import (/*webpackChunkName: "B"*/"./async/async_B.js")
↓
↓
↓
__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js"))
createChunkAssets: module 代码合并打包成 chunk
2.1 概述
次要执行逻辑程序为:
this.hooks.renderManifest.call([], options)
触发,获取render()
对象,此时还没生成代码source = fileManifest.render()
,触发render()
进行代码渲染拼凑this.emitAsset(file, source, assetInfo)
将代码写入到文件中
seal() {
//... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
// ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
// 为 module 设置深度标记
this.assignDepths(entryModules);
buildChunkGraph(this, chunkGraphInit);
this.createModuleHashes();
this.codeGeneration(err => {this.createChunkAssets(err => {});
});
}
function createChunkAssets(callback) {
asyncLib.forEachLimit(
this.chunks,
(chunk, callback) => {// this.getRenderManifest=this.hooks.renderManifest.call([], options);
let manifest = this.getRenderManifest({
chunk,
...
});
//... 遍历 manifest,调用(manifest[i]=fileManifest)fileManifest.render()
asyncLib.forEach(
manifest,
(fileManifest, callback) => {
//....
source = fileManifest.render();
this.emitAsset(file, source, assetInfo);
}
);
}
)
}
2.2 manifest=this.getRenderManifest
manifest=this.hooks.renderManifest.call([], options)
跟 parser
、generator
、dependencyTemplates
相似,这里也采纳 hooks.renderManifest.tap
注册监听,依据传入的 chunk
中不同类型的局部,比方
- 触发
JavascriptModulesPlugin.js
解决chunk
中的js
局部 - 触发
CssModulePlugins.js
解决chunk
中的css
局部
2.3 source = manifest[i].render()
由下面的剖析能够晓得,会依据 chunk
中有多少类型数据而采纳不同的 Plugin
进行解决,其中最常见的就是 JavascriptModulesPlugin
的解决,上面咱们应用 JavascriptModulesPlugin
看下整体的 render()
流程
2.3.1 source = JavascriptModulesPlugin.render()
依据不同的状态,进行 render()
办法内容的调用
chunk.hasRuntime()
->renderMain()
办法chunk.renderChunk()
->renderChunk()
办法
// JavascriptModulesPlugin
compilation.hooks.renderManifest.tap(
"JavascriptModulesPlugin",
(result, options) => {if (hotUpdateChunk) {render = () => this.renderChunk(...);
} else if (chunk.hasRuntime()) {render = () => this.renderMain(...);
} else {if (!chunkHasJs(chunk, chunkGraph)) {return result;}
render = () => this.renderChunk();
}
result.push({
render,
filenameTemplate,
pathOptions: {
hash,
runtime: chunk.runtime,
chunk,
contentHashType: "javascript"
},
...
});
return result;
}
)
2.3.2 renderMain(): 入口 entry 类型 chunk 触发代码生成
问题
mode: "development"
和mode: "production"
两种模式有什么区别?- 为什么
renderMain()
的内容是对应mode: "development"
的?mode:production
是有另外的分支解决还是在renderMain()
根底上进行革新呢?
整体流程图
上面会针对该流程图进行具体的文字描述剖析
打包产物详细分析
从下面所打包生成的代码,咱们能够总结进去打包产物为
名称 | 解释 | 示例 |
---|---|---|
webpackBootstrap | 最外层包裹的立刻执行函数 | (function(){}) |
webpack_modules | 所有 module 的代码,包含 |
|
webpack_module_cache | 对加载过的 module 进行缓存,如果曾经缓存过,下一次加载就不执行上面 __webpack_require__() 办法 |
var __webpack_module_cache__ = {}; |
function __webpack_require__(moduleId) | 通过 moduleId 进行模块的加载 |
|
__webpack_require__.m | 蕴含所有 module 的对象的别名 |
__webpack_require__.m = __webpack_modules__ |
runtime 各种工具函数 | 各种 __webpack_require__.xxx 的办法,提供各种能力,比方 __webpack_require__.O 提供 chunk loaded 的能力 |
无 |
Load entry module | 加载入口文件,如果入口文件须要依赖其它chunk ,则提早加载入口文件 |
无 |
整体流程图文字描述
依照指定程序合并代码
- 合并立刻执行函数的最头部的代码,一个
(()=> {
- 合并所有
module
的代码 - 合并
__webpack_module_cache__ = {};function __webpack_require__(moduleId)
等加载代码 - 合并
runtime
模块代码 - 合并
"Load entry module and return exports"
等代码,即立刻执行函数中的执行入口文件解析的代码局部,被动触发入口文件的加载 - 合并立刻执行函数的最初最初的代码,即
})()
因为计算代码和合并代码逻辑揉杂在一起,因而调整了上面代码程序,加强可读性
renderMain(renderContext, hooks, compilation) {let source = new ConcatSource();
const iife = runtimeTemplate.isIIFE();
// 1. 计算出 "function __webpack_require__(moduleId)" 等代码
const bootstrap = this.renderBootstrap(renderContext, hooks);
// 2. 计算出所有 module 的代码
const chunkModules = Template.renderChunkModules(
chunkRenderContext,
inlinedModules
? allModules.filter(m => !inlinedModules.has(m))
: allModules,
module => this.renderModule(module, chunkRenderContext, hooks, true),
prefix
);
// 3. 计算出运行时的代码
const runtimeModules =
renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk);
// 1. 合并立刻执行函数的最头部的代码,一个(()=> {if (iife) {if (runtimeTemplate.supportsArrowFunction()) {source.add("/******/ (() => {// webpackBootstrap\n");
} else {source.add("/******/ (function() {// webpackBootstrap\n");
}
prefix = "/******/ \t";
} else {prefix = "/******/";}
// 2. 合并所有 module 的代码
if (
chunkModules ||
runtimeRequirements.has(RuntimeGlobals.moduleFactories) ||
runtimeRequirements.has(RuntimeGlobals.moduleFactoriesAddOnly) ||
runtimeRequirements.has(RuntimeGlobals.require)
) {source.add(prefix + "var __webpack_modules__ = (");
source.add(chunkModules || "{}");
source.add(");\n");
source.add("/************************************************************************/\n");
}
// 3. 合并 "__webpack_module_cache__ = {};function __webpack_require__(moduleId)" 等加载代码
if (bootstrap.header.length > 0) {const header = Template.asString(bootstrap.header) + "\n";
source.add(
new PrefixSource(
prefix,
useSourceMap
? new OriginalSource(header, "webpack/bootstrap")
: new RawSource(header)
)
);
source.add("/************************************************************************/\n");
}
// 4. 合并 runtime 模块代码
if (runtimeModules.length > 0) {
source.add(
new PrefixSource(
prefix,
Template.renderRuntimeModules(runtimeModules, chunkRenderContext)
)
);
source.add("/************************************************************************/\n");
// runtimeRuntimeModules calls codeGeneration
for (const module of runtimeModules) {compilation.codeGeneratedModules.add(module);
}
}
// 5. 合并 "Load entry module and return exports" 等代码,即立刻执行函数中的执行入口文件解析的代码局部
// 被动触发入口文件的加载
source.add(
new PrefixSource(
prefix,
new ConcatSource(toSource(bootstrap.beforeStartup, "webpack/before-startup"),
"\n",
hooks.renderStartup.call(toSource(bootstrap.startup.concat(""),"webpack/startup"),
lastEntryModule,
{
...renderContext,
inlined: false
}
),
toSource(bootstrap.afterStartup, "webpack/after-startup"),
"\n"
)
)
);
// 6. 合并立刻执行函数的最初最初的代码
if (iife) {source.add("/******/})()\n");
}
}
Template.renderChunkModules
在
renderMain()
的流程中,次要还是调用了Template.renderChunkModules()
进行该chunk
波及到module
的渲染,上面咱们将简略剖析下该办法
传入该 chunk
蕴含的所有 modules
以及对应的 renderModule()
函数,返回所有 module
渲染的source
在不同的 Plugin
中书写 renderModule()
函数,应用 Template.renderChunkModules()
静态方法调用 renderModule()
函数生成所有 modules
的source
,实质还是利用 codeGeneration()
中生成的 codeGenerationResults
获取对应 module
的source
留神!要渲染的
modules
会依照identifier
进行排序渲染
2.3.3 renderChunk(): 非入口 entry 类型 chunk 触发代码生成
异步 chunk、合乎 splitChunkPlugin 的 cacheGroup 规定的 chunk
实质 renderChunk()
次要也是调用 Template.renderChunkModules()
进行所有 module
相干代码的生成
Template.renderChunkModules()
办法曾经在下面renderMain()
中剖析,这里不再赘述
而后拼接一些字符串造成最终的bundle
renderChunk(renderContext, hooks) {const { chunk, chunkGraph} = renderContext;
const modules = chunkGraph.getOrderedChunkModulesIterableBySourceType(
chunk,
"javascript",
compareModulesByIdentifier
);
const allModules = modules ? Array.from(modules) : [];
const chunkRenderContext = {
...renderContext,
chunkInitFragments: [],
strictMode: allStrict
};
const moduleSources =
Template.renderChunkModules(chunkRenderContext, allModules, module =>
this.renderModule(module, chunkRenderContext, hooks, true)
) || new RawSource("{}");
let source = tryRunOrWebpackError(() => hooks.renderChunk.call(moduleSources, chunkRenderContext),
"JavascriptModulesPlugin.getCompilationHooks().renderChunk");
//... 省略对 source 的优化解决
chunk.rendered = true;
return strictHeader
? new ConcatSource(strictHeader, source, ";")
: renderContext.runtimeTemplate.isModule()
? source
: new ConcatSource(source, ";");
}
Compilation.emitAsset(file, source, assetInfo)
// node_modules/webpack/lib/Compilation.js
seal() {
//... 依据 entry 初始化 chunk 和 chunkGroup,关联 chunk 和 chunkGroup
// ... 遍历 entry 所有的 dependencies,关联 chunk、dependencies、chunkGroup
// 为 module 设置深度标记
this.assignDepths(entryModules);
buildChunkGraph(this, chunkGraphInit);
this.createModuleHashes();
this.codeGeneration(err => {this.createChunkAssets(err => {});
});
}
function createChunkAssets(callback) {
asyncLib.forEachLimit(
this.chunks,
(chunk, callback) => {
let manifest = this.getRenderManifest({
chunk,
...
});
// manifest=this.hooks.renderManifest.call([], options);
asyncLib.forEach(
manifest,
(fileManifest, callback) => {
//....
source = fileManifest.render();
this.emitAsset(file, source, assetInfo);
}
);
}
)
}
将生成代码赋值到 assets 对象上,筹备写入文件,而后调用 callback
,完结compliation.seal()
流程
// node_modules/webpack/lib/Compilation.js
createChunkAssets(callback) {this.assets[file] = source;
}
Compiler.emitAsset
从上面代码能够晓得,compilation.seal
完结时会调用 onCompiled()
办法,从而触发 this.emitAssets()
办法
// node_modules/webpack/lib/Compiler.js
run(callback) {
//....
const onCompiled = (err, compilation) => {
this.emitAssets(compilation, err => {logger.time("emitRecords");
this.emitRecords(err => {logger.time("done hook");
const stats = new Stats(compilation);
this.hooks.done.callAsync(stats, err => {logger.timeEnd("done hook");
});
});
});
};
this.compile(onCompiled);
}
compile(callback) {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {return callback(null, compilation);
});
});
}
mkdirp()
是 webpack
封装的一个办法,实质还是调用 fs.mkdir(p, err => {})
异步地创立目录,而后调用 emitFiles()
办法
function emitAssets() {outputPath = compilation.getPath(this.outputPath, {});
mkdirp(this.outputFileSystem, outputPath, emitFiles);
}
而 emitFiles()
实质也是遍历 Compliation.emitAsset()
生成的资源文件 this.assets[file]
,而后从source
失去 binary (Buffer)
内容输入生成文件
const emitFiles = err => {const assets = compilation.getAssets();
asyncLib.forEachLimit(
assets,
15,
({name: file, source, info}, callback) => {
let targetFile = file;
if (targetFile.match(/\/|\\/)) {
const fs = this.outputFileSystem;
const dir = dirname(fs, join(fs, outputPath, targetFile));
mkdirp(fs, dir, writeOut);
} else {writeOut();
}
}
)
}
const writeOut = err => {
const targetPath = join(
this.outputFileSystem,
outputPath,
targetFile
);
// 从 source 失去 binary (Buffer)内容
const getContent = () => {if (typeof source.buffer === "function") {return source.buffer();
} else {const bufferOrString = source.source();
if (Buffer.isBuffer(bufferOrString)) {return bufferOrString;} else {return Buffer.from(bufferOrString, "utf8");
}
}
};
const doWrite = content => {this.outputFileSystem.writeFile(targetPath, content, err => {});
};
const processMissingFile = () => {const content = getContent();
//...
return doWrite(content);
};
processMissingFile();};
其它知识点
6.1 runtime 代码的品种和作用
摘录自 https://webpack.docschina.org/concepts/manifest#runtime
runtime,以及随同的 manifest 数据,次要是指:在浏览器运行过程中,webpack 用来连贯模块化应用程序所需的所有代码。它蕴含:在模块交互时,连贯模块所需的加载和解析逻辑。包含:曾经加载到浏览器中的连贯模块逻辑,以及尚未加载模块的提早加载逻辑。
runtime 蕴含的函数 | 作用 |
---|---|
__webpack_require__.o | 工具函数,判断是否有某属性 |
__webpack_require__.d | 对应 exports,用来定义导出变量对象:Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }) |
__webpack_require__.r | 辨别是否是 es 模块,给导出导出变量对象增加 __esModule:true 属性 |
__webpack_require__.l | 工具函数,动态创建 script 标签去加载 js,比方在热更新 HRM 中用来加载main.xxx.hot-update.js |
__webpack_require__.O | chunk loaded |
__webpack_require__.m | 蕴含所有 module 的对象的别名,__webpack_require__.m = __webpack_modules__ |
如果 entry Module
蕴含了异步加载的 import
逻辑,那么合并的 runtime
代码还会生成一些异步的办法
runtime 蕴含的函数 | 作用 |
---|---|
__webpack_require__.e | 加载异步 chunk 的入口办法,外部应用__webpack_require__.f 进行 chunk 的加载 |
__webpack_require__.f | 异步加载的对象,下面能够挂载一些异步加载的具体方法 |
__webpack_require__.f.j | 具体的 chunk 的加载办法,封装 promise ,检测是否有缓存installedChunkData ,而后应用多种办法返回后果,最终应用__webpack_require__.l 加载对应的 js |
__webpack_require__.g | 获取全局 this |
__webpack_require__.p | 获取 publicPath ,首先尝试从document 中获取 publicPath ,如果获取不到,咱们须要在output.publicPath /__webpack_public_path__ 被动申明publicPath |
__webpack_require__.u | 传入chunkId ,返回[chunkId].js ,拼接后缀 |
__webpack_require__.l | 工具函数,动态创建 script 标签去加载 js |
runtime 蕴含的办法具体的联动逻辑请看上面的剖析
6.2 webpack 如何解决循环依赖
同步 import、异步 import、require、export {xxx}、export default 等 5 种状况剖析
6.2.1 ES6 和 CommonJS 的区别
参考自阮一峰 -Node.js 如何解决 ES6 模块和 ES6 模块和 CommonJS 模块有哪些差别?
CommonJS | ES6 | |
---|---|---|
语法 | 应用 require 导入和 module.exports(exports) 导出 |
应用 import 导入和 export 导出 |
用法 | 运行时加载:只有代码遇到 require 指令时,才会去执行模块中的代码,如果曾经 require 一个模块,就要期待它执行结束后能力执行前面的代码 |
编译时输入接口:编译时就能够确定模块的依赖关系,编译时遇到 import 不会去执行模块,只会生成一个援用,等到真正须要时,才会到模块中进行取值 |
输入值 | CommonJS 模块输入的是一个值的复制 CommonJS 输入的是 module.exports={xx} 对象,如果这个对象蕴含根本类型,module 外部扭转了这个根本类型,输入的值不会受到影响,因而一个 Object 的根本类型是拷贝的,而不是援用,如果 Object 蕴含援用类型,如Array ,则会影响内部援用的值 |
ES6 模块输入的是值的援用,输入的是一个只读援用,等到脚本真正执行时,再依据这个只读援用,到被加载的那个模块外面去取值,因而 ES6 模块外部的任何变动,包含根本类型和援用类型的变动,都会导致内部援用的值发生变化。如果是 export default {xxx},因为导出的是一个对象,变动规定跟 CommonJS 一样,根本类型是值的拷贝,援用类型是内存地址的拷贝 |
6.2.2 具体例子剖析
加深对打包产物整体运行流程的了解
ES6 模式下 import 的循环援用
如上面代码所示,咱们在入口文件中 import ./es_a.js
,而后在es_a.js
中import ./es_b.js
,在 es_.js
中又import ./es_a.js
import {a_Value} from "./es_a";
console.error("筹备开始 es_entry1", a_Value);
咱们在下面比拟中说了 ES6
模块输入的是值的援用,输入的是一个只读援用,等到脚本真正执行时,再依据这个只读援用,到被加载的那个模块外面去取值
因而在看 webpack
打包文件时,咱们就能够直接判断出应该打印出的程序为:
- 一开始
import
会晋升到顶部,进行动态剖析 - 在
es_a.js
中,咱们最开始调用b_Value
,因而这个时候咱们会去找es_b
模块是否曾经加载,如果没有加载则创立对应的模块,而后进入es_b
模块,试图去寻找export
语句 - 在
es_b.js
中,咱们最开始调用a_Value
,咱们会寻找es_a
模块是否曾经加载,目前曾经加载了,然而因为还没执行结束es_a.js
的全部内容就进入es_b
模块,因而此时a_Value
=undefined
,而后export var b_Value
,继续执行结束es_b
模块剩下的内容 - 此时从
es_b.js
->es_a.js
,咱们拿到了b_Value
,将b_Value
打印进去,而后再export var a_Value
- 500 毫秒后,
es_b.js
定时器触发,去es_a
模块寻找对应的a_Value
,此时a_Value
="我是一开始的 a.js"
,并且扭转了b_Value
的值 - 1000 毫秒后,
es_a.js
定时器触发,去es_b
模块寻找对应的b_Value
,此时a_Value
="我是完结后扭转的 b.js"
咱们再依据 webpack
打包的后果查看打印的信息(上面的截图),跟咱们的推断一样
那
webpack
是如何产生这种成果的呢?
咱们间接查看 webpack
打包的产物,咱们能够发现
__webpack_require__.r
:标记为ESModule
__webpack_require__.d
:将export
晋升到最顶部,而后申明对应的key
以及对应的function
__webpack_require__(xxxx)
:这个时候才进行import
的解决- 残余代码:文件内容的代码逻辑
从上面两个代码块,咱们就能够很好解释下面打印信息的前后程序了,一开始
"./src/es_a.js"
:申明了对应的harmony export
的key
,而后__webpack_require__("./src/es_b.js")
调用es_b
模块,进入"./src/es_b.js"
"./src/es_b.js"
:申明了对应的harmony export
的key
,而后__webpack_require__("./src/es_a.js")
,此时_es_a_js__WEBPACK_IMPORTED_MODULE_0__
存在!然而a_Value
还没赋值,因为var a_Value
会进行变量晋升,因而拿到的a_Value
=undefined
,执行结束es_b.js
剩下的代码,此时b_Value
曾经赋值es_b.js
->es_a.js
,打印出对应的_es_b_js__WEBPACK_IMPORTED_MODULE_0__.b_Value
,而后赋值a_Value
- 在接下来的两个
setTimeout()
中,因为都是去__webpack_require__.d
获取对应key
的function()
,因而能够实时拿到变动的值
"./src/es_a.js":
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);
/* harmony export */
__webpack_require__.d(__webpack_exports__, {/* harmony export */ "a_Value": function () {return /* binding */ a_Value;}
/* harmony export */
});
/* harmony import */
var _es_b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es_b.js */ "./src/es_b.js");
console.info("a.js 开始运行");
console.warn("在 a.js 中 import 失去 b", _es_b_js__WEBPACK_IMPORTED_MODULE_0__.b_Value);
var a_Value = "我是一开始的 a.js";
console.info("a.js 完结运行");
setTimeout(() => {console.warn("在 a.js 完结运行后 (b.js 那边曾经在 500 毫秒前扭转值) 再次 import 失去 b",
_es_b_js__WEBPACK_IMPORTED_MODULE_0__.b_Value);
}, 1000);
})
"./src/es_b.js":
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {__webpack_require__.r(__webpack_exports__);
/* harmony export */
__webpack_require__.d(__webpack_exports__, {/* harmony export */ "b_Value": function () {return /* binding */ b_Value;}
/* harmony export */
});
/* harmony import */
var _es_a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./es_a.js */ "./src/es_a.js");
console.info("bbbbbbb.js 开始运行");
console.warn("在 bbbbbbb.js 中 import 失去 a", _es_a_js__WEBPACK_IMPORTED_MODULE_0__.a_Value);
var b_Value = "我是一开始的 b.js";
console.info("b.js 完结运行");
setTimeout(() => {
b_Value = "我是完结后扭转的 b.js";
console.warn("在 b.js 中提早后再次 import 失去 a", _es_a_js__WEBPACK_IMPORTED_MODULE_0__.a_Value);
}, 500);
})
CommonJS 模式下 require 的循环援用
console.error("筹备开始 entry");
const moduleA = require("./a.js");
const moduleB = require("./b.js");
console.error("entry 最初拿到的值是", moduleA.a_Value);
console.error("entry 最初拿到的值是", moduleB.b_Value);
具体的示例代码如上所示,它所生成的 webpack
代码如下所示,整体流程是比较简单的,会应用 module.exports
存储要裸露进来的数据,如果加载过一次,则会存入缓存中,因而下面示例代码整体步骤为
entry2.js
:触发require("./a.js")
,这个时候因为a
模块还没创立,因而会进入到a
模块中a.js
:先裸露了一个exports.a_Value
数据,而后触发require("./b.js")
,这个时候因为b
模块还没创立,因而会进入到b
模块中b.js
:进入b.js
后,会遇到require("./a.js")
,这个时候a
模块曾经创立,存入到__webpack_module_cache__["a"]
中,因而会间接从缓存中读取数据,此时读到了它的module.exports.a_Value
,显示进去后,又扭转了b
模块的module.exports.b_Value
数据a.js
:回到a.js
后,将失去的b
模块的module.exports.b_Value
打印进去,而后扭转本身的一个变量module.exports.a_Value
数据entry2.js
:a.js
->b.js
->a.js
后回到entry2.js
,将a
模块的一个变量module.exports.a_Value
和b
模块的一个变量module.exports.b_Value
打印进去
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {return cachedModule.exports;}
var module = __webpack_module_cache__[moduleId] = {exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_modules__ = ({
/***/ "./src/CommonJS/a.js":
/***/ (function (__unused_webpack_module, exports, __webpack_require__) {console.info("a.js 开始运行");
var a_Value = "我是一开始的 a.js";
exports.a_Value = a_Value;
const bModule = __webpack_require__(/*! ./b.js */ "./src/CommonJS/b.js");
console.warn("在 a.js 中 require 失去 bModule",
bModule.b_Value);
a_Value = "我是后扭转的 a.js!!!!!";
exports.a_Value = a_Value;
console.info("a.js 完结运行");
/***/
}),
/***/ "./src/CommonJS/b.js":
/***/ (function (__unused_webpack_module, exports, __webpack_require__) {console.info("bbbbbbb.js 开始运行");
var b_Value = "我是一开始的 b.js";
exports.b_Value = b_Value;
const aModule = __webpack_require__(/*! ./a.js */ "./src/CommonJS/a.js");
console.warn("在 bbbbbbb.js 中 require 失去 a",
aModule.a_Value);
b_Value = "我是后扭转的 b.js";
exports.b_Value = b_Value;
console.info("b.js 完结运行");
/***/
})
/******/
});
下面流程运行后果如下所示,因为整体流程都围绕模块的 module.exports.xxx
变量开展,比较简单,这里不再着重剖析
6.3 chunk 是如何实现与其它 chunk 的联动
6.3.1 入口文件有其它 chunk
的同步依赖
须要先加载其它
chunk
再进行入口文件其它代码的执行,常见于应用node_modules
的第三方库,触发splitChunks
的默认cacheGroups
打包造成一个chunk
在下面 renderMain()
的示例代码中,咱们的示例代码如下所示
import getE from "./item/entry1_a.js";
import {getG} from "./item/common_____g.js";
import _ from "loadsh";
console.info(_.add(13, 24));
var testvalue = getE() + getG();
import (/*webpackChunkName: "B"*/"./async/async_B.js").then(bModule => {bModule.default();
});
setTimeout(() => {const requireA = require("./require/require_A.js");
console.info("testvalue", testvalue + requireA.getRequireA());
}, 4000);
最终生成的 chunk
有
app4.js
:入口造成的Chunk
B.js
:异步 import
造成的Chunk
C.js
:异步 import
造成的Chunk
vendors-node_modules_loadsh_lodash_js.js
:命中 node_modules
而造成的Chunk
从上图咱们能够晓得,咱们在加载入口文件时,咱们应该须要先加载结束vendors-node_modules_loadsh_lodash_js.js
,因为在源码中,咱们是同步的,得先有loadsh
,而后执行入口文件的其它内容
而从生成的代码中(如下所示),咱们也能够发现的确如此,从生成的正文能够晓得,如果咱们的 entry module
依赖其它 chunk
,那么我就得应用__webpack_require__.O
提早加载入口文件./src/entry4.js
// This entry module depends on other loaded chunks and execution need to be delayed
var __webpack_exports__ = __webpack_require__.O(undefined,
["vendors-node_modules_loadsh_lodash_js"],
function () { return __webpack_require__("./src/entry4.js"); })
__webpack_exports__ = __webpack_require__.O(__webpack_exports__);
那 __webpack_require__.O
是如何判断的呢?
在调用第一次 __webpack_require__.O
时,会传入 chunkIds
,而后会将前置加载的chunk
数组存储到 deferred
中
!function () {var deferred = [];
__webpack_require__.O = function (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;
}
//...
return result;
};
}();
等到第二次调用__webpack_exports__ = __webpack_require__.O(__webpack_exports__)
,会触发一个遍历逻辑,也就是上面代码块所正文的那一行代码:
return __webpack_require__.O[key](chunkIds[j])
- ==>
return __webpack_require__.O.j(chunkIds[j])
- ==>
ƒ (chunkId) {return installedChunks[chunkId] === 0; }
理论就是检测是否曾经加载 deferred
存储的 chunks
,如果加载了,能力触发fn()
,也就是"./src/entry4.js"
入口文件的加载
!function () {var deferred = [];
__webpack_require__.O = function (result, chunkIds, fn, priority) {if (chunkIds) {
//...
return;
}
//...
for (var i = 0; i < deferred.length; i++) {var chunkIds = deferred[i][0];
var fn = deferred[i][1];
//...
var fulfilled = true;
for (var j = 0; j < chunkIds.length; j++) {//Object.keys(__webpack_require__.O).every(function (key) {return __webpack_require__.O[key](chunkIds[j]); })
if ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function (key) {return __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();
if (r !== undefined) result = r;
}
}
return result;
};
}();
因而,如果有申明 index.html
,webpack
会帮咱们造成上面的 html
文件,先加载同步依赖的js
,再加载对应的入口文件js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>
<script src="./build-dev/vendors-node_modules_loadsh_lodash_js.js"></script>
<script src="./build-dev/app4.js"></script>
</body>
</html>
6.3.2 入口文件有其它 chunk
的异步依赖
异步 chunk
会触发 __webpack_require__.e
进行解决,外部也是调用 __webpack_require__.f.j
->__webpack_require__.l
加载对应的[chunk].js
__webpack_require__.e = function (chunkId) {return Promise.all(Object.keys(__webpack_require__.f).reduce(function (promises, key) {__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js")
).then(bModule => {bModule.default();
});
从上面代码能够晓得,异步 ChunkB
返回的是一个立刻执行函数,会执行 self["webpackChunkwebpack_5_image"].push
,其实就是chunkLoadingGlobal.push
,会触发对应的__webpack_require__.m[moduleId]
实例化对应的数据
// B.js
(self["webpackChunkwebpack_5_image"] = self["webpackChunkwebpack_5_image"] || []).push([["B"], {
"./src/async/async_B.js":
(function (__unused_webpack_module, __webpack_exports__, __webpack_require__) {eval("...")
})
}]);
// app4.js
var chunkLoadingGlobal = self["webpackChunkwebpack_5_image"] = self["webpackChunkwebpack_5_image"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));// 在异步 chunk 在 chunkLoadingGlobal 初始化之前曾经塞入数据
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
var webpackJsonpCallback = function (parentChunkLoadingFunction, data) {
//...
var moreModules = data[1];
//...
if (chunkIds.some(function (id) {return installedChunks[id] !== 0; })) {for (moduleId in moreModules) {if (__webpack_require__.o(moreModules, moduleId)) {__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
}
for (; i < chunkIds.length; i++) {chunkId = chunkIds[i];
if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {installedChunks[chunkId][0]();}
installedChunks[chunkId] = 0;
}
//...
}
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal))
传入的第二个参数会作为初始参数跟起初调用的参数进行合并,因而parentChunkLoadingFunction
=chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
,而data
就是B.js
中push
的数据
webpackJsonpCallback()
加载实现对应的 chunk.js
后,会将 chunk.js
的内容存储到对应的 __webpack_require__.m[moduleId]
中,而后调用installedChunks[chunkId][0]()
installedChunks[chunkId][0]()
是什么呢?在之前的 __webpack_require__.f.j()
中,咱们注册了对应的installedChunks[chunkId] = [resolve, reject];
__webpack_require__.f.j = function (chunkId, promises) {
// ....
var promise = new Promise(function (resolve, reject) {installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
};
这个时候咱们又能够回到一开始的 __webpack_require__.e
,__webpack_require__.f
加载对应的 chunk.js
后,会扭转 promises
的状态,从而触发 __webpack_require__.bind(__webpack_require__, "./src/async/async_B.js")
的执行,而此时的 "./src/async/async_B.js"
曾经在之前的 jsonp
申请中存入到 __webpack_require__.m[moduleId]
,因而这里就能够间接获取对应的内容实现异步chunk
的调用
__webpack_require__.e = function (chunkId) {return Promise.all(Object.keys(__webpack_require__.f).reduce(function (promises, key) {__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js")
).then(bModule => {bModule.default();
});
6.3.3 其它细小知识点
而在 webpack
官网中,咱们从 https://webpack.docschina.org/concepts/manifest/ 能够晓得
一旦你的利用在浏览器中以 index.html 文件的模式被关上,一些 bundle 和利用须要的各种资源都须要用某种形式被加载与链接起来。在通过打包、压缩、为提早加载而拆分为细小的 chunk 这些 webpack 优化 之后,你精心安排的 /src 目录的文件构造都曾经不再存在。所以 webpack 如何治理所有所需模块之间的交互呢?
当 compiler
开始执行、解析和映射应用程序时,它会保留所有模块的具体要点。这个数据汇合称为 "manifest"
当实现打包并发送到浏览器时,runtime
会通过 manifest
来解析和加载模块,也就是说在浏览器运行时,runtime
是 webpack
用来连贯模块化的应用程序的所有代码
runtime
蕴含:在模块交互时,连贯模块所需的加载和解析逻辑
runtime
包含:浏览器中的已加载模块的连贯,以及懒加载模块的执行逻辑
6.4 runtime 代码与 module、chunk 的关联
6.4.1 计算:runtimeRequirements
的初始化
runtime
蕴含很多工具办法,一个Chunk
怎么晓得它须要什么工具办法?比方一个Chunk
只有同步,没有异步,天然不会生成异步runtime
的代码
在下面 HarmonyImportDependency.Template
的剖析咱们能够晓得,生成代码时,会触发dep.getImportStatement()
,理论就是RuntimeTemplate.importStatement()
//node_modules/webpack/lib/dependencies/HarmonyImportDependency.js
HarmonyImportDependency.Template = class HarmonyImportDependencyTemplate extends (ModuleDependency.Template) {apply(dependency, source, templateContext) {const importStatement = dep.getImportStatement(false, templateContext);
//...
}
}
getImportStatement() {
return runtimeTemplate.importStatement({
update,
module: moduleGraph.getModule(this),
chunkGraph,
importVar: this.getImportVar(moduleGraph),
request: this.request,
originModule: module,
runtimeRequirements
});
}
在 RuntimeTemplate.importStatement()
中,会生成理论的代码,比方 /* harmony import */
、__webpack_require__(xxxx)
等等
// node_modules/webpack/lib/RuntimeTemplate.js
importStatement() {
const optDeclaration = update ? "":"var ";
const exportsType = module.getExportsType(
chunkGraph.moduleGraph,
originModule.buildMeta.strictHarmonyModule
);
runtimeRequirements.add(RuntimeGlobals.require);
const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`;
if (exportsType === "dynamic") {runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport);
return [
importContent,
`/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n`
];
}
return [importContent, ""];
}
下面代码对应的实质就是 webpack
生成具体 module
代码时,entry4.js
中外部 import
的替换语句
在下面转化语句的流程中,咱们也将对应的 runtime
模块须要的代码放入到 runtimeRequirements
中,比方
runtimeRequirements=["__webpack_require__", "__webpack_require__.n"]
下面的剖析是同步的 /* harmony import */
流程剖析,那如果是异步的 import
呢?
这里咱们能够应用倒推法,咱们能够发现,实质
runtimeRequirements.push()
都是RuntimeGlobals
这个变量的值,所以咱们能够从下面的打包产物中,找到异步所须要的runtime
办法:__webpack_require__.e
,就能够轻易找到对应的变量为RuntimeGlobals.ensureChunk
,而后就能够轻易找到对应的代码所在位置,进行剖析
因而咱们能够很轻松明确整个异步 import
计算 runtime
依赖的流程,通过 sourceBlock()
->sourceDependency()
-> 触发对应的ImportDependency
对应的ImportDependency.Template
从而触发 runtimeTemplate.moduleNamespacePromise()
实现代码的转化以及 runtimeRequirements
的计算
6.4.2 依赖收集:runtimeRequirements 和 module
在经验 module.codeGeneration
生成模块代码,并且顺便创立 runtimeRequirements
的流程后,会调用 processRuntimeRequirements()
进行 runtimeRequirements
的解决,将数据放入到 chunkGraph
中
this.codeGeneration(err => {
//...module.codeGeneration
this.processRuntimeRequirements();}
processRuntimeRequirements() {for(const module of modules) {for (const runtime of chunkGraph.getModuleRuntimes(module)) {
const runtimeRequirements =
codeGenerationResults.getRuntimeRequirements(module, runtime);
if (runtimeRequirements && runtimeRequirements.size > 0) {set = new Set(runtimeRequirements);
}
chunkGraph.addModuleRuntimeRequirements(module, runtime, set);
}
}
//...
}
6.4.3 依赖收集:runtimeRequirements 和 chunk
以 chunk
为单位,遍历所有该 chunk
中蕴含的 module
所依赖的 runtimeRequirements
,而后应用const set=new Set()
进行去重,最终将 chunk
和runtimeRequirements
的关系放入到 chunkGraph
中
this.hooks.additionalChunkRuntimeRequirements.call(chunk, set, context)
触发的逻辑:判断是否须要为set
汇合数据增加对应的item
processRuntimeRequirements() {
//... 解决 module 和 runtimeRequirements
for (const chunk of chunks) {const set = new Set();
for (const module of chunkGraph.getChunkModulesIterable(chunk)) {
const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements(
module,
chunk.runtime
);
for (const r of runtimeRequirements) set.add(r);
}
this.hooks.additionalChunkRuntimeRequirements.call(chunk, set, context);
for (const r of set) {this.hooks.runtimeRequirementInChunk.for(r).call(chunk, set, context);
}
chunkGraph.addChunkRuntimeRequirements(chunk, set);
}
}
6.4.4 依赖收集:runtimeRequirements
和chunkGraphEntries
processRuntimeRequirements() {
//... 解决 module 和 runtimeRequirements
//... 解决 chunk 和 runtimeRequirements
for (const treeEntry of chunkGraphEntries) {const set = new Set();
for (const chunk of treeEntry.getAllReferencedChunks()) {
const runtimeRequirements =
chunkGraph.getChunkRuntimeRequirements(chunk);
for (const r of runtimeRequirements) set.add(r);
}
this.hooks.additionalTreeRuntimeRequirements.call(
treeEntry,
set,
context
);
for (const r of set) {
this.hooks.runtimeRequirementInTree
.for(r)
.call(treeEntry, set, context);
}
chunkGraph.addTreeRuntimeRequirements(treeEntry, set);
}
}
chunkGraphEntries
是什么?
chunkGraphEntries = this._getChunkGraphEntries()
获取所有同步 entry
和异步 entry
所具备的 runtimeChunk
,而后都放入到treeEntries
这个汇合中去
webpack5
容许将runtime
代码剥离进去造成runtimeChunk
,而后提供给多个chunk
一起应用
_getChunkGraphEntries() {/** @type {Set<Chunk>} */
const treeEntries = new Set();
for (const ep of this.entrypoints.values()) {const chunk = ep.getRuntimeChunk();
if (chunk) treeEntries.add(chunk);
}
for (const ep of this.asyncEntrypoints) {const chunk = ep.getRuntimeChunk();
if (chunk) treeEntries.add(chunk);
}
return treeEntries;
}
hooks.additionalTreeRuntimeRequirements.call
触发多个 Plugin
的监听,为 set
汇合减少item
hooks.runtimeRequirementInTree.call
for (const r of set) {
this.hooks.runtimeRequirementInTree
.for(r)
.call(treeEntry, set, context);
}
依据目前的 runtimeRequirements
,即RuntimeGlobals.ensureChunk = "__webpack_require__.e"
触发对应的逻辑解决
比方上图所示,咱们在 module.codeGeneration()
能够替换对应的代码为 "__webpack_require__.e"
,在下面的runtime 代码的品种和作用
中,咱们晓得这是代表异步申请的办法,然而咱们只替换了源码,如下代码块所示
__webpack_require__.e(/*! import() | B */ "B").then(__webpack_require__.bind(__webpack_require__, /*! ./async/async_B.js */ "./src/async/async_B.js")
).then(bModule => {bModule.default();
});
咱们还须要生成 _webpack_require__.e
办法的具体内容,即对应的 runtime
工具办法须要生成,如下所示
__webpack_require__.e = function (chunkId) {return Promise.all(Object.keys(__webpack_require__.f).reduce(function (promises, key) {__webpack_require__.f[key](chunkId, promises);
return promises;
}, []));
};
因而咱们通过 this.hooks.runtimeRequirementInTree
解决这种状况,如下面例子所示,咱们最终应用 compilation.addRuntimeModule()
减少了一个代码模块,在前面的代码生成中,咱们就能够触发 EnsureChunkRuntimeModule.js
的generate()
进行对应代码的生成,如下图所示
它所对应的就是咱们打包后 webpack/runtime/ensure chunk
的内容
chunkGraph.addTreeRuntimeRequirements
addTreeRuntimeRequirements(chunk, items) {const cgc = this._getChunkGraphChunk(chunk);
const runtimeRequirements = cgc.runtimeRequirementsInTree;
for (const item of items) runtimeRequirements.add(item);
}
6.4.5 代码生成:合并到其它 chunk
在下面的 this.hooks.runtimeRequirementInTree.call
的剖析中,咱们能够晓得,会触发compilation.addRuntimeModule
//node_modules/webpack/lib/RuntimePlugin.js
compilation.hooks.runtimeRequirementInTree
//__webpack_require__.e
.for(RuntimeGlobals.ensureChunk)
.tap("RuntimePlugin", (chunk, set) => {const hasAsyncChunks = chunk.hasAsyncChunks();
if (hasAsyncChunks) {
//__webpack_require__.f
set.add(RuntimeGlobals.ensureChunkHandlers);
}
compilation.addRuntimeModule(
chunk,
new EnsureChunkRuntimeModule(set)
);
return true;
});
实质上就是把某一个 runtime
的办法当作 Module
与Chunk
进行关联,而后在 chunk
生成代码时,会将 chunk
蕴含的 modules
,包含这些runtime
的modules
进行代码的生成
// node_modules/webpack/lib/Compilation.js
addRuntimeModule(chunk, module, chunkGraph = this.chunkGraph) {
// Deprecated ModuleGraph association
if (this._backCompat)
ModuleGraph.setModuleGraphForModule(module, this.moduleGraph);
// add it to the list
this.modules.add(module);
this._modules.set(module.identifier(), module);
// connect to the chunk graph
chunkGraph.connectChunkAndModule(chunk, module);
chunkGraph.connectChunkAndRuntimeModule(chunk, module);
if (module.fullHash) {chunkGraph.addFullHashModuleToChunk(chunk, module);
} else if (module.dependentHash) {chunkGraph.addDependentHashModuleToChunk(chunk, module);
}
//.......
}
而真正生成 runtime
的中央是在生成代码时获取对应 chunk
的runtimeModule
进行代码生成,比方下图的 renderMain()
中,咱们能够拿到一个 runtimeModules
,仔细观察其实每一个Module
就是一个工具办法,每一个 Module
生成的 PrefixSource
就是理论工具办法的内容
chunk
对应的 modules
代码生成如下所示
chunk
对应的 runtime Modules
生成的代码就是下面剖析的各种工具办法的代码
如果配置了 runtime 造成独立的 chunk,实质也是应用
chunk
对应的runtime Modules
生成代码
6.4.6 代码生成:runtime 造成独立的 chunk
webpack.config.js 配置和 entry 打包内容展现
webpack5
容许在 optimization
配置对应的参数,比方上面代码块配置 name: 'runtime'
,能够生成一个runtimeChunk
,多个Chunk
就能够共用生成的runtime.js
webpack5
也容许依据不同的Chunk
抽离出对应的runtimeChunk
module.exports = {
optimization: {
runtimeChunk: {name: 'runtime',},
chunkIds: "named",
splitChunks: {
chunks: 'all',
maxInitialRequests: 10,
maxAsyncRequests: 10,
cacheGroups: {
test3: {
chunks: 'all',
minChunks: 3,
name: "test3",
priority: 3
},
test2: {
chunks: 'all',
minChunks: 2,
name: "test2",
priority: 2,
maxSize: 50
}
}
}
}
}
当配置生成 runtimeChunk
时,入口类型 Chunk
的代码渲染办法会从 renderMain()
变为 renderChunk()
,如上面截图所示,renderChunk()
最显著的特点是整个文件会应用全局的变量进行 push
操作
renderChunk()
能够参考下面的剖析,逻辑也比较简单,这里不再赘述
造成新的Chunk
简略剖析是如何造成新的
runtimeChunk
初始化时会判断是否有配置 options.optimization.runtimeChunk
,而后确定runtime
的名称
//node_modules/webpack/lib/WebpackOptionsApply.js
if (options.optimization.runtimeChunk) {const RuntimeChunkPlugin = require("./optimize/RuntimeChunkPlugin");
new RuntimeChunkPlugin(options.optimization.runtimeChunk).apply(compiler);
}
//node_modules/webpack/lib/optimize/RuntimeChunkPlugin.js
apply(compiler) {
compiler.hooks.thisCompilation.tap("RuntimeChunkPlugin", compilation => {
compilation.hooks.addEntry.tap(
"RuntimeChunkPlugin",
(_, { name: entryName}) => {if (entryName === undefined) return;
const data = compilation.entries.get(entryName);
if (data.options.runtime === undefined && !data.options.dependOn) {
// Determine runtime chunk name
let name = this.options.name;
if (typeof name === "function") {name = name({ name: entryName});
}
data.options.runtime = name;
}
}
);
});
}
在 seal
阶段的初始化阶段,只有配置 options.optimization.runtimeChunk
才会触发上面增加 this.addChunk(runtime)
的逻辑
同步入口和异步依赖都会调用
this.addChunk()
办法
seal(callback) {for (const [name, { dependencies, includeDependencies, options}] of this
.entries) {const chunk = this.addChunk(name);
//... 解决失常入口文件为 chunk
}
outer: for (const [
name,
{options: { dependOn, runtime}
}
] of this.entries) {if (dependOn) {//....} else if (runtime) {const entry = this.entrypoints.get(name);
let chunk = this.namedChunks.get(runtime);
if (chunk) {//...} else {chunk = this.addChunk(runtime);
chunk.preventIntegration = true;
runtimeChunks.add(chunk);
}
entry.unshiftChunk(chunk);
chunk.addGroup(entry);
entry.setRuntimeChunk(chunk);
}
}
buildChunkGraph(this, chunkGraphInit);
}
runtimeChunk 生成代码流程
正如下面依赖收集:runtimeRequirements
和 chunkGraphEntries
的剖析一样,如果有配置 options.optimization.runtimeChunk
,则chunkGraphEntries
为配置的runtimeChunk
如果没有配置,那么 chunkGraphEntries
就是入口文件自身,比方 entry4.js
造成 app4
这个 Chunk
,那么此时chunkGraphEntries
就是 app4
这个Chunk
最终都会触发 hooks.runtimeRequirementInTree
进行 compilation.addRuntimeModule(chunk, module)
减少了一个代码模块,如果有配置 runtimeChunk
,那么此时chunk
就是runtimeChunk
,否则就是app4 Chunk
processRuntimeRequirements() {
//... 解决 module 和 runtimeRequirements
//... 解决 chunk 和 runtimeRequirements
for (const treeEntry of chunkGraphEntries) {const set = new Set();
for (const chunk of treeEntry.getAllReferencedChunks()) {
const runtimeRequirements =
chunkGraph.getChunkRuntimeRequirements(chunk);
for (const r of runtimeRequirements) set.add(r);
}
this.hooks.additionalTreeRuntimeRequirements.call(
treeEntry,
set,
context
);
for (const r of set) {
this.hooks.runtimeRequirementInTree
.for(r)
.call(treeEntry, set, context);
}
chunkGraph.addTreeRuntimeRequirements(treeEntry, set);
}
}
因而无论有无配置 options.optimization.runtimeChunk
,最终代码runtime
代码始终都是上面的代码块,即 runtimeChunk
实质触发的是 renderMain()
办法生成代码
renderMain() {
//...
const runtimeModules =
renderContext.chunkGraph.getChunkRuntimeModulesInOrder(chunk);
if (runtimeModules.length > 0) {
source.add(
new PrefixSource(
prefix,
Template.renderRuntimeModules(runtimeModules, chunkRenderContext)
)
);
}
//...
}
参考
- 「万字进阶」深入浅出 Commonjs 和 Es Module
- ES6 模块和 CommonJS 模块有哪些差别?
- 阮一峰 -Node.js 如何解决 ES6 模块
- webpack 打包后运行时文件剖析
- 精通 Webpack 外围原理专栏
- webpack@4.46.0 源码剖析 专栏
其它工程化文章
- 「Webpack5 源码」热更新 HRM 流程浅析
- 「Webpack5 源码」make 阶段(流程图)剖析
- 「Webpack5 源码」enhanced-resolve 门路解析库源码剖析
- 「Webpack5 源码」seal 阶段(流程图)剖析(一)
- 「Webpack5 源码」seal 阶段剖析(二)-SplitChunksPlugin 源码
- 「vite4 源码」dev 模式整体流程浅析(一)
- 「vite4 源码」dev 模式整体流程浅析(二)