webpack 4 源码主流程分析(九):优化 chunk

chunk 的一些优化

在 chunk 生成后,开始进行 chunk 优化之类的处理。

在触发钩子 optimize,optimizeModules(module 相关的优化)等之后,忽略掉本次打包未触发插件的钩子,执行:

this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups);
复制代码

触发插件:

  • EnsureChunkConditionsPlugin 处理 chunkCondition

  • RemoveEmptyChunksPlugin 移除空 chunk

  • MergeDuplicateChunksPlugin 处理重复 chunk

this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups);
复制代码

触发插件:

  • SplitChunksPlugin 优化切割 chunk

  • RemoveEmptyChunksPlugin 再次移除空 chunk

设置 module.id

this.hooks.reviveModules.call(this.modules, this.records);
复制代码

触发插件 RecordIdsPlugin:设置 module.id

this.hooks.beforeModuleIds.call(this.modules);
复制代码

触发插件 NamedModulesPlugin: 设置 module.id 为 文件相对路径,然后执行:

this.applyModuleIds();
复制代码

这一步主要用于设置 module.id(如果 id 在上一步没有设置的话),内部具体算法为:

先遍历各 module,找出其中最大的 id 以他为最大值(usedIdmax),计算出比他小的所有未使用的正整数和(usedIdmax+1)作为 unusedIds 用于给没有设置 id 的 module 使用,unusedIds 用尽后,则设置 id 为 (usedIdmax+1)++

this.sortItemsWithModuleIds();
复制代码

根据 module.id 给 module,chunk,reasons 等排序。

设置 chunk.id

this.hooks.reviveChunks.call(this.chunks, this.records);
复制代码

触发插件 RecordIdsPlugin:设置 chunk.id

this.hooks.optimizeChunkOrder.call(this.chunks);
复制代码

触发插件 OccurrenceOrderChunkIdsPlugin:chunks 排序

this.hooks.beforeChunkIds.call(this.chunks);
复制代码

触发插件 NamedChunksPlugin:设置 chunk.id = chunk.name

this.applyChunkIds();
复制代码

这一步主要用于设置 chunk.id,算法与 this.applyModuleIds 一致。

this.sortItemsWithChunkIds();
复制代码

根据 chunk.id 给 module,chunk,reasons,errors,warnings,children 等排序。

if (shouldRecord) {
  this.hooks.recordModules.call(this.modules, this.records);
  this.hooks.recordChunks.call(this.chunks, this.records);
}
复制代码

依旧是对 records 的一些设置。

创建 hash

接下来执行:

this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
if (shouldRecord) {
  this.hooks.recordHash.call(this.records);
}
复制代码

进入 createHash,前文已介绍生成 hash 的方法,此处先初始化一个 hash,然后执行:

this.mainTemplate.updateHash(hash);
this.chunkTemplate.updateHash(hash);
复制代码
  • mainTemplate: 渲染生成包含 webpack runtime bootstrap 代码的 chunk

  • chunkTemplate: 渲染生成普通 chunk

mainTemplate 在 update('maintemplate','3') 后,触发 MainTemplate.hooks: hash,执行插件 JsonpMainTemplatePlugin,WasmMainTemplatePlugin 内的相关事件,hash.buffer 更新为 maintemplate3jsonp6WasmMainTemplatePlugin2。

chunkTemplate 在 update('ChunkTemplate','2') 后,触发ChunkTemplate.hooks: hash,执行插件 JsonpChunkTemplatePlugin内的相关事件,hash.buffer 更新为 maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow。

for (const key ofObject.keys(this.moduleTemplates).sort()) {
  this.moduleTemplates[key].updateHash(hash);
}

// 以下代码为 complation 实例化的时候所定义this.moduleTemplates = {
  javascript: newModuleTemplate(this.runtimeTemplate, 'javascript'),
  webassembly: newModuleTemplate(this.runtimeTemplate, 'webassembly'),
};
复制代码

将 this.moduleTemplates 的 key 排序后执行各自的 updateHash,hash.buffer 更新为 maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow1FunctionModuleTemplatePlugin21

然后如果有 children,warnings,errors 也把他们的 hash 或者 messageupdate 进去。然后执行:

创建 module hash

for (let i = 0; i < modules.length; i++) {
  constmodule = modules[i];
  const moduleHash = createHash(hashFunction);
  module.updateHash(moduleHash);
  module.hash = /** @type {string} */ (moduleHash.digest(hashDigest));
  module.renderedHash = module.hash.substr(0, hashDigestLength);
}
复制代码

这里循环初始化了每个 module 的 hash,并调用了每个 module 的 updateHash:

module.updateHash(moduleHash);

//上面 module.updateHash 调用
hash.update(this._buildHash); //这里加入了 _buildHashsuper.updateHash(hash);

//上面 super 调用
hash.update(`${this.id}`);
hash.update(JSON.stringify(this.usedExports));
super.updateHash(hash);

//上面 super 调用//调用各自 dependencies,blocks,variables的 updateHashfor (const dep ofthis.dependencies) dep.updateHash(hash);
for (const block ofthis.blocks) block.updateHash(hash);
for (const variable ofthis.variables) variable.updateHash(hash);
复制代码

最终得到 moduleHash.buffer 形如:ac01f98d10f099796d2f3d600c2592d1./src/a.jsnull0,28./src/b.jsnamespace./src/b.js29,57./src/c.jsnamespace./src/c.js58,121../github/test-loader/loader.js?number=20000!./src/e.jsnamespace../github/test-loader/loader.js?number=20000!./src/e.js./src/b.jsnamespace./src/b.jsaddaddnamespacenullnull./src/c.jsnamespace./src/c.jssubsubnamespacenullnull../github/test-loader/loader.js?number=20000!./src/e.jsnamespace../github/test-loader/loader.js?number=20000!./src/e.jsdivdivnamespacenullnull

然后最终生成出 module 各自的 hash 和 renderedHash。

创建 chunk hash

继续执行,先对 chunks 进行排序,然后遍历 chunks:

for (let i = 0; i < chunks.length; i++) {
  const chunk = chunks[i];
  const chunkHash = createHash(hashFunction);
  try {
    if (outputOptions.hashSalt) {
      chunkHash.update(outputOptions.hashSalt);
    }
    chunk.updateHash(chunkHash);
    const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
    template.updateHashForChunk(chunkHash, chunk, this.moduleTemplates.javascript, this.dependencyTemplates);
    this.hooks.chunkHash.call(chunk, chunkHash);
    chunk.hash = /** @type {string} */ (chunkHash.digest(hashDigest));
    hash.update(chunk.hash);
    chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);
    this.hooks.contentHash.call(chunk);
  } catch (err) {
    this.errors.push(newChunkRenderError(chunk, '', err));
  }
}
复制代码

这里循环初始化了每个 chunk 的 hash,并调用了每个 chunk 的 updateHash:

chunk.updateHash(chunkHash);

//上面 chunk.updateHash 调用
hash.update(`${this.id} `);
hash.update(this.ids ? this.ids.join(',') : '');
hash.update(`${this.name || ''} `);
for (const m ofthis._modules) {
  hash.update(m.hash); //此处把每个 module 的 hash 一并加入
}
复制代码

得到 chunkHash.buffer 形如 bundle bundlebundle 99d78a1615d2e348fbf274adb4e0b67c4fa9f69c98e5b41607cb6354e95983c3824bbf3e0b5e82705f88a41a6741b08f2f18bdc137e8a1e8e6cc78ca7ce0caf64b500a96034ab069ecf31c34f944ede6,然后判断 chunk 是否含有 runtime 代码( template 判断入口 chunk 与运行时 chunk 一致则为 this.mainTemplate,不一致则为 this.chunkTemplate )。

chunkTemplate

如果是 chunkTemplate 的 updateHashForChunk,则执行:

this.updateHash(hash); //与上文 this.chunkTemplate.updateHash(hash) 执行相同this.hooks.hashForChunk.call(hash, chunk);
复制代码

this.hooks.hashForChunk.call(hash, chunk) 触发插件 JsonpChunkTemplatePlugin 相关事件, updateentryModule 和 group.childrenIterable。

mainTemplate

如果是 mainTemplate 的 updateHashForChunk,则执行:

this.updateHash(hash); //与上文 this.mainTemplate.updateHash(hash) 执行相同this.hooks.hashForChunk.call(hash, chunk);
for (const line ofthis.renderBootstrap('0000', chunk, moduleTemplate, dependencyTemplates)) {
  hash.update(line);
}
复制代码

this.hooks.hashForChunk.call(hash, chunk) 触发插件 TemplatedPathPlugin 相关事件,根据 chunkFilename 的不同配置,update chunk.getChunkMaps 的不同导出,chunk.getChunkMaps 的实现为:

getChunkMaps(realHash) {
  const chunkHashMap = Object.create(null);
  const chunkContentHashMap = Object.create(null);
  const chunkNameMap = Object.create(null);

  for (const chunk ofthis.getAllAsyncChunks()) {
    chunkHashMap[chunk.id] = realHash ? chunk.hash : chunk.renderedHash;
    for (const key ofObject.keys(chunk.contentHash)) {
      if (!chunkContentHashMap[key]) {
        chunkContentHashMap[key] = Object.create(null);
      }
      chunkContentHashMap[key][chunk.id] = chunk.contentHash[key];
    }
    if (chunk.name) {
      chunkNameMap[chunk.id] = chunk.name;
    }
  }

  return {
    hash: chunkHashMap, // chunkFilename 配置为 chunkhash的导出contentHash: chunkContentHashMap, // chunkFilename 配置为 contenthash 的导出name: chunkNameMap // chunkFilename 配置为 name 的导出
  };
}
复制代码

可见各种类型的 hash 都与其他的 不含runtime模块即异步模块 的 hash 有强关联,所以前面的 chunk 排序也就很重要。

this.renderBootstrap 用于拼接 webpack runtime bootstrap 代码字符串。这里相当于把每一行 runtime 代码循环 update 进去,到此 chunk hash 生成结束。 将 chunk.hashupdate 到 hash 上。 最终得到 chunk.hash 和 chunk.renderedHash。

创建 content hash & fullhash & hash

然后执行:

this.hooks.contentHash.call(chunk);
复制代码

这里触发 JavascriptModulesPlugin 相关事件,主要作用是创建生成 chunk.contentHash.javascript,也就是 contentHash 生成相关,大体跟生成 chunk hash 一致.

最后在 createHash 里得到 Compilation.hash 和 Compilation.fullhash,hash 生成到此结束。chunk 相关优化到此结束。

本章小结

  1. 本章主要是对 chunk 的一些优化工作,暴露了很多相关的优化钩子;

  1. 设置了 module.id 及 chunk.id 并排序;

  1. 创建了 hash,包括 module hash,chunk hash,content hash,fullhash,hash

作者:Korey

链接:https://juejin.cn/post/6844904047233728519

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值