问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Webpack模块编译打包及运行时(Runtime)逻辑深度解析

创作时间:
作者:
@小白创作中心

Webpack模块编译打包及运行时(Runtime)逻辑深度解析

引用
CSDN
1.
https://blog.csdn.net/Tyro_java/article/details/140095009

Webpack在构建过程中,会将所有Module内容转换为适当的产物代码形态,并以Chunk为单位合并Module产物代码,最终构建出Webpack Bundle代码文件。本文将深入分析这个过程的源码,详细剖析模块转译、运行时依赖分析、产物合并的具体实现逻辑。

什么是模块转译?

Webpack的打包功能并不是将原始文件代码“复制-粘贴”到产物文件那么简单,为了确保代码能在不同环境(多种版本的浏览器、Node、Electron等)正常运行,构建时需要对模块源码适当做一些转换操作。例如:

示例包含index.js、name.js两个JS代码模块,经过Webpack构建后生成如图右侧所示的产物文件,文件自上而下包含三块内容:

  • name.js模块对应的、函数形态的转译代码;
  • Webpack按需注入的运行时代码;
  • index.js模块对应的IIFE(立即执行函数)转译代码。

其中,index.js编译前后的内容变化包括:

  • 整个模块被包裹进IIFE(立即执行函数)中;
  • 添加__webpack_require__.r(webpack_exports);语句,用于适配ESM规范;
  • 源码中的import语句被转译为__webpack_require__函数调用;
  • 源码console语句所使用的name变量被转译为_name__WEBPACK_IMPORTED_MODULE_0__.default;
  • 添加若干注释。

模块转译主流程

在前文《Webpack: 三种Chunk产物的打包逻辑》中,我们已经介绍了compilation.seal函数内会调用buildChunkGraph生成Chunk依赖关系图。在此之后seal函数会开始触发一堆优化钩子,借助插件对ChunkGraph做诸如合并、拆分、删除无效Chunk等优化操作,并在最后调用compilation.codeGeneration方法:

class Compilation {
  seal(callback) {
    // 初始化 ChunkGraph、ChunkGroup 对象
    for (const [name, { dependencies, includeDependencies, options }] of this.entries) {
      // ...
    }
    for (const [name,{options: { dependOn, runtime },},] of this.entries) {
      // ...
    }
    // 构建 ChunkGroup
    buildChunkGraph(this, chunkGraphInit);
    // 执行诸多优化钩子
    this.hooks.optimize.call();
    // ...
    this.hooks.optimizeTree.callAsync(this.chunks, this.modules, (err) => {
      // ...
      this.hooks.optimizeChunkModules.callAsync(this.chunks, this.modules, (err) => {
          // ...
          this.hooks.beforeCodeGeneration.call();
          // 开始生成最终产物代码
          this.codeGeneration(/* ... */);
        }
      );
    });
  }
}

codeGeneration方法负责生成最终的资产代码,主要流程包括三个关键步骤:

  • 单模块转译:这一步主要用于计算模块实际输出代码,遍历compilation.modules数组,调用module对象的codeGeneration方法,执行模块转译计算;
  • 收集运行时依赖:计算模块运行时,首先调用compilation.processRuntimeRequirements方法,将上一步生成的runtimeRequirements数组一一转换为RuntimeModule对象,并挂载到ChunkGroup中;
  • 模块合并:调用compilation.createChunkAssets方法,以Chunk为单位,将相应的所有module及runtimeModule按规则塞进「产物框架」中,最终合并输出成完整的Bundle文件。

单模块转译

「模块转译」操作从module.codeGeneration调用开始。这个过程首先调用JavascriptGenerator.generate函数,遍历模块的dependencies数组,依次调用依赖对象对应的template.apply方法更新模块内容。重要步骤如下:

class JavascriptGenerator {
    generate(module, generateContext) {
        // 先取出 module 的原始代码内容
        const source = new ReplaceSource(module.originalSource());
        const { dependencies, presentationalDependencies } = module;
        const initFragments = [];
        for (const dependency of [...dependencies, ...presentationalDependencies]) {
            // 找到 dependency 对应的 template
            const template = generateContext.dependencyTemplates.get(dependency.constructor);
            // 调用 template.apply,传入 source、initFragments
            // 在 apply 函数可以直接修改 source 内容,或者更改 initFragments 数组,影响后续转译逻辑
            template.apply(dependency, source, {initFragments})
        }
        // 遍历完毕后,调用 InitFragment.addToSource 合并 source 与 initFragments
        return InitFragment.addToSource(source, initFragments, generateContext);
    }
}
// Dependency 子类
class xxxDependency extends Dependency {}
// Dependency 子类对应的 Template 定义
const xxxDependency.Template = class xxxDependencyTemplate extends Template {
    apply(dep, source, {initFragments}) {
        // 1. 直接操作 source,更改模块代码
        source.replace(dep.range[0], dep.range[1] - 1, 'some thing')
        // 2. 通过添加 InitFragment 实例,补充代码
        initFragments.push(new xxxInitFragment())
    }
}

这里的重点是JavascriptGenerator.generate函数并不操作module源码,它仅仅提供一个执行框架,真正处理模块内容转译的逻辑都在xxxDependencyTemplate对象的apply函数实现。

收集运行时模块

为了正常、正确运行业务项目,Webpack需要将开发者编写的业务代码以及支撑、调配这些业务代码的运行时一并打包到产物(bundle)中。大多数Webpack特性都需要特定运行时才能跑起来,包括异步加载、HMR、WASM、Module Federation等。

Webpack收集运行时依赖的过程主要集中在compilation.processRuntimeRequirements函数中,函数中包含三次循环:

  • 第一次循环遍历所有module,收集所有module的runtime依赖;
  • 第二次循环遍历所有chunk,将chunk下所有module的runtime统一收录到chunk中;
  • 第三次循环遍历所有runtime chunk,收集其对应的子chunk下所有runtime依赖,之后遍历所有依赖并发布runtimeRequirementInTree钩子,主要是RuntimePlugin插件订阅该钩子并根据依赖类型创建对应的RuntimeModule子类实例。

合并最终产物

模块合并主流程由compilation.createChunkAssets函数触发,该函数通过renderManifest钩子对外发布bundle打包需求。JavascriptModulesPlugin监听这个钩子,按照chunk的内容特性,调用不同的打包函数。核心逻辑包括:

  • 计算出bundle CMD核心代码,包含__webpack_module_cache__对象和__webpack_require__函数;
  • 计算出当前chunk下,除entry外其他模块的代码;
  • 计算出运行时模块代码;
  • 按顺序拼接bundle,包括CMD实现、runtime模块代码、其他模块代码和entry模块代码。

总结

Webpack构建过程可以简单划分为Init、Make、Seal三个阶段:

  • Init阶段负责初始化Webpack内部若干插件与状态;
  • Make阶段解决资源读入问题,递归读入、解析所有模块内容,并构建ModuleGraph;
  • Seal阶段更复杂:一方面构建ChunkGraph,另一方面转译每个模块代码,最后合并为Bundle文件。

通过深入理解Webpack的工作原理,可以帮助开发者更好地分析和解决实际开发中的问题,提升对Webpack架构和实现细节的理解。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号