从零构建一个插件系统(二)串行插件系统
9 分钟阅读
书接上文,在第一篇我们讨论了一个插件系统需要包含的重点部分有哪些,下面就来实现一个基本的串行插件系统,在这里我们还是以我的 Nextjs 构建 SSG 的目标为清单。
- 拉取所有的 Issues
- 对 Issue 内容中的图片进行防盗链处理
- 自动提取文章摘要
- 提取文章的缩略图
- 合并专栏
- 实现缓存
- 输出最终的 JSON 文件
需求分析🔗
我们先从快速验证开始。一个插件系统,核心的部分为:
.use()
注册插件。.run()
启动流程。context
对象来传递数据。
基于上述内容,我们可以快速写出第一个版本的代码。
类型定义🔗
TYPESCRIPT// 插件生命周期,我们先定义三个核心阶段
export type PluginLifecycle = "load" | "transform" | "generate";
// 插件的类型,它有一个名字,和几个可选的生命周期钩子函数
export type Plugin<Context> = {
name: string;
load?: (ctx: Context) => Promise<void> | void;
transform?: (ctx: Context) => Promise<void> | void;
generate?: (ctx: Context) => Promise<void> | void;
};
// Issue 的数据结构
export type Issue = {
id: number;
content: string;
// 后续 transform 阶段会添加的字段
summary?: string;
thumbnail?: string;
};
// 全局共享的上下文,目前很简单,就是 issues 数组
export type Context = {
issues: Issue[];
};
插件系统实现🔗
TYPESCRIPTexport class PluginSystem<Ctx extends Context> {
private plugins: Plugin<Ctx>[] = [];
// 所有插件共享这一个 context 实例
private ctx: Ctx;
constructor(initialContext: Ctx) {
this.ctx = initialContext;
}
use(plugin: Plugin<Ctx>) {
this.plugins.push(plugin);
return this; // 支持链式调用
}
async run() {
// 定义生命周期的执行顺序
const lifecycles: PluginLifecycle[] = ["load", "transform", "generate"];
for (const lifecycle of lifecycles) {
console.log(`\n--- Running ${lifecycle} lifecycle ---`);
for (const plugin of this.plugins) {
// 检查插件是否实现了当前生命周期的钩子
const hook = plugin[lifecycle];
if (typeof hook === "function") {
console.log(`Executing plugin: ${plugin.name}`);
// 串行执行,等待上一个插件完成
await hook.call(null, this.ctx);
}
}
}
return this.ctx;
}
}
使用示例🔗
TYPESCRIPT// 伪造两个插件来测试
const fetchIssuesPlugin: Plugin<Context> = {
name: "fetch-issues-plugin",
async load(ctx) {
console.log(" -> Fetching issues...");
ctx.issues = [
{ id: 1, content: "第一个 issue,包含图片 https://img.com/a.png" },
{ id: 2, content: "第二个 issue,内容丰富" },
];
},
};
const outputPlugin: Plugin<Context> = {
name: "output-plugin",
generate(ctx) {
console.log(" -> Generating output...");
// 模拟输出
console.log("【Final Context】", JSON.stringify(ctx, null, 2));
},
};
// 运行系统
async function main() {
const pluginSystem = new PluginSystem({ issues: [] });
await pluginSystem.use(fetchIssuesPlugin).use(outputPlugin).run();
}
main();
通过上述代码我们实现了最基本的串行流程。但正如我们在上一篇讨论的,它有一个巨大的隐患:所有插件都能在任何阶段修改整个 context
,这可能会导致误操作,下面来进行修复。
2. 上下文划分🔗
现在,我们来修复这个问题。我们的目标是:插件在不同的生命周期,只能拿到它该拿到的 API,做它该做的事。
我们将对类型定义和 PluginSystem
的实现进行一次重构。
类型定义 (v2)🔗
TYPESCRIPTimport fs from "fs/promises";
import path from "path";
// --- 基础数据类型 ---
export type Issue = {
id: number;
content: string;
};
// 这是在 transform 阶段,所有插件共享和修改的数据载体
export type TransformContext = {
issues: Issue[];
summary?: string; // 假设这是从 issue 提取的全局摘要
thumbnail?: string; // 全局缩略图
};
// --- 受限的上下文 API 定义 ---
// `load` 钩子能拿到的 API,它只能“提交”资源
type LoadContextAPI = {
emitIssue: (issue: Issue) => void;
};
// `generate` 钩子能拿到的 API,它只能“写”文件
type GenerateContextAPI = {
writeFile: (filePath: string, content: string) => Promise<void>;
};
// --- 插件与钩子定义 ---
// 将所有钩子组合成一个类型
export type PluginHooks = {
load?: (ctx: LoadContextAPI) => Promise<void> | void;
// transform 钩子直接接收可变的数据上下文
transform?: (ctx: TransformContext) => Promise<void> | void;
// generate 钩子接收最终数据和专用的 API
generate?: (
ctx: Readonly<TransformContext>, // generate 阶段不应再修改数据,设为只读
api: GenerateContextAPI
) => Promise<void> | void;
};
// 插件的最终类型
export type Plugin = {
name: string;
} & PluginHooks;
/**
* 这是一个辅助函数,它什么也不做,但能提供完美的 TypeScript 类型推断。
* 开发者用它来包裹插件定义,就能在编写钩子时获得正确的上下文类型提示。
* @param plugin 插件对象
*/
export function definePlugin(plugin: Plugin): Plugin {
return plugin;
}
插件系统实现 (V2)🔗
TYPESCRIPTexport class PluginSystem {
// 将钩子按生命周期分类存储,这样结构更清晰
private hooks: {
load: Required<PluginHooks>["load"][];
transform: Required<PluginHooks>["transform"][];
generate: Required<PluginHooks>["generate"][];
};
constructor() {
this.hooks = {
load: [],
transform: [],
generate: [],
};
}
use(plugin: Plugin) {
// 注册时,将插件的钩子函数放入对应的数组
if (plugin.load) this.hooks.load.push(plugin.load);
if (plugin.transform) this.hooks.transform.push(plugin.transform);
if (plugin.generate) this.hooks.generate.push(plugin.generate);
return this;
}
async run() {
// --- 1. load 生命周期 ---
console.log(`\n--- Running load lifecycle ---`);
const loadedIssues: Issue[] = [];
const loadContextAPI: LoadContextAPI = {
emitIssue: (issue) => {
console.log(` [emitIssue] Loaded issue: ${issue.id}`);
loadedIssues.push(issue);
},
};
for (const hook of this.hooks.load) {
await hook(loadContextAPI);
}
// --- 2. transform 生命周期 ---
console.log(`\n--- Running transform lifecycle ---`);
// 创建一个在 transform 阶段共享的可变数据上下文
const transformContext: TransformContext = { issues: loadedIssues };
for (const hook of this.hooks.transform) {
// 钩子函数直接修改 transformContext 对象
await hook(transformContext);
}
// --- 3. generate 生命周期 ---
console.log(`\n--- Running generate lifecycle ---`);
const generateContextAPI: GenerateContextAPI = {
writeFile: async (filePath: string, content: string) => {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, "utf-8");
console.log(` [writeFile] Output generated: ${filePath}`);
},
};
for (const hook of this.hooks.generate) {
// 传入最终的数据(设为只读)和专用的 API
await hook(Object.freeze(transformContext), generateContextAPI);
}
// 返回最终处理好的数据,方便调试
return transformContext;
}
}
使用示例 (v2)🔗
因为上述对类型进行了重构,所以插件的编写基于 definePlugin
就可以提供完美的类型提示。
TYPESCRIPT// 插件定义
import { definePlugin } from "./core";
export const fetchIssuesPlugin = definePlugin({
name: "fetch-issues-plugin",
async load(ctx) {
// ctx 上只有 emitIssue 方法,无法做其他事情
ctx.emitIssue({
id: 1,
content: "第一个 issue,包含图片 https://img.com/a.png",
});
ctx.emitIssue({ id: 2, content: "第二个 issue,内容丰富" });
},
});
export const outputPlugin = definePlugin({
name: "output-plugin",
async generate(ctx, api) {
// ctx 是只读的,api 上只有 writeFile 方法
const outputData = {
issues: ctx.issues,
summary: ctx.summary,
thumbnail: ctx.thumbnail,
};
await api.writeFile(
"./dist/blog-data.json",
JSON.stringify(outputData, null, 2)
);
},
});
// 运行
const pluginSystem = new PluginSystem();
pluginSystem
.use(fetchIssuesPlugin)
.use(outputPlugin) // 之后可以加入更多的 transform 插件
.run();
最后🔗
通过这次重构,我们不仅实现了一个可用的串行插件系统,更重要的是,我们通过精细化的类型设计和受限的上下文 API,建立了一套更健壮、更安全的“游戏规则”。这为我们后续引入更复杂的并发、缓存等功能打下了坚实的基础。
下一篇会重点讨论如何升级当前的插件系统,让其支持并发构建来大大加速整个插件流程。此外如果文章有错误或者不清晰地方或者指出。