跳到主要内容

认识

2023年03月05日
柏拉文
越努力,越幸运

一、认识


RollupAST 解析的功能开始,完成代码的词法分析(tokenize)和语义分析(parse),实现模块依赖图和作用域链的搭建,并完成 Tree Shaking、循环依赖检测及 Bundle 代码生成,最终实现一个类似 RollupBundler

在执行 rollup 命令之后,在 cli 内部的主要逻辑简化如下:

// Build 阶段
const bundle = await rollup.rollup(inputOptions);

// Output 阶段
await Promise.all(outputOptions.map(bundle.write));

// 构建结束
await bundle.close();

Rollup 内部主要经历了 BuildOutput 两大阶段:

Preview
  1. Build 阶段: 主要负责创建模块依赖图,初始化各个模块的 AST 以及模块之间的依赖关系, 返回一个 Bundle 对象, 这个对象的作用在于存储各个模块的内容及依赖关系,同时暴露generatewrite方法,以进入到后续的 Output 阶段(writegenerate方法唯一的区别在于前者打包完产物会写入磁盘,而后者不会)。

    // src/index.js
    import { a } from './module-a';
    console.log(a);

    // src/module-a.js
    export const a = 1;
    const rollup = require('rollup');
    const util = require('util');
    async function build() {
    const bundle = await rollup.rollup({
    input: ['./src/index.js'],
    });
    console.log(util.inspect(bundle));
    }
    build();
  2. Output: 完成打包及输出的过程

    const rollup = require('rollup');
    async function build() {
    const bundle = await rollup.rollup({
    input: ['./src/index.js'],
    });
    const result = await bundle.generate({
    format: 'es',
    });
    console.log('result:', result);
    }

    build();

对于一次完整的构建过程而言, Rollup 会先进入到 Build 阶段,解析各模块的内容及依赖关系,然后进入 Output 阶段,完成打包及输出的过程。对于不同的阶段,Rollup 插件会有不同的插件工作流程

二、Build 工作流


对于 Build 阶段,插件 Hook 的调用流程如下图所示。流程图的最上面声明了不同 Hook 的类型,也就是我们在上面总结的 5Hook 分类,每个方块代表了一个 Hook,边框的颜色可以表示AsyncSync 类型,方块的填充颜色可以表示ParallelSequentialFirst 类型。

Preview
  1. 首先经历 options 钩子进行配置的转换,得到处理后的配置对象。

  2. 随之 Rollup 会调用 buildStart 钩子,正式开始构建流程。

  3. Rollup 先进入到 resolveId 钩子中解析文件路径。(从 input 配置指定的入口文件开始)。

  4. Rollup 通过调用 load 钩子加载模块内容。

  5. 紧接着 Rollup 执行所有的 transform 钩子来对模块内容进行进行自定义的转换,比如 babel 转译。

  6. 现在 Rollup 拿到最后的模块内容,进行 AST 分析,得到所有的 import 内容,调用 moduleParsed 钩子:

    • 6.1 如果是普通的 import,则执行 resolveId 钩子,继续回到步骤3。

    • 6.2 如果是动态 import,则执行 resolveDynamicImport 钩子解析路径,如果解析成功,则回到步骤4加载模块,否则回到步骤3通过 resolveId 解析路径。

  7. 直到所有的 import 都解析完毕,Rollup 执行buildEnd钩子,Build 阶段结束。

当然,在 Rollup 解析路径的时候,即执行 resolveId 或者 resolveDynamicImport 的时候,有些路径可能会被标记为 external(翻译为排除),也就是说不参加 Rollup 打包过程,这个时候就不会进行loadtransform等等后续的处理了。

在流程图最上面,不知道大家有没有注意到watchChangecloseWatcher这两个 Hook,这里其实是对应了 rollupwatch 模式。当你使用 rollup --watch 指令或者在配置文件配有 watch: true 的属性时,代表开启了 Rollupwatch 打包模式,这个时候 Rollup 内部会初始化一个 watcher 对象,当文件内容发生变化时,watcher 对象会自动触发 watchChange 钩子执行并对项目进行重新构建。在当前打包过程结束时,Rollup 会自动清除 watcher 对象调用 closeWacher 钩子。

三、Output 工作流


Preview
  1. 执行所有插件的 outputOptions 钩子函数,对 output 配置进行转换。

  2. 执行 renderStart,并发执行 renderStart 钩子,正式开始打包。

  3. 并发执行所有插件的bannerfooterintrooutro 钩子(底层用 Promise.all 包裹所有的这四种钩子函数),这四个钩子功能很简单,就是往打包产物的固定位置(比如头部和尾部)插入一些自定义的内容,比如协议声明内容、项目介绍等等。

  4. 从入口模块开始扫描,针对动态 import 语句执行 renderDynamicImport 钩子,来自定义动态 import 的内容。

  5. 对每个即将生成的 chunk,执行 augmentChunkHash 钩子,来决定是否更改 chunk 的哈希值,在 watch 模式下即可能会多次打包的场景下,这个钩子会比较适用。

  6. 如果没有遇到 import.meta 语句,则进入下一步,否则:

    • 6.1 对于 import.meta.url 语句调用 resolveFileUrl 来自定义 url 解析逻辑

    • 6.2 对于其他 import.meta 属性,则调用 resolveImportMeta 来进行自定义的解析。

  7. 接着 Rollup 会生成所有 chunk 的内容,针对每个 chunk 会依次调用插件的 renderChunk 方法进行自定义操作,也就是说,在这里时候你可以直接操作打包产物了。

  8. 随后会调用 generateBundle 钩子,这个钩子的入参里面会包含所有的打包产物信息,包括 chunk (打包后的代码)、asset(最终的静态资源文件)。你可以在这里删除一些 chunk 或者 asset,最终这些内容将不会作为产物输出。

  9. 前面提到了rollup.rollup方法会返回一个bundle对象,这个对象是包含generatewrite两个方法,两个方法唯一的区别在于后者会将代码写入到磁盘中,同时会触发writeBundle钩子,传入所有的打包产物信息,包括 chunkasset,和 generateBundle钩子非常相似。不过值得注意的是,这个钩子执行的时候,产物已经输出了,而 generateBundle 执行的时候产物还并没有输出。顺序如下图所示:

    Preview
  10. 当上述的 bundleclose 方法被调用时,会触发 closeBundle 钩子,到这里 Output 阶段正式结束。

注意: 当打包过程中任何阶段出现错误,会触发 renderError 钩子,然后执行 ``closeBundle` 钩子结束打包。