从零到一解读Rollup Plugin

时间:2022-06-03 02:33:55

从零到一解读Rollup Plugin

rollup plugin 这篇文章读读改改,终于和大家见面啦~~~

尽管对于 rollup 的插件编写,官网上对于 rolup 插件的介绍几乎都是英文,学习起来不是很友好, 例子也相对较少,但目前针对 rollup 插件的分析与开发指南的文章已经不少见,以关于官方英文文档的翻译与函数钩子的分析为主。

讲道理,稀里糊涂直接看源码分析只会分分钟劝退我,而我只想分分钟写个 rollup 插件而已~~

rollup 为什么需要 Plugin

rollup -c 打包流程

在 rollup 的打包流程中,通过相对路径,将一个入口文件和一个模块创建成了一个简单的 bundle。随着构建更复杂的 bundle,通常需要更大的灵活性——引入 npm 安装的模块、通过 Babel 编译代码、和 JSON 文件打交道等。通过 rollup -c 打包的实现流程可以参考下面的流程图理解。

从零到一解读Rollup Plugin

为此,我们可以通过 插件(plugins) 在打包的关键过程中更改 Rollup 的行为。

这其实和 webpack 的插件相类似,不同的是,webpack 区分 loader 和 plugin,而 rollup 的 plugin 既可以担任 loader 的角色,也可以胜任传统 plugin 的角色。

理解 rollup plugin

引用官网的解释:Rollup 插件是一个具有下面描述的一个或多个属性、构建钩子和输出生成钩子的对象,它遵循我们的约定。一个插件应该作为一个包来分发,该包导出一个可以用插件特定选项调用的函数,并返回这样一个对象。插件允许你定制 Rollup 的行为,例如,在捆绑之前编译代码,或者在你的 node_modules 文件夹中找到第三方模块。

简单来说,rollup 的插件是一个普通的函数,函数返回一个对象,该对象包含一些属性(如 name),和不同阶段的钩子函数(构建 build 和输出 output 阶段),此处应该回顾下上面的流程图。

关于约定

  • 插件应该有一个带有 rollup-plugin-前缀的明确名称。
  • 在 package.json 中包含 rollup-plugin 关键字。
  • 插件应该支持测试,推荐 mocha 或者 ava 这类开箱支持 promises 的库。
  • 尽可能使用异步方法。
  • 用英语记录你的插件。
  • 确保你的插件输出正确的 sourcemap。
  • 如果你的插件使用 'virtual modules'(比如帮助函数),给模块名加上 \0 前缀。这可以阻止其他插件执行它。

分分钟写个 rollup 插件

为了保持学习下去的热情与动力,先举个栗子压压惊,如果看到插件实现的各种源码函数钩子部分觉得脑子不清醒了,欢迎随时回来重新看这一小节,重拾勇气与信心!

插件其实很简单

可以打开rollup 插件列表,随便找个你感兴趣的插件,看下源代码。

有不少插件都是几十行,不超过 100 行的。比如图片文件多格式支持插件@rollup/plugin-image 的代码甚至不超过 50 行,而将 json 文件转换为 ES6 模块的插件@rollup/plugin-json 源代码更少。

一个例子

  1. // 官网的一个例子 
  2. export default function myExample () { 
  3.   return { 
  4.     name'my-example', // 名字用来展示在警告和报错中 
  5.     resolveId ( source ) { 
  6.       if (source === 'virtual-module') { 
  7.         return source; // rollup 不应该查询其他插件或文件系统 
  8.       } 
  9.       return null; // other ids 正常处理 
  10.     }, 
  11.     load ( id ) { 
  12.       if (id === 'virtual-module') { 
  13.         return 'export default "This is virtual!"'; // source code for "virtual-module" 
  14.       } 
  15.       return null; // other ids 
  16.     } 
  17.   }; 
  18.  
  19. // rollup.config.js 
  20. import myExample from './rollup-plugin-my-example.js'
  21. export default ({ 
  22.   input: 'virtual-module', // 配置 virtual-module 作为入口文件满足条件通过上述插件处理 
  23.   plugins: [myExample()], 
  24.   output: [{ 
  25.     file: 'bundle.js'
  26.     format: 'es' 
  27.   }] 
  28. }); 

光看不练假把式,模仿写一个:

  1. // 自己编的一个例子 QAQ 
  2. export default function bundleReplace () { 
  3.   return { 
  4.     name'bundle-replace', // 名字用来展示在警告和报错中 
  5.     transformBundle(bundle) { 
  6.       return bundle 
  7.         .replace('key_word''replace_word'
  8.         .replace(/正则/, '替换内容'); 
  9.     }, 
  10.   }; 
  11.  
  12. // rollup.config.js 
  13. import bundleReplace from './rollup-plugin-bundle-replace.js'
  14. export default ({ 
  15.   input: 'src/main.js', // 通用入口文件 
  16.   plugins: [bundleReplace()], 
  17.   output: [{ 
  18.     file: 'bundle.js'
  19.     format: 'es' 
  20.   }] 
  21. }); 

嘿!这也不难嘛~~~

rollup plugin 功能的实现

我们要讲的 rollup plugin 也不可能就这么简单啦~~~

接下来当然是结合例子分析实现原理~~

其实不难发现,rollup 的插件配置与 webpack 等框架中的插件使用大同小异,都是提供配置选项,注入当前构建结果相关的属性与方法,供开发者进行增删改查操作。

那么插件写好了,rollup 是如何在打包过程中调用它并实现它的功能的呢?

相关概念

首先还是要了解必备的前置知识,大致浏览下 rollup 中处理 plugin 的方法,基本可以定位到 PluginContext.ts(上下文相关)、PluginDriver.ts(驱动相关)、PluginCache.ts(缓存相关)和 PluginUtils.ts(警告错误异常处理)等文件,其中最关键的就在 PluginDriver.ts 中了。

首先要清楚插件驱动的概念,它是实现插件提供功能的的核心 -- PluginDriver,插件驱动器,调用插件和提供插件环境上下文等。

钩子函数的调用时机

大家在研究 rollup 插件的时候,最关注的莫过于钩子函数部分了,钩子函数的调用时机有三类:

  • const chunks = rollup.rollup 执行期间的构建钩子函数 - Build Hooks
  • chunks.generator(write)执行期间的输出钩子函数 - Output Generation Hooks
  • 监听文件变化并重新执行构建的 rollup.watch 执行期间的 watchChange 钩子函数

钩子函数处理方式分类

除了以调用时机来划分钩子函数以外,我们还可以以钩子函数处理方式来划分,这样来看钩子函数就主要有以下四种版本:

  • async: 处理 promise 的异步钩子,即这类 hook 可以返回一个解析为相同类型值的 promise,同步版本 hook 将被标记为 sync。
  • first: 如果多个插件实现了相同的钩子函数,那么会串式执行,从头到尾,但是,如果其中某个的返回值不是 null 也不是 undefined 的话,会直接终止掉后续插件。
  • sequential: 如果多个插件实现了相同的钩子函数,那么会串式执行,按照使用插件的顺序从头到尾执行,如果是异步的,会等待之前处理完毕,在执行下一个插件。
  • parallel: 同上,不过如果某个插件是异步的,其后的插件不会等待,而是并行执行,这个也就是我们在 rollup.rollup() 阶段看到的处理方式。

构建钩子函数

为了与构建过程交互,你的插件对象需要包含一些构建钩子函数。构建钩子是构建的各个阶段调用的函数。构建钩子函数可以影响构建执行方式、提供构建的信息或者在构建完成后修改构建。rollup 中有不同的构建钩子函数,在构建阶段执行时,它们被 [rollup.rollup(inputOptions)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L34) 触发。

构建钩子函数主要关注在 Rollup 处理输入文件之前定位、提供和转换输入文件。构建阶段的第一个钩子是 options,最后一个钩子总是 buildEnd,除非有一个构建错误,在这种情况下 closeBundle 将在这之后被调用。

顺便提一下,在观察模式下,watchChange 钩子可以在任何时候被触发,以通知新的运行将在当前运行产生其输出后被触发。当 watcher 关闭时,closeWatcher 钩子函数将被触发。

输出钩子函数

输出生成钩子函数可以提供关于生成的包的信息并在构建完成后立马执行。它们和构建钩子函数拥有一样的工作原理和相同的类型,但是不同的是它们分别被 ·[bundle.generate(output)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L44) 或 [bundle.write(outputOptions)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/cli/run/build.ts#L64) 调用。只使用输出生成钩子的插件也可以通过输出选项传入,因为只对某些输出运行。

输出生成阶段的第一个钩子函数是 outputOptions,如果输出通过 bundle.generate(...) 成功生成则第一个钩子函数是 generateBundle,如果输出通过 [bundle.write(...)](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/watch/watch.ts#L200) 生成则最后一个钩子函数是 [writeBundle](https://github.com/rollup/rollup/blob/master/src/rollup/rollup.ts#L176),另外如果输出生成阶段发生了错误的话,最后一个钩子函数则是 renderError。

另外,closeBundle 可以作为最后一个钩子被调用,但用户有责任手动调用 bundle.close() 来触发它。CLI 将始终确保这种情况发生。

以上就是必须要知道的概念了,读到这里好像还是看不明白这些钩子函数到底是干啥的!那么接下来进入正题!

钩子函数加载实现

[PluginDriver](https://github.com/rollup/rollup/blob/07b3a02069594147665daa95d3fa3e041a82b2d0/src/utils/PluginDriver.ts#L124) 中有 9 个 hook 加载函数。主要是因为每种类别的 hook 都有同步和异步的版本。

接下来先康康 9 个 hook 加载函数及其应用场景(看完第一遍不知所以然,但是别人看了咱也得看,先看了再说,看不懂就多看几遍 QAQ~)

排名不分先后,仅参考它们在 PluginDriver.ts 中出现的顺序