浅析 webpack 打包流程(原理) 四 - chunk 优化

接上文:浅析 webpack 打包流程(原理) 三 - 生成 chunk

六、chunk 优化

chunk 优化阶段概述
暴露了很多 chunk 优化相关的钩子:
触发optimize相关 hook 移除空 chunk 和 重复 chunk,如配置了SplitChunksPlugin也会在此时进行 chunk 分包;
然后触发其他 hook 分别设置 module.id、chunk.id 并对它们进行排序
创建了各类 hash,包括 module hash,chunk hash,content hash,fullhash,hash。
之前 chunk 已经根据 webpack 的预处理和默认规则进行了一轮分包,现在 webpack 会根据我们配置的插件来对 chunks 进行优化。

6.1 chunk 的初步优化

在触发 compilation.hooks: 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,可以看下插件内compilation.hooks.optimizeChunksAdvanced.tap(...)注册的代码
  • RemoveEmptyChunksPlugin 再次移除空 chunk
  • RuntimeChunkPlugin 如有配置 optimization.runtimeChunk,可单独抽离 runtime 代码

6.2 设置 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 等排序。

6.3 设置 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 等排序,然后:

// /lib/Compilation.js
if (shouldRecord) {
  this.hooks.recordModules.call(this.modules, this.records);
  this.hooks.recordChunks.call(this.chunks, this.records);
}

依旧是对 records 的一些设置。

6.4 创建 hash

接下来执行:

// /lib/Compilation.js
this.hooks.beforeHash.call();
this.createHash();
this.hooks.afterHash.call();
if (shouldRecord) {
  this.hooks.recordHash.call(this.records);
}

进入 createHash 方法,先初始化一个 hash,然后执行:

// /lib/Compilation.js
createHash() {
  // ... 初始化 hash
  this.mainTemplate.updateHash(hash);
  this.chunkTemplate.updateHash(hash);
}
  • mainTemplate:本意是用来渲染主 chunk (入口 chunk) 的模版,入口 chunk 默认包含 runtime (webpackBootstrap 代码)。如果通过optimization.runtimeChunk单独把 runtime 抽取出来,那么只有 runtime chunk 应用 mainTemplate,其余都是普通 chunk。输出的文件用output.filename定义文件名。
  • chunkTemplate:用来渲染生成普通 chunk 的模版。默认应用于所有异步 chunk。一旦单独提取了 runtime,则除了 runtime chunk 之外的 chunk 都属于普通 chunk。若入口 chunk 拆了其余包(比如第三方插件),那么这些拆出的同步 chunk 也应用chunkTemplate。默认根据output.filename定义文件名,如果定义了output.chunkFilename则以此为准。

mainTemplateupdate('maintemplate','3')后,触发MainTemplate.hooks: hash,执行插件 JsonpMainTemplatePlugin、WasmMainTemplatePlugin 内的订阅事件,hash.buffer 更新为 "maintemplate3jsonp6WasmMainTemplatePlugin2"。
chunkTemplateupdate('ChunkTemplate','2')后,触发ChunkTemplate.hooks: hash,执行插件 JsonpChunkTemplatePlugin 内的订阅事件,hash.buffer 更新为 "maintemplate3jsonp6WasmMainTemplatePlugin2ChunkTemplate2JsonpChunkTemplatePlugin4webpackJsonpwindow"。

// /lib/Compilation.js
// moduleTemplates 为 complation 实例化时所定义
this.moduleTemplates = {
  javascript: new ModuleTemplate(this.runtimeTemplate, 'javascript'),
  webassembly: new ModuleTemplate(this.runtimeTemplate, 'webassembly'),
};

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

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

然后把 children、warnings、errors 的 hash 或者 message update 进去。

6.4.1 创建 module hash

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

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

让我们看下 module.updateHash 方法:

// 先调用
// /lib/NormalModule.js
updateHash(hash) {
  hash.update(this._buildHash); // 这里加入了 _buildHash
  super.updateHash(hash);
}

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

// 上面 Module 的 super 调用
// /lib/DependenciesBlock.js
// 调用各自 dependencies、blocks、variables 的 updateHash
updateHash(hash) {
  for (const dep of this.dependencies) dep.updateHash(hash);
  for (const block of this.blocks) block.updateHash(hash);
  for (const variable of this.variables) variable.updateHash(hash);
}

最终得到 moduleHash.buffer 形如:
"d30251197267ff9c8f1e37f43af3b15d./src/a.jsnull12,38./src/b.jsnamespace./src/b.js./src/b.jsnamespace./src/b.jsaddaddnamespacenullnull{"name":null}0./src/c.js"
"6627949a75e04e8f80d66cbf8c7c5446./src/c.jsnull12,34./src/d.jsnamespace./src/d.js./src/d.jsnamespace./src/d.jsdefaultdefaultnamespacenullnull{"name":null}./src/b.js"
......

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

6.4.2 创建 chunk hash

继续往下,先对 chunks 进行排序,然后执行 chunks 的遍历:循环初始化每个 chunk 的 hash,并调用每个 chunk 的 updateHash。

// /lib/Compilation.js
// 遍历 chunks
for (let i = 0; i < chunks.length; i++) {
  const chunk = chunks[i];
  const chunkHash = createHash(hashFunction); // 初始化每个 chunk 的 hash
  try {
    if (outputOptions.hashSalt) {
      chunkHash.update(outputOptions.hashSalt);
    }
    chunk.updateHash(chunkHash);
    // 判断 chunk 是否含有 runtime 代码
    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(new ChunkRenderError(chunk, '', err));
  }
}

chunk 的 updateHash 方法:

// /lib/Chunk.js
updateHash(hash) {
  hash.update(`${this.id} `);
  hash.update(this.ids ? this.ids.join(",") : "");
  hash.update(`${this.name || ""} `);
  for (const m of this._modules) {
    hash.update(m.hash); // 把每个 module 的 hash 一并加入
  }
}

得到 chunkHash.buffer,然后判断 chunk 是否含有 runtime 代码,有就使用 mainTemplate 作为模版,无就用 chunkTemplate。

runtime:字面意思是运行时代码。它主要内容是名为 webpackBootstrap 的一个自执行函数,包含模块交互时连接模块所需的加载和解析逻辑的所有代码,还伴随着 manifest 数据(chunks 映射关系的 list)。它负责项目的运行,webpack 通过它来连接模块化应用程序。

然后执行: template.updateHashForChunk:

chunkTemplate.updateHashForChunk
// /lib/ChunkTemplate.js
updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
  // 与上文 Compilation 的 createHash 中 this.chunkTemplate.updateHash(hash) 执行相同
  this.updateHash(hash); 
  this.hooks.hashForChunk.call(hash, chunk);
}

ChunkTemplate.hooks:hashForChunk触发插件 JsonpChunkTemplatePlugin 的注册事件:update、entryModule 和 group.childrenIterable。

mainTemplate.updateHashForChunk
// /lib/MainTemplate.js
updateHashForChunk(hash, chunk, moduleTemplate, dependencyTemplates) {
  // 与上文 Compilation 的 createHash 中  this.mainTemplate.updateHash(hash) 执行相同
  this.updateHash(hash); 
  this.hooks.hashForChunk.call(hash, chunk);
  for (const line of this.renderBootstrap("0000", chunk, moduleTemplate, dependencyTemplates)) {
  hash.update(line);
  }
}

MainTemplate.hooks:hashForChunk触发插件 TemplatedPathPlugin 注册事件,根据 chunkFilename 的不同配置,update chunk.getChunkMaps 的不同导出。

以下为chunk.getChunkMaps 方法:

// /lib/Chunk.js
getChunkMaps(realHash) {
  const chunkHashMap = Object.create(null);
  const chunkContentHashMap = Object.create(null);
  const chunkNameMap = Object.create(null);

  for (const chunk of this.getAllAsyncChunks()) {
    chunkHashMap[chunk.id] = realHash ? chunk.hash : chunk.renderedHash;
    for (const key of Object.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.hash update 到 hash 上。 最终得到 chunk.hash 和 chunk.renderedHash。

6.4.3 创建 content hash & fullhash & hash

接着执行:this.hooks.contentHash.call(chunk)触发 JavascriptModulesPlugin 订阅事件,主要作用是创建生成chunk.contentHash.javascript,也就是 contentHash 生成相关,大体跟生成 chunk hash 一致。

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

下文:浅析 webpack 打包流程(原理) 五 - 构建资源

webpack 打包流程系列(未完):
浅析 webpack 打包流程(原理) - 案例 demo
浅析 webpack 打包流程(原理) 一 - 准备工作
浅析 webpack 打包流程(原理) 二 - 递归构建 module
浅析 webpack 打包流程(原理) 三 - 生成 chunk
浅析 webpack 打包流程(原理) 四 - chunk 优化
浅析 webpack 打包流程(原理) 五 - 构建资源
浅析 webpack 打包流程(原理) 六 - 生成文件

参考鸣谢:
webpack打包原理 ? 看完这篇你就懂了 !
webpack 透视——提高工程化(原理篇)
webpack 透视——提高工程化(实践篇)
webpack 4 源码主流程分析
[万字总结] 一文吃透 Webpack 核心原理
有点难的 Webpack 知识点:Dependency Graph 深度解析
webpack系列之六chunk图生成

你可能感兴趣的:(浅析 webpack 打包流程(原理) 四 - chunk 优化)