本文内容基于
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模式整体流程浅析(二)
发表回复