rollup 插件架构-驱动设计 PluginDriver
文章目录
- Graph
- PluginDriver
- 生成 PluginDriver 实例和 PluginCache 缓存
- 创建插件上下文 pluginContext
- 初始化 pluginContext 缓存设置、方法
- 插件中使用缓存
- 可替换的 replace pluginContext
- PluginDriver 提供 asyn、first、parallel 等类型 hook
- getSortedPlugins 运行时收集并存储插件对应 hook
- sync hook
- parallel hook
- this.runHookSync
- this.runHook
- 和 webpack 插件系统区别
Graph
- 在依赖图谱 Graph 中创建 PluginDriver
- Graph 负责整个 Rollup 打包过程中模块的解析、转化、生成,所以在Graph 中创建 PlunginDriver 能够获得整个打包生命周期的模块信息
const graph = new Graph(inputOptions, watcher);
PluginDriver
生成 PluginDriver 实例和 PluginCache 缓存
- 整个 Rollup 构建生命周期中,通过 pluginDriver 这个实例去触发 hook
export default class Graph {readonly acornParser: typeof acorn.Parser;readonly cachedModules = new Map<string, ModuleJSON>();// ...entryModules: Module[] = [];readonly fileOperationQueue: Queue;readonly moduleLoader: ModuleLoader;readonly modulesById = new Map<string, Module | ExternalModule>();needsTreeshakingPass = false;phase: BuildPhase = BuildPhase.LOAD_AND_PARSE;readonly pluginDriver: PluginDriver;// ...constructor (private readonly options: NormalizedInputOptions, watcher: RollupWatcher | null) {// 根据用户设置配置插件缓存this.pluginCache = options.cache?.plugins || Object.create(null);// ...this.pluginDriver = new PluginDriver(this, options, options.plugins, this.pluginCache); // 给插件封装能调用 graph 的方法// ...}
创建插件上下文 pluginContext
- pluginContext 是交给用户在插件中可以调用的相关方法、属性
- 可以看到多个插件的上下文共享同一个 pluginCache、graph
export class PluginDriver {// ... 相关方法private readonly pluginContexts: ReadonlyMap<Plugin, PluginContext>;private readonly plugins: readonly Plugin[];private readonly sortedPlugins = new Map<AsyncPluginHooks, Plugin[]>();private readonly unfulfilledActions = new Set<HookAction>();constructor(private readonly graph: Graph,private readonly options: NormalizedInputOptions,userPlugins: readonly Plugin[],private readonly pluginCache: Record<string, SerializablePluginCache> | undefined,basePluginDriver?: PluginDriver) {// ...this.plugins = [...(basePluginDriver ? basePluginDriver.plugins : []), ...userPlugins];const existingPluginNames = new Set<string>();// 为每个插件创建一个 pluginContext this.pluginContexts = new Map(this.plugins.map(plugin => [plugin,getPluginContext(plugin, pluginCache, graph, options, this.fileEmitter, existingPluginNames)]));// ...}
}
初始化 pluginContext 缓存设置、方法
- 将每个 plugin 按照传递的配置设置缓存 key,存放进 Graph 的 pluginCache 集合中保存
export function getPluginContext(plugin: Plugin,pluginCache: Record<string, SerializablePluginCache> | void,graph: Graph,options: NormalizedInputOptions,fileEmitter: FileEmitter,existingPluginNames: Set<string>
): PluginContext {let cacheable = true;if (typeof plugin.cacheKey !== 'string') {if ( // 插件没写 name 不缓存plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) ||plugin.name.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) ||existingPluginNames.has(plugin.name)) {cacheable = false;} else {existingPluginNames.add(plugin.name);}}let cacheInstance: PluginCache;if (!pluginCache) {cacheInstance = NO_CACHE;} else if (cacheable) {// 根据插件传递的配置设置缓存 keyconst cacheKey = plugin.cacheKey || plugin.name;cacheInstance = createPluginCache( // 封装操作 cache 缓存的操作(get、has、set、delete)// 在创建时已经根据 key 分配了对象pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)));} else {cacheInstance = getCacheForUncacheablePlugin(plugin.name);}// 返回给开发者可以调用的方法、属性return {addWatchFile(id) {if (graph.phase >= BuildPhase.GENERATE) {return this.error(errorInvalidRollupPhaseForAddWatchFile());}graph.watchFiles[id] = true;},cache: cacheInstance,// ...};
}// 记录缓存操作
export function createPluginCache(cache: SerializablePluginCache): PluginCache {return {delete(id: string) {return delete cache[id];},get(id: string) {const item = cache[id];if (!item) return;item[0] = 0;return item[1];},has(id: string) {const item = cache[id];if (!item) return false;item[0] = 0;return true;},set(id: string, value: any) {cache[id] = [0, value];}};
}
插件中使用缓存
- 插件 cache 的内容都会放在 Graph 的 pluginCache 中,在分配缓存时已经根据插件的 key 进行了设置,所以在插件中可以直接 this.cache 进行使用而不必担心和其它插件的缓存冲突
{name: "test-plugin",buildStart() {if (!this.cache.has("cache")) {this.cache.set("cache", "cache something");} else {// 第二次执行rollup的时候会执行console.log(this.cache.get("cache"));}},
}
可替换的 replace pluginContext
- pluginContext 会根据不同的 hook,动态增加属性、方法,比如 transform hook
- 在通过 pluginDriver.hookReduceArg0 调用 transform hook时,第四个参数即是替换后的 pluginContext
code = await pluginDriver.hookReduceArg0('transform',[currentSource, id],transformReducer,(pluginContext, plugin): TransformPluginContext => { pluginName = plugin.name;return {...pluginContext, // 在原来 context 的基础上再添加额外的属性addWatchFile(id: string) {transformDependencies.push(id); // 收集插件中通过 this.addWatchFile 添加的文件 idpluginContext.addWatchFile(id);},cache: customTransformCache? pluginContext.cache: getTrackedPluginCache(pluginContext.cache, useCustomTransformCache),};});
PluginDriver 提供 asyn、first、parallel 等类型 hook
- rollup 根据不同场景提供了不同类型的 hook
- async:该钩子也可以返回一个解析为相同类型的值的 Promise;否则,该钩子被标记为 sync。
- first:如果有多个插件实现此钩子,则钩子按顺序运行,直到钩子返回一个不是 null 或 undefined 的值。
- sequential:如果有多个插件实现此钩子,则所有这些钩子将按指定的插件顺序运行。如果钩子是 async,则此类后续钩子将等待当前钩子解决后再运行。
- parallel:如果有多个插件实现此钩子,则所有这些钩子将按指定的插件顺序运行。如果钩子是 async,则此类后续钩子将并行运行,而不是等待当前钩子。
export class PluginDriver {// ...hookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(hookName: H,parameters: Parameters<FunctionPluginHooks[H]>,replaceContext?: ReplaceContext): ReturnType<FunctionPluginHooks[H]> | null {// ...}async hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(hookName: H,parameters: Parameters<FunctionPluginHooks[H]>,replaceContext?: ReplaceContext): Promise<void> {// ...}// ...
}
getSortedPlugins 运行时收集并存储插件对应 hook
- Rollup 通过 getSortedPlugins 对插件的对应 hook 进行排序后收集在 this.sortedPlugins 集合中存储
- 根据 pre、normal、post 顺序排序每个hook
// 抽取插件中有对应 hookName 的插件
private getSortedPlugins( hookName: keyof FunctionPluginHooks | AddonHooks,validateHandler?: (handler: unknown, hookName: string, plugin: Plugin) => void
): Plugin[] {return getOrCreate( // 根据 hookName 抽取所有符合的插件,运行时收集对应 hook 的插件放进 this.sortedPlugins 里this.sortedPlugins,hookName,() => getSortedValidatedPlugins(hookName, this.plugins, validateHandler) // 抽取插件中有对应 hookName 的插件);
}export function getOrCreate<K, V>(map: Map<K, V>, key: K, init: () => V): V {const existing = map.get(key);if (existing !== undefined) {return existing;}const value = init();map.set(key, value);return value;
}export function getSortedValidatedPlugins(hookName: keyof FunctionPluginHooks | AddonHooks,plugins: readonly Plugin[],validateHandler = validateFunctionPluginHandler
): Plugin[] {const pre: Plugin[] = [];const normal: Plugin[] = [];const post: Plugin[] = [];// 遍历所有插件,根据指定hook(options、transform、...)提取插件for (const plugin of plugins) {const hook = plugin[hookName];if (hook) {if (typeof hook === 'object') {validateHandler(hook.handler, hookName, plugin);if (hook.order === 'pre') {pre.push(plugin);continue;}if (hook.order === 'post') {post.push(plugin);continue;}} else {validateHandler(hook, hookName, plugin);}normal.push(plugin);}}// 根据 pre、normal、post 顺序排序每个hookreturn [...pre, ...normal, ...post];
}
sync hook
- 同步执行 hook,返回第一个非 null 的结果
// chains synchronously, first non-null result stops and returnshookFirstSync<H extends SyncPluginHooks & FirstPluginHooks>(hookName: H,parameters: Parameters<FunctionPluginHooks[H]>,replaceContext?: ReplaceContext): ReturnType<FunctionPluginHooks[H]> | null {for (const plugin of this.getSortedPlugins(hookName)) {const result = this.runHookSync(hookName, parameters, plugin, replaceContext);if (result != null) return result;}return null;}
parallel hook
- 用于并行执行的 hook,忽略返回值
- 通过 Promise.all 并行执行
async hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(hookName: H,parameters: Parameters<FunctionPluginHooks[H]>,replaceContext?: ReplaceContext): Promise<void> {const parallelPromises: Promise<unknown>[] = [];for (const plugin of this.getSortedPlugins(hookName)) { // getSortedPlugins 根据 hookName 提取对应的插件if ((plugin[hookName] as { sequential?: boolean }).sequential) { // 非顺序执行就先存起来然后并行执行await Promise.all(parallelPromises);parallelPromises.length = 0;await this.runHook(hookName, parameters, plugin, replaceContext);} else {parallelPromises.push(this.runHook(hookName, parameters, plugin, replaceContext)); // 将调用过程放进 Promise.then 任务中,等待 parallelPromises.push 同步任务收集完成后执行}}await Promise.all(parallelPromises); // 并行执行所有收集到的 runHook }
this.runHookSync
- 同步调用时,通过 this.pluginContexts.get(plugin) 获取到插件上下文供插件开发者使用
private runHookSync<H extends SyncPluginHooks>(hookName: H,parameters: Parameters<FunctionPluginHooks[H]>,plugin: Plugin,replaceContext?: ReplaceContext): ReturnType<FunctionPluginHooks[H]> {const hook = plugin[hookName]!;const handler = typeof hook === 'object' ? hook.handler : hook;let context = this.pluginContexts.get(plugin)!;if (replaceContext) {context = replaceContext(context, plugin);}try {// eslint-disable-next-line @typescript-eslint/ban-typesreturn (handler as Function).apply(context, parameters);} catch (error_: any) {return error(errorPluginError(error_, plugin.name, { hook: hookName }));}}
this.runHook
- 异步调用时,如果是结果也是 Promise,会暂存然后等待所有Promise都执行结束
private runHook<H extends AsyncPluginHooks | AddonHooks>(hookName: H,parameters: unknown[],plugin: Plugin,replaceContext?: ReplaceContext | null): Promise<unknown> {// We always filter for plugins that support the hook before running itconst hook = plugin[hookName];const handler = typeof hook === 'object' ? hook.handler : hook;let context = this.pluginContexts.get(plugin)!; // 获取插件的上下文,上下文通过 PluginDriver 封装了一系列和 graph 等相关的方法if (replaceContext) { //transform 钩子需要往 plugin 上下文中添加额外的内容context = replaceContext(context, plugin);}let action: [string, string, Parameters<any>] | null = null;return Promise.resolve().then(() => {if (typeof handler !== 'function') {return handler;}const hookResult = (handler as Function).apply(context, parameters); // 执行插件对应的 hookName 钩子if (!hookResult?.then) {return hookResult;}action = [plugin.name, hookName, parameters]; // 如果钩子返回 Promise 存储对应操作this.unfulfilledActions.add(action);return Promise.resolve(hookResult).then(result => { // action was fulfilledthis.unfulfilledActions.delete(action!); // 钩子返回 Promise 完成清除对应未 fullfilled 的操作return result;});}).catch(error_ => {if (action !== null) {this.unfulfilledActions.delete(action);}return error(errorPluginError(error_, plugin.name, { hook: hookName }));});}
和 webpack 插件系统区别
- Rollup 通过抽象化一个 PluginDriver 的实例负责专门驱动插件的调用,并且 PluginDriver 和 Graph 绑定,能够共享打包过程的信息;webpack 通过 tapable 进行订阅发布,本身可以脱离 webpack 使用
- 在运行模式上,Rollup 是运行时根据 hookName 收集对应的插件 ,然后对插件进行排序后存储,通过 runHook 或 runHookSync 进行调用;webpack 通过订阅发布,先注册插件,然后在生命周期的流程中调用。总的来说 Rollup 的运行时收集比起 webpack 具有一点点内存优势
相关文章:

rollup 插件架构-驱动设计 PluginDriver
文章目录 GraphPluginDriver生成 PluginDriver 实例和 PluginCache 缓存创建插件上下文 pluginContext初始化 pluginContext 缓存设置、方法插件中使用缓存可替换的 replace pluginContextPluginDriver 提供 asyn、first、parallel 等类型 hookgetSortedPlugins 运行时收集并存…...

netty实现mqtt(IOT)
springbootnettymqtt服务端实现 springbootnettymqtt客户端实现 MQTT协议基本讲解(结合netty) 李兴华netty视频教程中mqtt讲解 EMQX官网、mqttx客户端 IOT云平台 simple(6)springboot netty实现IOT云平台基本的架构(mqtt、Rabbitmq&…...

基于STC12C5A60S2系列1T 8051单片机的液晶显示器LCD1602显示汉字的功能
基于STC12C5A60S2系列1T 8051单片机的液晶显示器LCD1602显示汉字的功能 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍LCD1602字符型液晶显示器介绍一、LCD1602字符型…...

Springboot+Redis:实现缓存 减少对数据库的压力
🎉🎉欢迎光临,终于等到你啦🎉🎉 🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀 🌟持续更新的专栏Redis实战与进阶 本专栏讲解Redis从原理到实践 …...

springboot组件的单例模式和分布式分析
springboot组件的单例模式和分布式分析 一、基本概念 在Spring Boot应用中,单例模式是非常常见的一种设计模式,它被广泛应用于Bean的生命周期管理。Spring容器默认会将所有的Component、Service、Repository和Controller注解标记的类作为单例对象进行实…...

Linux:zip命令介绍
简介 zip命令可以用来解压缩文件,或者对文件进行打包操作。zip是个使用广泛的压缩程序,文件经它压缩后会另外产生具有“.zip”扩展名的压缩文件。 语法 zip [选项] [参数] 选项 -A:调整可执行的自动解压缩文件; -b<工作目录&g…...

远程桌面无法连接怎么办?
远程桌面无法连接是指在尝试使用远程桌面功能时出现连接失败的情况。这种问题可能会给工作和生活带来极大的不便,因此我们需要寻找解决办法。在讨论解决方案之前,我们先来了解一下【天联】组网的优势。 【天联】组网的优势有很多。它能够解决复杂网络环境…...

HarmonyOS实战开发-拼图、如何实现获取图片,以及图片裁剪分割的功能。
介绍 该示例通过ohos.multimedia.image和ohos.multimedia.mediaLibrary接口实现获取图片,以及图片裁剪分割的功能。 效果预览 使用说明: 使用预置相机拍照后启动应用,应用首页会读取设备内的图片文件并展示获取到的第一个图片,…...

【LeetCode热题100】【二叉树】二叉树的最近公共祖先
题目链接:236. 二叉树的最近公共祖先 - 力扣(LeetCode) 二叉树皆可递归,可以递归查找两个节点的所在地,如果两个节点一个在root的左子树一个在右子树,说明root就是公共祖先,并且因为是递归&…...

动态规划专练( 1049.最后一块石头的重量Ⅱ)
1049.最后一块石头的重量Ⅱ 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。 每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x < y。那么粉碎的可能结果如…...

2024年最佳WordPress插件
我喜欢的最佳WordPress插件(也是经验丰富的WordPress开发者强烈推荐的)。所有这些插件都是编码干净、超快且一流的。我还包括了对我不喜欢的插件的想法……只为了让你有进一步的了解。 目录 隐藏 1 古腾堡块: 2 内容: 3 缓存…...

Docker 安装 RocketMQ
目录 一、新建两个配置文件 1.1 创建docker-compose.yml文件 1.2 .新建broker.conf文件 二、运行 三、可视化界面 一、新建两个配置文件 1.1 创建docker-compose.yml文件 version: 3.5 services:rmqnamesrv:image: foxiswho/rocketmq:servercontainer_name: rmqnamesrvports…...

计算机网络——交换机和路由器
目录 前言 引言 交换机是用来做什么的? 与路由器有什么区别? 网关 子网掩码 网关、路由 前言 本博客是博主用于复习计算机网络的博客,如果疏忽出现错误,还望各位指正。 这篇博客是在B站掌芝士zzs这个UP主的视频的总结&am…...

Redis Pipelining 底层原理分析及实践
作者:vivo 互联网服务器团队-Wang Fei Redis是一种基于客户端-服务端模型以及请求/响应的TCP服务。在遇到批处理命令执行时,Redis提供了Pipelining(管道)来提升批处理性能。本文结合实践分析了Spring Boot框架下Redis的Lettuce客户端和Redisson客户端对P…...

milvus各组件的结构体分析
milvus各组件的结构体分析 各组件启动,需要构建各组件的结构体,一共8个。 runComponent(ctx, localMsg, wg, components.NewRootCoord, metrics.RegisterRootCoord) runComponent(ctx, localMsg, wg, components.NewProxy, metrics.RegisterProxy) run…...

vue2和vue3 全选
vue3 <template><input type"checkbox" v-model"selectAll" />全选<ul><li v-for"item in list" :key"item.id">{{ item.value }} <input type"checkbox" v-model"item.check" />…...

Java中的Set、List、Map的区别及主要实现类方法
Java中的Set、List、Map的区别 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操作数目不固定的一组数据。 所有的JAVA集合都位于 java.util包中! JAVA集合只能存放引…...

gitignore:常用说明
示例: Java HELP.md target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/** !**/src/test/**### IntelliJ IDEA.idea *.iws *.iml *.ipr### NetBeans/nbproject/private/ /nbbuild/ /dist/ /nbdist/ /.nb-gradle/ build/ logs/### VS Code.vscode/ 说明&#…...

HarmonyOS NEXT应用开发—在Native侧实现进度通知功能
介绍 本示例通过模拟下载场景介绍如何将Native的进度信息实时同步到ArkTS侧。 效果图预览 使用说明 点击“Start Download“按钮后,Native侧启动子线程模拟下载任务Native侧启动子线程模拟下载,并通过Arkts的回调函数将进度信息实时传递到Arkts侧 实…...

水利自动化控制系统平台介绍
水利自动化控制系统平台介绍 在当今社会,水资源的管理和保护日益成为全球关注的重要议题。随着科技的进步和信息化的发展,水利监测系统作为一种集成了现代信息技术、自动化控制技术以及环境监测技术的综合性平台,正在逐步改变传统的水利管理模…...

flask后端+网页前端:基于 socket.io 的双向通信和服务器部署
我想实现的效果是,我的服务器提供两个路由网址,网页A用于拍照、然后录音,把照片和录音传给服务器,服务器发射信号,通知另一个路由的网页B更新,把刚刚传来的照片和录音显示在网页上。 然后网页B用户根据这个…...

【Docker】解决 docker build 提示 `Wrong architecture ‘amd64‘`
解决 docker build 提示 Wrong architecture amd64 使用 securify2 的 docker 版本进行 sc 安全扫描 执行语句 RUN wget https://github.com/souffle-lang/souffle/releases/download/1.6.2/souffle_1.6.2-1_amd64.deb -O /tmp/souffle.deb &&\ gdebi --n /tmp/souff…...

机器学习_XGBoost模型_用C++推理示例Demo
1. 需求 将 python 训练好的 xgboost 模型, 使用C 进行加载并进行推理(预测) 2. 代码实现 #include <iostream> #include <fstream> #include <sstream> #include <vector> #include <string> #include <xgboost/c_api.h>const char *m…...

C语言 | Leetcode C语言题解之第21题合并两个有序链表
题目: 题解: /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/typedef struct ListNode ListNode; struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {/…...

2024考研调剂须知
----------------------------------------------------------------------------------------------------- 考研复试科研背景提升班 教你快速深入了解掌握考研复试面试中的常见问题以及注意事项,系统的教你如何在短期内快速提升自己的专业知识水平和编程以及英语…...

PCIE协议版--M.2接口规范V1.0中文版1——电气规格篇
3.电气规范 3.1 Connectivity Socket 1 系统接口信号 表15适用于Socket 1-SD和Socket 1-DP输出版本。 3.1.1.补充NFC信号 当一个SIM设备被用作安全元素时,NFC解决方案可以与表16中列出的附加信号相结合。 3.1.2.电源和地 PCI Express M.2 Socket 1使用一个3.3 V…...

【JVM】JVM堆占用情况分析(频繁创建的对象、内存泄露等问题)、jmap+jhat、jvisualvm工具使用
文章目录 一. 相关命令1. 查看进程堆内存整体使用情况:OOM的可能2. 统计类的对象数量以及内存占用:定位内存泄漏 二. 分析内存占用1. 使用 jhat 排查对象堆占用情况1.1. 排查步骤1.2. 具体分析例子a. 分析频繁创建对象导致的OOM 1.3. OQL查看某一个对象的…...

【蓝桥杯每日一题】4.11 更小的数(不用区间DP)
题目来源: 蓝桥杯 2023 省 A]更小的数 - 洛谷 这题只需要用到双指针就OK~ 思路1: 翻转数组的子数组,然后进行比较大小将翻转后的数组存储在字符串 k k k中,然后将字符串 k k k与字符串 a a a进行逐一元素比较(因为…...

【线段树】2276. 统计区间中的整数数目
算法可以发掘本质,如: 一,若干师傅和徒弟互有好感,有好感的师徒可以结对学习。师傅和徒弟都只能参加一个对子。如何让对子最多。 二,有无限多1X2和2X1的骨牌,某个棋盘若干格子坏了,如何在没有坏…...

ChatGPT 写作利器:探索ChatGPT在论文写作中的应用
ChatGPT无限次数:点击直达 ChatGPT 写作利器:探索ChatGPT在论文写作中的应用 引言 ChatGPT是一种强大的自然语言处理工具,能够为我们提供高效、准确的文本生成功能。在论文写作领域,ChatGPT的应用也逐渐受到关注。本文将探讨ChatGPT在论文写…...