seal 阶段
在模块源码编译解析后,进入到seal
阶段,执行compilation.seal
方法。该阶段的主要目的是建立module
和chunk
的关系,并且将chunk
内的代码进行拼接整合,形成可以执行的代码块。
chunk
webpack 会根据模块依赖图的内容组织分包 —— Chunk 对象,默认的分包规则有:
- 同一个 entry 下触达到的模块组织成一个 chunk。
- 异步模块单独组织为一个 chunk。
- entry.runtime 单独组织成一个 chunk。
形成的chunk
和module
之间的关系会记录在ChunkGraph
当中:
ChunkGraphModule
用于记录module
与外界的关系,其中chunks
参数记录了module
关联的chunk
。ChunkGraphChunk
用于记录chunk
与外界的关系,其中modules
参数记录了chunk
关联的modules
。Entrypoint
继承自ChunkGroup
,用于组织chunks
。记录了chunks
以及chunkGroup
的父子关系。
在seal
函数的前半段,主要集中于建立入口module
的chunk
关系。
buildChunkGraph
buildChunkGraph
的主要执行者是visitModules
函数:
for (const [chunkGroup, modules] of inputEntrypointsAndModules) {
/** @type {ChunkGroupInfo} */
const chunkGroupInfo = {
// ...
};
const chunk = chunkGroup.getEntrypointChunk();
for (const module of modules) {
queue.push({
action: ADD_AND_ENTER_MODULE,
block: module,
module,
chunk,
chunkGroup,
chunkGroupInfo
});
}
该函数会遍历modules
(这个modules
为入口module
),并将其加入到queue
中。接着会循环执行queue
:
while (queue.length || queueConnect.size) {
processQueue();
// ...
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
processQueue
方法是queue
的具体执行内容,精简后的内容如下:
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
chunkGraph.connectChunkAndEntryModule(
chunk,
module,
);
case ADD_AND_ENTER_MODULE: {
chunkGraph.connectChunkAndModule(chunk, module);
}
case ENTER_MODULE: {
queueItem.action = LEAVE_MODULE;
queue.push(queueItem);
}
case PROCESS_BLOCK: {
processBlock(block);
break;
}
}
可以看出这里其实是通过connectChunkAndModule
建立chunk
和module
之间的关系。processBlock
又会遍历当前module
下引用到的其他module
,并添加到queue
中:
queueBuffer.push({
action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
block: refModule,
module: refModule,
chunk,
chunkGroup,
chunkGroupInfo
});
因此,最终通过递归的方式遍历所有相关的module
,建立了modules
和chunks
之间的关系。
hooks.optimizeChunks
- RemoveEmptyChunksPlugin:移除非入口的空
chunks
。 - MergeDuplicateChunksPlugin:合并重复的
chunks
。 - SplitChunksPlugin:分包。
开始生成代码
在得到chunks
与modules
后,经过一系列优化,最终会对模块进行遍历,为chunk
生成最终的代码。
// 开始生成代码
this.codeGeneration()
codeGeneration(callback) {
// ...
for (const module of this.modules) {
const runtimes = chunkGraph.getModuleRuntimes(module);
for (const runtime of runtimes) {
const hash = chunkGraph.getModuleHash(module, runtime);
jobs.push({ module, hash, runtime, runtimes: [runtime] });
}
}
this._runCodeGenerationJobs(jobs, callback);
}
codeGeneration
会遍历每一个module
,然后生成一个任务,添加到jobs
当中,关于runtime
的作用,可以参考这篇文章。完后会调用_runCodeGenerationJobs
方法执行每个任务:
asyncLib.eachLimit(
jobs,
this.options.parallelism,
({ module, hash, runtime, runtimes }, callback) => {
this._codeGenerationModule(
module,
runtime,
runtimes,
hash,
dependencyTemplates,
chunkGraph,
moduleGraph,
runtimeTemplate,
errors,
results,
(err, codeGenerated) => {
if (codeGenerated) statModulesGenerated++;
else statModulesFromCache++;
callback(err);
}
);
},
);
_codeGenerationModule() {
// 其他代码省略
result = module.codeGeneration({
chunkGraph,
moduleGraph,
dependencyTemplates,
runtimeTemplate,
runtime
});
}
可以看出最终调用的是module.codeGeneration
方法进行代码的生成。codeGeneration
的核心又是generator.generate
方法的调用:
codeGeneration(
// 其他代码省略
this.generator.generate(this, {
dependencyTemplates,
runtimeTemplate,
moduleGraph,
chunkGraph,
runtimeRequirements,
runtime,
concatenationScope,
getData,
type
});
)
JavascriptGenerator
在webpack/lib/javascript/JavascriptGenerator.js
文件中找到generate
函数:
generate(module, generateContext) {
// 1. 获取 module.build 后解析出来的代码
const originalSource = module.originalSource();
// 2. 复制一份代码
const source = new ReplaceSource(originalSource);
const initFragments = [];
// 3. 处理代码
this.sourceModule(module, initFragments, source, generateContext);
// 4. 返回拼接代码
return InitFragment.addToSource(source, initFragments, generateContext);
}
sourceModule
这里的核心是sourceModule
方法:
sourceModule(module, initFragments, source, generateContext) {
for (const dependency of module.dependencies) {
this.sourceDependency(
// ...
);
}
if (module.presentationalDependencies !== undefined) {
for (const dependency of module.presentationalDependencies) {
this.sourceDependency(
// ...
);
}
}
for (const childBlock of module.blocks) {
this.sourceBlock(
// ...
);
}
}
该方法会通过sourceDependency
来处理module.build
过程中分析出来的dependencies
、presentationalDependencies
以及blocks
。sourceBlock
最终也是调用sourceDependency
方法:
sourceDependency(module, dependency, initFragments, source, generateContext) {
const constructor = /** @type {new (...args: any[]) => Dependency} */ (
dependency.constructor
);
const template = generateContext.dependencyTemplates.get(constructor);
}
获取template
第一步根据dependency
获取相应的template
。这里的dependencyTemplates
定义在compilation
对象上,对于不同的dependency
,他们会有不同的template
。这些templates
是在实例化compilation
,触发hooks.compilation
时添加到dependencyTemplates
当中的,例如:
compiler.hooks.compilation.tap(
"HarmonyModulesPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyTemplates.set(
HarmonyCompatibilityDependency,
new HarmonyCompatibilityDependency.Template()
);
compilation.dependencyFactories.set(
HarmonyImportSideEffectDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HarmonyImportSideEffectDependency,
new HarmonyImportSideEffectDependency.Template()
);
// ...
})
)
sourceDependency
第二步执行template.apply
方法开始处理dependency
:
template.apply(dependency, source, templateContext);
这里以HarmonyImportSideEffectDependency
为例,如使用import a from './moduleA.js'
时,会解析为HarmonyImportSideEffectDependency
:
HarmonyImportSideEffectDependency.Template = class HarmonyImportSideEffectDependencyTemplate extends (
HarmonyImportDependency.Template
) {
apply(dependency, source, templateContext) {
const { moduleGraph, concatenationScope } = templateContext;
super.apply(dependency, source, templateContext);
}
};
最终调用HarmonyImportDependency.Template
的apply
方法:
const { module, chunkGraph, moduleGraph, runtime } = templateContext;
const connection = moduleGraph.getConnection(dep);
const referencedModule = connection && connection.module;
const moduleKey = referencedModule
? referencedModule.identifier()
: dep.request;
const key = `harmony import ${moduleKey}`;
首先会获取referencedModule
,它是dep
对应的module
,而传入的module
则是referencedModule
的父级module
。然后根据moduleKey
生成一个key
值。随后调用getImportState
:
const importStatement = dep.getImportStatement(false, templateContext);
getImportStatement(
update,
{ runtimeTemplate, module, moduleGraph, chunkGraph, runtimeRequirements }
) {
return runtimeTemplate.importStatement({
update,
module: moduleGraph.getModule(this),
chunkGraph,
importVar: this.getImportVar(moduleGraph),
request: this.request,
originModule: module,
runtimeRequirements
});
}
传入的参数module
代表当前dependency
对应的module
,originModule
代表父module
,request
代表import from
的路径,importVar
使用getImportVar
产生:
getImportVar(moduleGraph) {
const module = moduleGraph.getParentModule(this);
const meta = moduleGraph.getMeta(module);
let importVarMap = meta.importVarMap;
if (!importVarMap) meta.importVarMap = importVarMap = new Map();
let importVar = importVarMap.get(moduleGraph.getModule(this));
if (importVar) return importVar;
importVar = `${Template.toIdentifier(
`${this.userRequest}`
)}__WEBPACK_IMPORTED_MODULE_${importVarMap.size}__`;
importVarMap.set(moduleGraph.getModule(this), importVar);
return importVar;
}
importVarMap
的键为module
,值为module 名称
。importVar
为拼接而成,通过Template
对路径符号替换,另外还通过importVarMap.size
保证了名称的唯一性,如:
// 生成前
'./moduleA.js'
// 生成后
'_moduleA__WEBPACK_IMPORTED_MODULE_0__'
接下来就是调用importStatement
方法了,首先获取moduleId
(同时也是模块路径),还会通过comment
方法在id
前面添加相关注释:
const moduleId = this.moduleId({
module,
chunkGraph,
request,
weak
});
moduleId({ module, chunkGraph, request, weak }) {
return `${this.comment({ request })}${JSON.stringify(moduleId)}`;
}
然后拼接成新的import
内容:
const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`;
return [importContent, ""];
(其中optDeclaration
由update
来定,如果是重新赋值的话那么就为''
空字符串:
const optDeclaration = update ? "" : "var ";
由于使用了__webpack_require__
这个方法,因此需要添加该方法到runtimeRequirements
中:
runtimeRequirements.add(RuntimeGlobals.require);
生成完importStatement
内容后,最后会执行:
templateContext.initFragments.push(
new ConditionalInitFragment(
importStatement[0] + importStatement[1],
InitFragment.STAGE_HARMONY_IMPORTS,
dep.sourceOrder,
key,
runtimeCondition
)
);
将这行新的import
代码形成对象添加到initFragments
当中。至此sourceDependency
方法也基本执行完成了,经过这一系列的sourceDependency
后,所有的dependency
都将转变为新的语句,存放到initFragments
当中。
addToSource
获取完所有的代码片段后,开始执行InitFragment.addToSource
方法拼接代码:
const keyedFragments = new Map();
for (const [fragment] of sortedFragments) {
keyedFragments.set(fragment.key || Symbol(), fragment);
}
for (let fragment of keyedFragments.values()) {
concatSource.add(fragment.getContent(context));
const endContent = fragment.getEndContent(context);
if (endContent) {
endContents.push(endContent);
}
}
concatSource.add(source);
return concatSource;
addToSource
处理完后,会将新的代码以数组的形式返回,并且source
是数组的最后一个元素。生成代码后会回到codeGeneration
函数:
if (source) {
sources.set(type, new CachedSource(source));
}
const resultEntry = {
sources,
runtimeRequirements,
data
};
return resultEntry;
将所有生成的代码sources
和使用到的runtimeRequirements
返回。
总结
seal
阶段大概可以分为三个阶段:
第一个阶段为chunk
关系建立,此时会遍历所有的module
,并形成module
和chunk
之间的关系。
第二个阶段为chunk
的优化阶段,该阶段会对生成的chunk
进行处理优化,如清除空的chunk
、重复的chunk
、对chunk
进行分包等等。
第三个阶段为生成代码阶段,该阶段会对chunks
以及chunk
下的modules
进行遍历,根据module.build
解析的代码生成新的代码片段。
其中生成代码的过程主要是通过sourceDependency
对module.build
中解析出来的module.dependencies
和module.blocks
进行代码生成。首先会根据dependency
获取相应的生成模板template
。其次调用template.apply
方法将dependency
替换成新的代码块。最后使用addToSource
方法将所有dependency
替换后的代码以及源码形成数组形式。