今天我们将探讨如何构建一个支持并发执行的插件系统。在上一篇《从零构建一个插件系统 2. 串行插件系统》中,我们实现了一个功能分离且具备完整 TypeScript 类型推导的串行系统。本篇将在此基础上,通过引入并发机制来显著提升插件的整体执行效率。
要实现带有依赖关系的并发,一个核心的前置知识是 拓扑排序 (Topological Sorting) 。若无依赖关系,简单的 Promise.all
即可满足需求。然而,在真实的插件生态中,插件间往往存在复杂的执行顺序依赖,这正是拓扑排序发挥作用的场景。
拓扑排序
拓扑排序 是一种应用于 有向无环图 (Directed Acyclic Graph, DAG) 的算法,它能将图中的所有顶点排成一个线性序列。该序列满足以下条件:对于图中任意一条从顶点
u
指向顶点v
的边,u
在序列中都出现在v
之前。
这个定义可以简化为:分析一个任务集合的依赖关系,并给出一个可行的执行顺序。
以一个插件系统为例,假设存在插件 A、B、C、D,其依赖关系如下:
- 插件 B 依赖插件 A (
A → B
) - 插件 C 依赖插件 B (
B → C
) - 插件 D 无任何依赖
这个关系可以表示为如下的 DAG:
txt
A → B → C
D
根据拓扑排序,一个可行的执行顺序是 [A, D, B, C]
或 [D, A, B, C]
。这意味着 A 和 D 可以并发执行,A 执行完毕后才能执行 B,B 执行完毕后才能执行 C。
算法实现
根据上述的例子,以下是拓扑排序的一种经典实现,基于 Kahn 算法 ,它利用队列和节点的 入度 (In-degree) 来完成排序。
ts
// 定义插件的依赖关系
type PluginDependency = {
[plugin: string]: string[]; // key: 插件名, value: 依赖的插件名列表
};
/**
* 对插件依赖进行拓扑排序
* @param dependencies 插件及其依赖项的映射
* @returns 按执行顺序列出的插件名称数组
* @throws 如果检测到循环依赖,则抛出错误
*/
function topologicalSort(dependencies: PluginDependency): string[] {
// 步骤 1: 构建邻接表 (Adjacency List) 和入度表 (In-degree Map)
const graph = new Map<string, string[]>(); // 存储依赖关系:A -> [B] (B依赖A)
const inDegree = new Map<string, number>(); // 存储每个插件的依赖数量
const plugins = Object.keys(dependencies);
// 初始化数据结构
for (const plugin of plugins) {
graph.set(plugin, []);
inDegree.set(plugin, 0);
}
// 填充邻接表和入度表
for (const [plugin, prerequisites] of Object.entries(dependencies)) {
for (const prereq of prerequisites) {
if (graph.has(prereq)) {
// 'plugin' 依赖 'prereq', 添加一条从 prereq -> plugin 的边
graph.get(prereq)!.push(plugin);
// 'plugin' 的入度加一
inDegree.set(plugin, inDegree.get(plugin)! + 1);
}
}
}
// 步骤 2: 将所有入度为 0 的节点加入队列
const queue: string[] = [];
for (const [plugin, degree] of inDegree.entries()) {
if (degree === 0) {
queue.push(plugin);
}
}
// 步骤 3: 处理队列中的节点
const sortedOrder: string[] = [];
while (queue.length > 0) {
const currentPlugin = queue.shift()!;
sortedOrder.push(currentPlugin);
// 遍历当前节点的所有邻接点(即依赖当前节点的插件)
const dependentPlugins = graph.get(currentPlugin) || [];
for (const dependent of dependentPlugins) {
// 将其入度减 1
const newDegree = inDegree.get(dependent)! - 1;
inDegree.set(dependent, newDegree);
// 如果入度变为 0,则加入队列
if (newDegree === 0) {
queue.push(dependent);
}
}
}
// 步骤 4: 检查是否存在循环依赖
if (sortedOrder.length !== plugins.length) {
const cyclicPlugins = plugins.filter((p) => !sortedOrder.includes(p));
throw new Error(
`检测到循环依赖,无法完成排序。相关插件: ${cyclicPlugins.join(", ")}`
);
}
return sortedOrder;
}
// 示例
const pluginDependencies: PluginDependency = {
a: [],
b: ["a"],
c: ["b"],
d: [],
};
console.log("插件执行顺序:", topologicalSort(pluginDependencies));
// 输出: 插件执行顺序: [ 'a', 'd', 'b', 'c' ] (或 'd', 'a', ...)
下面是上述代码的执行流程模拟:
-
构建图:
graph
:Map { 'a' => ['b'], 'b' => ['c'], 'c' => [], 'd' => [] }
inDegree
:Map { 'a' => 0, 'b' => 1, 'c' => 1, 'd' => 0 }
-
初始化队列: 遍历
inDegree
,将入度为 0 的a
和d
加入队列。queue
:['a', 'd']
sortedOrder
:[]
-
处理队列:
- 第一次循环:
queue.shift()
取出a
。sortedOrder
变为['a']
。a
的邻接点b
的入度从 1 减为 0,b
入队。- 当前状态:
queue: ['d', 'b']
,sortedOrder: ['a']
- 第二次循环:
queue.shift()
取出d
。sortedOrder
变为['a', 'd']
。d
没有邻接点。- 当前状态:
queue: ['b']
,sortedOrder: ['a', 'd']
- 第三次循环:
queue.shift()
取出b
。sortedOrder
变为['a', 'd', 'b']
。b
的邻接点c
的入度从 1 减为 0,c
入队。- 当前状态:
queue: ['c']
,sortedOrder: ['a', 'd', 'b']
- 第四次循环:
queue.shift()
取出c
。sortedOrder
变为['a', 'd', 'b', 'c']
。c
没有邻接点。- 当前状态:
queue: []
,sortedOrder: ['a', 'd', 'b', 'c']
- 第一次循环:
-
结束:
queue
为空,循环结束。sortedOrder.length
等于插件总数,排序成功。
整个代码的流程其实就是统计相关的依赖,然后初次执行入度为 0 的插件,之后根据邻接表来继续插入入度为 0 的插件继续执行,重复直到结束。
插件系统架构重构
为了在系统中更好地集成依赖管理,我们首先将插件的定义从函数式转向面向对象。
为何选择 Class
在设计插件系统时,使用函数或类都是可行的选择。然而,对于一个需要处理复杂依赖关系和内部状态的系统,基于 Class
有明显优势:
- 函数式: 依赖关系通常通过字符串 ID 来声明,如
dependencies: ['plugin-a']
。这种方式是脆弱的,容易因拼写错误而失效,且缺乏编译时检查。 - Class: 依赖关系可以直接通过类引用来声明,如
dependencies: [PluginA]
。这种方式是类型安全的,IDE 可以提供自动补全和重构支持,极大地提升了开发体验和代码的健壮性。
综上,虽然函数式插件更轻量,但在构建一个需要处理复杂依赖、配置和状态的并发插件系统时,Class
提供了更结构化、更健壮和更易于扩展的解决方案。
types.ts
关于也进行了调整,不过不阅读也不影响理解文章,但是为了完整性,下面还是贴了一下完整的类型实现。
点击查看:types.ts 定义
ts
/**
* @file types.ts
* @description 在此文件中集中定义所有核心类型,以便于管理和复用。
*/
// --- 基础数据结构 ---
/**
* 代表从源(如 GitHub Issues)拉取的基础资源。
*/
export type Issue = {
id: number;
title: string; // 新增标题字段,用于专栏处理
content: string;
};
// --- 生命周期上下文 API ---
/**
* `load` 生命周期钩子中可用的上下文 API。
*/
export type LoadContext = {
emitIssue: (issue: Issue) => void;
};
/**
* `transform` 生命周期中共享的数据上下文。
*/
export type TransformContext = {
issues: Issue[];
summary?: string;
thumbnail?: string;
// 新增专栏字段,用于存放处理结果
// 格式为:{ [专栏名]: issueId[] }
columns?: Record<string, number[]>;
};
/**
* `generate` 生命周期钩子中可用的上下文 API。
*/
export type GenerateContext = {
writeFile: (filePath: string, content: string) => Promise<void>;
};
// --- 插件与钩子注册 ---
/**
* 定义了插件可以注册的各种生命周期回调函数。
*/
export type PluginHooksRegister = {
onLoad: (callback: (ctx: LoadContext) => Promise<void> | void) => void;
onTransform: (
callback: (ctx: TransformContext) => Promise<void> | void
) => void;
onGenerate: (
callback: (
ctx: Readonly<TransformContext>,
api: GenerateContext
) => Promise<void> | void
) => void;
};
// --- 插件类定义 ---
/**
* 代表插件类的构造函数类型。
*/
export type PluginClass = new () => Plugin;
/**
* 所有插件必须继承的抽象基类。
*/
export abstract class Plugin {
/**
* 此插件依赖的其他插件类的列表(可选)。
*/
dependencies?: PluginClass[];
/**
* 插件的名称,自动从其类名派生。
*/
get name(): string {
return this.constructor.name;
}
/**
* 插件的核心逻辑入口。
*/
abstract apply(hooks: PluginHooksRegister): void;
}
插件实现
所有插件继承自一个 Plugin
抽象基类,并通过 dependencies
属性声明依赖,在 apply
方法中注册逻辑。
ts
export class ExtractSummary extends Plugin {
// 通过类引用声明依赖
dependencies: PluginClass[] = [ImageProxy];
apply(hooks: PluginHooksRegister) {
hooks.onTransform(async (ctx) => {
// ... 插件逻辑
});
}
}
点击查看:所有插件的实现示例代码
ts
import { Plugin, PluginClass, PluginHooksRegister } from "./types";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
/** 1. 拉取 Issues */
export class FetchIssues extends Plugin {
apply(hooks: PluginHooksRegister) {
hooks.onLoad(async (ctx) => {
/* ... */
});
}
}
/** 2. 处理图片链接 (无依赖) */
export class ImageProxy extends Plugin {
apply(hooks: PluginHooksRegister) {
hooks.onTransform(async (ctx) => {
/* ... */
});
}
}
/** 3. 处理专栏 (无依赖) */
export class ProcessColumns extends Plugin {
apply(hooks: PluginHooksRegister) {
hooks.onTransform(async (ctx) => {
/* ... */
});
}
}
/** 4. 提取摘要 (依赖图片处理) */
export class ExtractSummary extends Plugin {
dependencies: PluginClass[] = [ImageProxy];
apply(hooks: PluginHooksRegister) {
hooks.onTransform(async (ctx) => {
/* ... */
});
}
}
/** 5. 提取缩略图 (依赖图片处理) */
export class ExtractThumbnail extends Plugin {
dependencies: PluginClass[] = [ImageProxy];
apply(hooks: PluginHooksRegister) {
hooks.onTransform(async (ctx) => {
/* ... */
});
}
}
/** 6. 生成最终的 JSON 输出 */
export class Output extends Plugin {
constructor(options: { prefix?: string } = {}) {
super(); /* ... */
}
apply(hooks: PluginHooksRegister) {
hooks.onGenerate(async (ctx) => {
/* ... */
});
}
}
PluginSystem
实现
PluginSystem
类是整个系统的协调器,负责插件的注册、生命周期的管理以及并发调度。下面为 PluginSystem
使用方式。
ts
const system = new PluginSystem();
// 注册插件实例
system
.use(new FetchIssues())
.use(new ImageProxy())
.use(new ProcessColumns())
.use(new ExtractSummary())
.use(new Output());
// 运行整个系统
await system.run();
core.ts
这里提前贴一下插件系统的实现部分,后续的代码讲解基于这个文件来进行。
ts
import fs from "fs/promises";
import path from "path";
import {
Plugin,
PluginHooksRegister,
LoadContext,
TransformContext,
GenerateContext,
Issue,
} from "./types";
type LoadCallback = (ctx: LoadContext) => Promise<void> | void;
type TransformCallback = (ctx: TransformContext) => Promise<void> | void;
type GenerateCallback = (
ctx: Readonly<TransformContext>,
api: GenerateContext
) => Promise<void> | void;
type LifecycleMapping = {
load: { callback: LoadCallback; args: [LoadContext] };
transform: { callback: TransformCallback; args: [TransformContext] };
generate: {
callback: GenerateCallback;
args: [Readonly<TransformContext>, GenerateContext];
};
};
export class PluginSystem {
private plugins: Plugin[] = [];
private hooks: {
load: Map<Plugin, LoadCallback>;
transform: Map<Plugin, TransformCallback>;
generate: Map<Plugin, GenerateCallback>;
} = { load: new Map(), transform: new Map(), generate: new Map() };
use(plugin: Plugin): this {
this.plugins.push(plugin);
return this;
}
private collectHooks() {
for (const plugin of this.plugins) {
const register: PluginHooksRegister = {
onLoad: (callback) => this.hooks.load.set(plugin, callback),
onTransform: (callback) => this.hooks.transform.set(plugin, callback),
onGenerate: (callback) => this.hooks.generate.set(plugin, callback),
};
plugin.apply(register);
}
}
async run() {
this.collectHooks();
console.log("\n--- [load] 生命周期开始 ---");
const issues: Issue[] = [];
const loadContext: LoadContext = {
emitIssue: (issue) => issues.push(issue),
};
await this.runLifecycle("load", loadContext);
console.log("\n--- [transform] 生命周期开始 ---");
const transformContext: TransformContext = { issues };
await this.runLifecycle("transform", transformContext);
console.log("\n--- [generate] 生命周期开始 ---");
const generateContext: GenerateContext = {
writeFile: async (filePath, content) => {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, "utf-8");
console.log(`[Generate] 产物已生成: ${filePath}`);
},
};
await this.runLifecycle(
"generate",
Object.freeze(transformContext),
generateContext
);
return transformContext;
}
private async runLifecycle<K extends keyof LifecycleMapping>(
lifecycle: K,
...args: LifecycleMapping[K]["args"]
) {
const lifecycleHooks = this.hooks[lifecycle];
const activePlugins = this.plugins.filter((p) => lifecycleHooks.has(p));
if (activePlugins.length === 0) return;
const adj = new Map<string, string[]>();
const inDegree = new Map<string, number>();
const classToInstances = new Map<string, Plugin[]>();
for (const plugin of activePlugins) {
const className = plugin.constructor.name;
if (!classToInstances.has(className)) {
classToInstances.set(className, []);
inDegree.set(className, 0);
adj.set(className, []);
}
classToInstances.get(className)!.push(plugin);
}
for (const [className, instances] of classToInstances.entries()) {
const dependencies = instances[0].dependencies || [];
for (const depClass of dependencies) {
const dependencyClassName = depClass.name;
if (inDegree.has(dependencyClassName)) {
adj.get(dependencyClassName)!.push(className);
inDegree.set(className, (inDegree.get(className) || 0) + 1);
}
}
}
const queue: string[] = [];
for (const [name, degree] of inDegree.entries()) {
if (degree === 0) queue.push(name);
}
let executedCount = 0;
while (queue.length > 0) {
const currentClassNamesToRun = [...queue];
queue.length = 0;
executedCount += currentClassNamesToRun.length;
const promises = currentClassNamesToRun.flatMap((className) =>
classToInstances.get(className)!.map((instance) => {
const hook = lifecycleHooks.get(instance)! as Function;
console.log(` -> [${lifecycle}] 开始执行: ${instance.name}`);
// @ts-expect-error
return Promise.resolve(hook(...args));
})
);
await Promise.all(promises);
for (const className of currentClassNamesToRun) {
for (const dependent of adj.get(className) || []) {
inDegree.set(dependent, inDegree.get(dependent)! - 1);
if (inDegree.get(dependent) === 0) queue.push(dependent);
}
}
}
if (executedCount < classToInstances.size) {
const circular = Array.from(inDegree.entries())
.filter(([, d]) => d > 0)
.map(([n]) => n);
throw new Error(`[${lifecycle}] 检测到循环依赖: ${circular.join(", ")}`);
}
}
}
PluginSystem 流程讲解
从使用方式可以看到,我们的插件系统依然只是对外暴露 use
和 run
这两个方法。因为我们也从这两个方法作为入口来进行讲解,关于拓扑排序的核心实现放到下面来重点讲解。
- 注册阶段 (
use
):use(plugin)
方法的职责单一,仅将插件实例存入内部的this.plugins
数组。 - 执行阶段 (
run
):run()
方法是系统的主入口,按以下顺序执行:collectHooks()
: 此私有方法是准备步骤。它遍历所有已注册的插件,调用其apply
方法。apply
方法会接收到一个register
对象,通过此对象将插件的回调函数注册到系统对应生命周期的钩子集合中(如hooks.load
,hooks.transform
)。- 执行
load
生命周期: 创建loadContext
,然后调用await this.runLifecycle('load', ...)
。 - 执行
transform
生命周期: 创建transformContext
,然后调用await this.runLifecycle('transform', ...)
。 - 执行
generate
生命周期: 创建generateContext
,然后调用await this.runLifecycle('generate', ...)
。
此流程确保了生命周期之间的串行执行,而每个生命周期内部的并发逻辑则完全委托给 runLifecycle
方法。
并发调度核心 (runLifecycle
)
runLifecycle
方法是实现并发调度的关键。它在单个生命周期内,运用拓扑排序的思想,实现了一种 分波次并发 (Wave-based Concurrency) 的执行模式。
以 transform
生命周期为例,其依赖关系图如下:
txt
[ transform 生命周期依赖图 ]
+------------------+
/ | ExtractSummary | (依赖 ImageProxy)
/ +------------------+
/
+----------------+ +--------------------+
| ImageProxy | ----> | ExtractThumbnail | (依赖 ImageProxy)
+----------------+ +--------------------+
+------------------+
| ProcessColumns | (无依赖)
+------------------+
该方法的执行逻辑如下:
- 构建依赖图: 分析当前生命周期内所有活动插件的
dependencies
属性,构建出 入度表 (inDegree
) 和 邻接表 (adj
)。 - 初始化队列: 将所有入度为 0 的插件类名放入执行队列
queue
。 - 循环执行:
- 执行:
Promise.all
并发执行当前队列中的所有插件。 - 等待:
await
等待这一波次所有插件执行完毕。 - 解锁: 遍历刚刚执行完的插件,将其邻接点(依赖者)的入度减 1。
- 入队: 如果一个插件的入度在此过程中变为 0,则将其加入队列,准备在下一波次中执行。
- 执行:
- 终止: 当队列为空时,循环终止,表示当前生命周期的所有任务已完成。
runLifecycle
模拟执行
让我们以 transform
阶段为例,详细模拟其执行过程:
-
初始状态:
inDegree
:Map { 'ImageProxy' => 0, 'ProcessColumns' => 0, 'ExtractSummary' => 1, 'ExtractThumbnail' => 1 }
queue
:[]
-
步骤 1: 初始化队列 (Wave 1)
- 遍历
inDegree
,将所有入度为 0 的插件类名推入queue
。 queue
变为['ImageProxy', 'ProcessColumns']
。
- 遍历
-
步骤 2: 执行第一波 (Wave 1)
currentClassNamesToRun
=['ImageProxy', 'ProcessColumns']
。queue
清空。Promise.all
并发执行ImageProxy
和ProcessColumns
插件。await
等待它们全部完成。
-
步骤 3: 解锁下一波
- 遍历
currentClassNamesToRun
:- 处理
ImageProxy
:其依赖者ExtractSummary
和ExtractThumbnail
的入度均减 1,变为 0。 - 将
ExtractSummary
和ExtractThumbnail
推入queue
。 - 处理
ProcessColumns
:无依赖者,无操作。
- 处理
- 此时,
queue
变为['ExtractSummary', 'ExtractThumbnail']
。
- 遍历
-
步骤 4: 执行第二波 (Wave 2)
currentClassNamesToRun
=['ExtractSummary', 'ExtractThumbnail']
。queue
清空。Promise.all
并发执行ExtractSummary
和ExtractThumbnail
插件。await
等待它们全部完成。
-
步骤 5: 解锁
ExtractSummary
和ExtractThumbnail
均无依赖者,无操作。queue
保持为空。
-
步骤 6: 结束
queue
为空,while
循环终止。transform
生命周期执行完毕。
最后
通过将 拓扑排序 算法与面向对象的插件设计相结合,我们成功地将一个串行插件系统升级为一个支持并发执行的高性能系统。该系统不仅通过类和依赖引用保证了代码的健壮性和可维护性,还通过"分波次并发"的调度策略,在保证执行顺序正确性的前提下实现了效率的最大化。
在下一篇文章中,我们将进一步探讨如何为该系统引入缓存机制,以优化那些耗时且结果稳定的操作,从而进一步提升构建性能。