ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Backstage 后端插件扩展点(Extension Points)完整指南:从定义到设计实践

Backstage 后端插件扩展点(Extension Points)完整指南:从定义到设计实践 Backstage 后端插件扩展点Extension Points完整指南从定义到设计实践【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage扩展点是 Backstage 新后端系统New Backend System中插件向外部暴露深度定制能力的一等公民机制插件通过registerExtensionPoint注册实现模块Module通过依赖注入方式消费这些扩展点来扩展插件行为。本文以 Backstage 仓库中 05-extension-points.md 架构文档为主线结合backstage/backend-plugin-api与backstage/plugin-scaffolder-node的真实源码实现系统讲解扩展点的定义、注册、工厂变体、模块扩展点以及接口设计原则帮助你掌握在 Backstage 中构建可扩展插件的完整方法论。为什么需要扩展点静态配置的边界Backstage 插件可以使用静态配置static configuration实现轻量级定制例如通过配置文件为插件设置消息或开关选项。但这种方式存在天然上限当用户需要注入自定义处理器、注册新的 action、替换某个执行逻辑时静态配置根本无法表达这种代码级的扩展能力。扩展点正是为突破这一限制而设计的机制它与模块Module相辅相成插件注册扩展点模块安装到后端并消费扩展点。模块的详细行为在 06-modules.md 中有完整介绍简单来说模块总是与它扩展的插件安装在同一后端实例中且每个模块只能扩展一个插件。从概念上看扩展点与服务Service非常相似——它们都通过引用对象reference object封装一个接口。但两者有本质区别维度服务Service扩展点Extension Point注册方后端系统 / 服务工厂插件自身工厂关联有 service factory 创建实例没有任何工厂访问范围所有插件/模块可见仅扩展同一插件的模块可访问用途提供共享功能暴露插件的定制入口服务的详细设计参见 03-services.md其中服务通过createServiceRef创建引用并由createServiceFactory提供实现而扩展点则由插件在register回调中直接给出实现。扩展点必须从 node 库包导出插件扩展点始终应从插件的 node 库包node library package导出例如backstage/plugin-catalog-node、backstage/plugin-scaffolder-node。这样做有两个关键理由避免模块与插件产生直接依赖模块只需依赖轻量的 node 库包而无需直接依赖插件包从而避免因版本不匹配导致插件包被重复安装便于扩展点长期演进node 库包是 API 的稳定承载面可以在不影响插件内部实现的前提下独立演化。需要导出的扩展点数量没有硬性限制但应警惕 API 表面API surface的复杂度膨胀。实践上导出多个方法少的扩展点优于导出少数方法多的扩展点——前者更易于维护和单独演进。定义扩展点createExtensionPoint扩展点通过backstage/backend-plugin-api中的createExtensionPoint方法创建需要提供接口类型作为泛型和唯一 ID。以下是在plugins/scaffolder-node/src/extensions.ts中真实存在的scaffolderActionsExtensionPoint定义原文档使用addAction单数方法当前仓库源码已演进为addActions可变参数形式import { createExtensionPoint } from backstage/backend-plugin-api; import { TemplateAction } from ./actions; export interface ScaffolderActionsExtensionPoint { addActions(...actions: TemplateActionany, any, any[]): void; } export const scaffolderActionsExtensionPoint createExtensionPointScaffolderActionsExtensionPoint({ id: scaffolder.actions, });查看 createExtensionPoint 源码 可以看到它返回的是一个带有标记的对象export function createExtensionPointT( options: CreateExtensionPointOptions, ): ExtensionPointT { return { id: options.id, T: null as T, toString() { return extensionPoint{${options.id}}; }, $$type: backstage/ExtensionPoint, }; }ExtensionPointT类型的完整定义位于 types.ts它包含id字符串、用于typeof extensionPoint.T类型提取的占位字段T运行时恒为null仅作类型用途、便于调试的toString()以及$$type: backstage/ExtensionPoint品牌标记。ID 约定与命名模式细节可参见 08-naming-patterns.md。注册扩展点registerExtensionPoint要让模块能够使用扩展点插件必须先在register回调中通过env.registerExtensionPoint注册实现。这是插件与模块之间共享可变状态 启动时序保证的核心模式export const scaffolderPlugin createBackendPlugin( { pluginId: scaffolder, register(env) { const actions new Mapstring, TemplateActionany(); env.registerExtensionPoint( scaffolderActionsExtensionPoint, { addAction(action) { if (actions.has(action.id)) { throw new Error(Scaffolder actions with ID ${action.id} has already been installed); } actions.set(action.id, action); }, }, ); env.registerInit({ deps: { ... }, async init({ ... }) { // Use the registered actions when setting up the scaffolder ... const installedActions Array.from(actions.values()); }, }); }, }, );这段代码展示了扩展点机制最核心的时序保证注册扩展点时创建一个闭包closure当模块调用addAction时action 被写入共享的actionsMap插件随后可以在自己的init方法中安全地读取actions——因为所有扩展该插件的模块都会在插件初始化之前被完全初始化模块与插件的初始化顺序详见 06-modules.md也就是说当插件的init被调用时所有 action 都已添加完毕、可被访问反过来一旦插件的init方法 resolved就不能再与扩展点交互了。registerExtensionPoint的类型签名见 types.ts插件BackendPluginRegistrationPoints与模块BackendModuleRegistrationPoints的注册点接口都提供两种重载直接传入(ref, impl)实现或传入包含extensionPoint与factory的 options 对象。工厂式扩展点将启动失败归因于模块在有些场景下你希望把启动失败归因到提供扩展的模块而不是让整个插件启动直接失败。为此可以使用registerExtensionPoint的变体不直接提供实现而是注册一个工厂函数来按需产生实现。该工厂接收一个ExtensionPointFactoryContext其中带有reportModuleStartupFailure方法用于上报启动失败并将其归因给具体模块。import { createBackendPlugin, ExtensionPointFactoryContext, } from backstage/backend-plugin-api; import { assertError, ForwardedError } from backstage/errors; import { createProviderConnection, Provider } from ./internal; type ProviderEntry { provider: Provider; context: ExtensionPointFactoryContext; }; export const examplePlugin createBackendPlugin({ pluginId: example, register(env) { const providers: ProviderEntry[] []; // Using the variant of registerExtensionPoint that takes an options object. env.registerExtensionPoint({ extensionPoint: exampleProvidersExtensionPoint, // The factory function produces a separate instance for each module. factory: context ({ addProvider(provider) { // Store the context together with the provider so we can report failures later providers.push({ provider, context }); }, }), }); env.registerInit({ deps: { database: coreServices.database }, async init({ database }) { for (const { provider, context } of providers) { const connection await createProviderConnection(provider, database); try { // This connects each provider that was installed by a module await provider.connect(connection); } catch (error: unknown) { // If the connection fails, we can report this as a failure of the module rather than the plugin assertError(error); context.reportModuleStartupFailure({ error: new ForwardedError(Failed to connect provider, error), }); } } }, }); }, });这个模式的关键点每个模块获得独立的实现实例factory函数会针对每个模块被调用产出一份独立的扩展点实现失败归因当模块提供的 provider 在插件init阶段连接失败时通过保存在闭包中的context.reportModuleStartupFailure({ error })把错误标记为该模块的启动失败而非整个插件启动失败调用时机约束从 ExtensionPointFactoryContext 定义 的注释可知reportModuleStartupFailure必须在插件的init函数返回之前调用。模块如何使用扩展点模块是扩展点的真正消费者。它通过createBackendModule创建在registerInit的deps中同时声明扩展点依赖和服务依赖。以下来自 06-modules.md 的示例展示了如何通过catalogProcessingExtensionPoint为 catalog 插件添加自定义处理器// plugins/catalog-backend-module-example-processor/src/module.ts import { createBackendModule } from backstage/backend-plugin-api; import { catalogProcessingExtensionPoint } from backstage/plugin-catalog-node; import { MyCustomProcessor } from ./MyCustomProcessor; export const catalogModuleExampleCustomProcessor createBackendModule({ pluginId: catalog, moduleId: example-custom-processor, register(env) { env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, logger: coreServices.logger, }, async init({ catalog }) { catalog.addProcessor(new MyCustomProcessor(logger)); }, }); }, });注意两个细节扩展点与服务可互换声明deps中可以同时出现扩展点引用如catalogProcessingExtensionPoint与服务引用如coreServices.logger初始化时它们都会被解析注入一个模块还可以同时依赖多个扩展点模块包默认导出约定与插件类似每个模块包应将模块实例作为包的默认导出例如export { catalogModuleExampleCustomProcessor as default } from ./module.ts;这样在后端实例中只需引用包名即可安装backend.add( import(internal/backstage-plugin-catalog-backend-module-example-processor), );每个模块包通常只包含一个模块但该模块可以扩展多个扩展点也可以利用配置按条件启用/禁用某些扩展——该模式只适用于彼此相关的扩展否则应拆分为独立的模块包。模块扩展点模块自身的扩展面与插件一样模块也可以注册自己的扩展点。其 API 与插件的注册和使用方式完全相同。差异在于定位模块扩展点主要用于允许插件的使用者对模块进行复杂的内部定制因此优先直接从模块包导出扩展点而不是为模块单独创建 node 库包使用方式与插件扩展点一致——创建一个独立的模块并在deps中声明对目标扩展点的依赖。例如backstage/plugin-notifications-backend-module-slack与backstage/plugin-notifications-backend-module-email都从模块包自身导出了各自的扩展点见 plugins/notifications-backend-module-slack/src/extensions.ts 与 plugins/notifications-backend-module-email/src/extensions.ts这正是模块扩展点从模块包直接导出这一约定的落地实例。扩展点接口设计原则扩展点的接口是插件需要长期维护的公共 API 表面设计时必须深思熟虑。架构文档给出了三条核心原则1. 只做加法additions only安装模块是用户的有意识动作——既然用户可以自行卸载添加该行为的模块扩展点接口就不需要支持移除操作。例如scaffolderActionsExtensionPoint无需提供删除 action 的方法因为用户只需卸载添加该 action 的模块即可达到移除效果。2. 单例模式singleton pattern当扩展点用于添加或覆盖某种默认行为时往往不适合允许多个模块同时安装。例如 scaffolder 想暴露自定义任务执行器的能力时让多个模块各自添加任务执行器是不合理的此时应使用 setter 语义保证全局只有一个实现其余模块尝试安装时直接抛错interface ScaffolderTaskRunnerExtensionPoint { setTaskRunner(taskRunner: SchedulerServiceTaskRunner): void; }对比addAction与setTaskRunner可以发现前者是多对多的集合语义允许多个模块各添加 action后者是一对一的覆盖语义只允许一个模块设置 task runner。设计时应根据扩展本质选择合适语义。3. 破坏性变更用弃旧建新如果某个扩展点已有使用方需要做破坏性变更时不要修改现有扩展点而是弃用deprecate旧扩展点并创建一个不同名称的新扩展点。新名称可以是全新的也可以在旧名称后追加版本号例如scaffolderActionsV2ExtensionPoint。这样既能让旧使用者平稳迁移又能保持新扩展点的演进空间。从源码看扩展点的完整生态在当前仓库中扩展点机制已被广泛使用这为理解本文内容提供了大量真实样本catalog 插件plugins/catalog-node/src/extensions.ts 定义catalogProcessingExtensionPoint、catalogLocationsExtensionPoint等扩展点供 catalog 的各 provider 模块消费scaffolder 插件plugins/scaffolder-node/src/extensions.ts 定义scaffolderActionsExtensionPoint由 plugins/scaffolder-backend-module-github 等模块安装各自 actionauth 插件plugins/auth-node/src/extensions/AuthProvidersExtensionPoint.ts 与AuthOwnershipResolutionExtensionPoint允许各认证 provider 模块注册search 插件plugins/search-backend-module-catalog/src/module.ts、plugins/search-backend-module-techdocs/src/module.ts 通过扩展点挂载索引器collator与装饰器decoratorkubernetes 插件plugins/kubernetes-node/src/extensions.ts 暴露集群供应商等扩展面permission 插件plugins/permission-node/src/plugin.ts 通过扩展点注册策略与条件规则techdocs 插件plugins/techdocs-node/src/extensions.ts 支持扩展构建器等。这些真实扩展点都遵循同一套模式node 库包导出引用 → 插件register回调注册实现 → 模块deps声明依赖 → 插件init时统一消费。结合单元测试如 createExtensionPoint.test.ts 与 createBackendModule.test.ts可以看到运行时对扩展点注册、时序与重复安装的校验逻辑。总结扩展点是 Backstage 新后端系统实现插件可扩展性的基石其设计可以提炼为四个要点定义用createExtensionPointT({ id })在 node 库包中声明接口引用ID 全局唯一注册插件在register回调中通过registerExtensionPoint提供实现用闭包累积模块注入的扩展并在init时消费——模块先于插件完成初始化保证了数据完整性工厂变体需要把启动失败归因给具体模块时改用factory选项注册配合ExtensionPointFactoryContext.reportModuleStartupFailure精确上报设计坚持只加不减、必要时用单例语义、破坏性变更走弃旧建新路线让扩展点成为可长期演进的公共 API。掌握这套机制后你既能以模块方式为现有插件catalog、scaffolder、auth、search、kubernetes 等贡献扩展能力也能为自己的插件设计出优雅、可持续维护的扩展面与 Backstage 庞大的插件生态无缝衔接。【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表