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替换后的代码以及源码形成数组形式。
