Skip to content

seal 阶段

在模块源码编译解析后,进入到seal阶段,执行compilation.seal方法。该阶段的主要目的是建立modulechunk的关系,并且将chunk内的代码进行拼接整合,形成可以执行的代码块。

chunk

参考文章

webpack 会根据模块依赖图的内容组织分包 —— Chunk 对象,默认的分包规则有:

  • 同一个 entry 下触达到的模块组织成一个 chunk。
  • 异步模块单独组织为一个 chunk。
  • entry.runtime 单独组织成一个 chunk。

形成的chunkmodule之间的关系会记录在ChunkGraph当中:

  • ChunkGraphModule用于记录module与外界的关系,其中chunks参数记录了module关联的chunk
  • ChunkGraphChunk用于记录chunk与外界的关系,其中modules参数记录了chunk关联的modules
  • Entrypoint继承自ChunkGroup,用于组织chunks。记录了chunks以及chunkGroup的父子关系。

seal函数的前半段,主要集中于建立入口modulechunk关系。

buildChunkGraph

buildChunkGraph的主要执行者是visitModules函数:

javascript
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

javascript
while (queue.length || queueConnect.size) {
  processQueue();

  // ...

  if (queue.length === 0) {
    const tempQueue = queue;
    queue = queueDelayed.reverse();
    queueDelayed = tempQueue;
  }
}

processQueue方法是queue的具体执行内容,精简后的内容如下:

javascript
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建立chunkmodule之间的关系。processBlock又会遍历当前module下引用到的其他module,并添加到queue中:

javascript
queueBuffer.push({
  action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
  block: refModule,
  module: refModule,
  chunk,
  chunkGroup,
  chunkGroupInfo
});

因此,最终通过递归的方式遍历所有相关的module,建立了moduleschunks之间的关系。

hooks.optimizeChunks

  • RemoveEmptyChunksPlugin:移除非入口的空chunks
  • MergeDuplicateChunksPlugin:合并重复的chunks
  • SplitChunksPlugin:分包。

开始生成代码

在得到chunksmodules后,经过一系列优化,最终会对模块进行遍历,为chunk生成最终的代码。

javascript
// 开始生成代码
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方法执行每个任务:

javascript
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方法的调用:

javascript
codeGeneration(
  // 其他代码省略
  this.generator.generate(this, {
    dependencyTemplates,
    runtimeTemplate,
    moduleGraph,
    chunkGraph,
    runtimeRequirements,
    runtime,
    concatenationScope,
    getData,
    type
  });
)

JavascriptGenerator

webpack/lib/javascript/JavascriptGenerator.js文件中找到generate函数:

javascript
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方法:

javascript
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过程中分析出来的dependenciespresentationalDependencies以及blockssourceBlock最终也是调用sourceDependency方法:

javascript
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当中的,例如:

javascript
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

javascript
template.apply(dependency, source, templateContext);

这里以HarmonyImportSideEffectDependency为例,如使用import a from './moduleA.js'时,会解析为HarmonyImportSideEffectDependency

javascript
HarmonyImportSideEffectDependency.Template = class HarmonyImportSideEffectDependencyTemplate extends (
 HarmonyImportDependency.Template
) {
 apply(dependency, source, templateContext) {
  const { moduleGraph, concatenationScope } = templateContext;
  super.apply(dependency, source, templateContext);
 }
};

最终调用HarmonyImportDependency.Templateapply方法:

javascript
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

javascript
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对应的moduleoriginModule代表父modulerequest代表import from的路径,importVar使用getImportVar产生:

javascript
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保证了名称的唯一性,如:

javascript
// 生成前
'./moduleA.js'
// 生成后
'_moduleA__WEBPACK_IMPORTED_MODULE_0__'

接下来就是调用importStatement方法了,首先获取moduleId(同时也是模块路径),还会通过comment方法在id前面添加相关注释:

javascript
const moduleId = this.moduleId({
  module,
  chunkGraph,
  request,
  weak
});

moduleId({ module, chunkGraph, request, weak }) {
  return `${this.comment({ request })}${JSON.stringify(moduleId)}`;
}

然后拼接成新的import内容:

javascript
const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`;

return [importContent, ""];

(其中optDeclarationupdate来定,如果是重新赋值的话那么就为''空字符串:

javascript
const optDeclaration = update ? "" : "var ";

由于使用了__webpack_require__这个方法,因此需要添加该方法到runtimeRequirements中:

javascript
runtimeRequirements.add(RuntimeGlobals.require);

生成完importStatement内容后,最后会执行:

javascript
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方法拼接代码:

javascript
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函数:

javascript
if (source) {
  sources.set(type, new CachedSource(source));
}
const resultEntry = {
  sources,
  runtimeRequirements,
  data
};
return resultEntry;

将所有生成的代码sources和使用到的runtimeRequirements返回。

总结

seal阶段大概可以分为三个阶段:

第一个阶段为chunk关系建立,此时会遍历所有的module,并形成modulechunk之间的关系。

第二个阶段为chunk的优化阶段,该阶段会对生成的chunk进行处理优化,如清除空的chunk、重复的chunk、对chunk进行分包等等。

第三个阶段为生成代码阶段,该阶段会对chunks以及chunk下的modules进行遍历,根据module.build解析的代码生成新的代码片段。

其中生成代码的过程主要是通过sourceDependencymodule.build中解析出来的module.dependenciesmodule.blocks进行代码生成。首先会根据dependency获取相应的生成模板template。其次调用template.apply方法将dependency替换成新的代码块。最后使用addToSource方法将所有dependency替换后的代码以及源码形成数组形式。