美文网首页
浅析 webpack 打包流程(原理) 四 - chunk 优化

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

作者: AizawaSayo | 来源:发表于2021-07-21 12:34 被阅读0次

    接上文:浅析 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 优化

          本文链接:https://www.haomeiwen.com/subject/aahjmltx.html