尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Backstage 新后端系统(New Backend System)实战指南:架构、插件开发与模块扩展

Backstage 新后端系统(New Backend System)实战指南:架构、插件开发与模块扩展 Backstage 新后端系统New Backend System实战指南架构、插件开发与模块扩展【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage本指南面向 Backstage 的插件作者与平台维护者系统讲解新一代后端系统的核心概念Backend、Plugin、Service、Extension Point、Module、createBackendPlugin/createBackendModule/createServiceFactory的完整用法、后端启动配置、测试方法以及仓库包结构。读完本文你将能够基于新后端系统从零创建一个可安装的插件、通过 Extension Point 扩展既有插件并掌握服务覆盖Service Override与多后端拆分部署的实战技巧。本文以 docs/plugins/new-backend-system.md 为骨架并参考了仓库内packages/backend-plugin-api、packages/backend、packages/backend-test-utils等源码实现。:::note 该文档属于旧版 plugins 文档的一部分。关于后端系统的规范文档已迁移至 docs/backend-system/index.md其中包含更详细、更新的 插件与模块构建指南、架构文档 与 核心服务文档。 :::状态与迁移建议新后端系统已经正式发布并可用于生产环境仓库中大量插件与模块已完成迁移。官方建议所有插件与部署都迁移到新系统。你可以在仓库的 packages/backend 中找到一个完整的新后端系统参考实现其入口文件 packages/backend/src/index.ts 展示了真实生产环境下如何组装数十个插件与模块。概述为什么需要新后端系统新后端系统旨在达成两个核心目标让后端插件的安装更简单传统后端需要为每个插件手写大量胶水代码创建路由、数据库、日志、认证等环境对象而新系统通过依赖注入Dependency Injection机制将这一切收敛为“注册即用”。让项目更容易保持更新新系统将可扩展性与可定制性提升为一等公民first-class concern使插件和系统本身可以在不产生破坏性变更的前提下持续演进。其中一个关键的架构变更是引入了与 Backstage 前端系统非常相似的依赖注入体系插件/模块声明它们依赖哪些服务ServiceRef与扩展点Extension Point后端在启动时负责解析并注入实现。下面是一段“创建后端 → 安装特性 → 启动”的最小完整示例也是新系统最直观的体现import { createBackend } from backstage/backend-defaults; // Create your backend instance const backend createBackend(); // Install all desired features backend.add(import(backstage/plugin-catalog-backend)); // Start up the backend backend.start();对比旧后端动辄上百行的createPlugin样板代码新系统把整个后端搭建压缩到了三步。在真实仓库中packages/backend/src/index.ts 正是以同样的方式依次backend.add(...)安装 auth、app、catalog、scaffolder、techdocs、signals、notifications 等插件及其模块最后调用backend.start()启动。构建模块Building Blocks新系统由五个一等公民概念构成。无论你是搭建自己的 Backstage 实例、开发插件还是通过模块扩展插件功能理解这些概念都是必要的。Backend后端实例后端实例本身是“部署单元”unit of deployment。它自身不包含任何业务功能只负责把各个部件“接线”到一起。你可以根据扩展与隔离的需要决定部署多少个后端把所有特性放在一个单体后端中或者拆分成多个更小的部署。Plugins插件插件提供实际功能与既有系统中的插件一致。插件之间完全独立运行如果插件之间需要通信必须通过“线上”over the wire即 HTTP 请求进行禁止插件之间通过代码直接通信。因此每个插件在架构上都可以被视作一个微服务。Services服务服务为插件提供公共工具避免每个插件从零实现一切。既有大量内置服务如日志、数据库访问、配置读取也可以导入第三方服务或自行创建。服务同时也是单个后端安装的定制点既可以整体覆盖override服务为自己的实现也可以对现有服务做局部定制。Extension Points扩展点许多插件存在扩展机制例如 Catalog 的 Entity Provider、Scaffolder 的自定义 Action。这些扩展模式在新系统中被编码为 Extension Point。扩展点在用法上看起来与服务类似同样在deps中声明依赖关键区别在于扩展点由插件自身注册并提供用于暴露该插件希望开放的自定义能力。扩展点从插件实例中单独导出一个插件可以同时暴露多个扩展点这使单个扩展点可以独立演进与弃用deprecate而不必处理一个庞大的 API 表面。Modules模块模块利用插件的 Extension Point 为插件添加新功能例如添加一个 Catalog Entity Provider或一个/多个 Scaffolder Action。可以通俗地理解模块是“插件的插件”。模块有以下约束每个模块只能扩展一个插件但可以与该插件注册的多个 Extension Point 交互模块必须与它扩展的插件部署在同一个后端实例中模块只能通过其插件注册的扩展点与插件通信模块与插件一样可以访问服务但会共享所扩展插件的服务实例不存在模块专属的服务实现。另外需要特别说明的是模块总是在它所扩展的插件之前完成初始化这保证了插件在启动时扩展点已经就绪。创建插件Creating Plugins插件使用createBackendPlugin函数创建。所有插件必须包含pluginId与register方法插件还可以接受一个可选或必填的 options 对象该对象会作为register方法的第二个参数传入其类型会被自动推断并转发给返回的插件工厂函数。最小插件示例import { configServiceRef, coreServices, createBackendPlugin, } from backstage/backend-plugin-api; // export type ExamplePluginOptions { exampleOption: boolean }; export const examplePlugin createBackendPlugin({ // unique id for the plugin pluginId: example, // Its possible to provide options to the plugin // register(env, options: ExamplePluginOptions) { register(env) { env.registerInit({ deps: { logger: coreServices.logger, }, // logger is provided by the backend based on the dependency on loggerServiceRef above. async init({ logger }) { logger.info(Hello from example plugin); }, }); }, });然后在后端安装该插件注意安装时直接传入插件工厂函数本身无需调用backend.add(examplePlugin);注意示例中的configServiceRef为旧版名称在 packages/backend-plugin-api/src/services/definitions/coreServices.ts 中配置服务的正式引用名为coreServices.rootConfigroot 作用域本文后续示例均使用coreServices命名空间下的正式名称。支持 options 的插件如果希望插件接受配置项只需在register方法的第二个参数接收 optionsexport const examplePlugin createBackendPlugin({ pluginId: example, register(env, options?: { silent?: boolean }) { env.registerInit({ deps: { logger: coreServices.logger }, async init({ logger }) { if (!options?.silent) { logger.info(Hello from example plugin); } }, }); }, });安装时传入选项backend.add(examplePlugin({ silent: true }));插件注册机制的源码视角从 packages/backend-plugin-api/src/wiring/createBackendPlugin.ts 的实现可以看到createBackendPlugin会校验pluginId必须匹配ID_PATTERN仅允许字母、数字与连字符且必须以字母开头不合规时打印警告或直接抛出异常构造一个register环境对象reg提供registerExtensionPoint、registerConnection、registerInit三个注册入口要求register内必须调用一次registerInit否则抛错registerInit was not called by register in pluginId最终返回一个$$type: backstage/BackendFeature、featureType: registrations的注册描述对象。仓库中一个真实的插件例子是 plugins/example-todo-list-backend/src/plugin.ts它在deps中声明httpAuth、logger、httpRouter三个核心服务在init中通过httpRouter.use(...)挂载 Express 路由并用httpRouter.addAuthPolicy({ path: /health, allow: unauthenticated })放行健康检查端点——这展示了“声明依赖 → 后端注入 → 使用服务”的完整闭环。创建模块Creating Modules创建模块使用createBackendModule函数核心约束与上文“Modules”一节一致模块通过插件注册的 Extension Point 为插件添加功能一个模块只能扩展一个插件但可以与该插件的多个 Extension Point 交互模块总是在它扩展的插件之前完成初始化。模块依赖目标插件的library 包node 包导出的 Extension Point例如backstage/plugin-catalog-node不直接声明对插件包本身如backstage/plugin-catalog-backend的依赖。下面是一个使用catalogProcessingExtensionPoint添加自定义 Catalog Processor 的模块示例import { createBackendModule } from backstage/backend-plugin-api; import { catalogProcessingExtensionPoint } from backstage/plugin-catalog-node; import { MyCustomProcessor } from ./processor; export const exampleCustomProcessorCatalogModule createBackendModule({ pluginId: catalog, moduleId: example-custom-processor, register(env) { env.registerInit({ deps: { catalog: catalogProcessingExtensionPoint, }, async init({ catalog }) { catalog.addProcessor(new MyCustomProcessor()); }, }); }, });createBackendModule要求同时提供pluginId必须与所扩展插件的 id 精确匹配与moduleId用于标识模块并防止重复安装二者的 ID 校验规则与插件一致详见 packages/backend-plugin-api/src/wiring/createBackendModule.ts。仓库中一个真实的模块例子是 packages/backend/src/authModuleGithubProvider.ts它以pluginId: auth声明扩展 auth 插件通过authProvidersExtensionPoint注册一个 GitHub OAuth Provider并自定义signInResolver将 GitHub 用户名映射为 BackstageUser实体引用。这个例子同时印证了“模块依赖-node包而非插件包”的规则它从backstage/plugin-auth-node导入扩展点。定义与注册 Extension Point模块在deps中把 Extension Point 当作普通依赖来声明。定义 Extension Point通常放在-node包中import { createExtensionPoint } from backstage/backend-plugin-api; export interface ScaffolderActionsExtensionPoint { addAction(action: ScaffolderAction): void; } export const scaffolderActionsExtensionPoint createExtensionPointScaffolderActionsExtensionPoint({ id: scaffolder.actions, });注册 Extension Point扩展点由插件注册、由模块扩展。插件在register中通过reg.registerExtensionPoint(extPoint, impl)注册实现模块在deps中声明该扩展点并在init中调用其方法。从源码看createExtensionPoint生成的是一个可独立导出的引用对象注册时无论是插件还是模块都会把扩展点与实现打包进getRegistrations()返回的注册描述中见 packages/backend-plugin-api/src/wiring/createBackendPlugin.ts。仓库中catalogProcessingExtensionPoint的真实定义位于 plugins/catalog-node/src/extensions.tsid 为catalog.processing其接口CatalogProcessingExtensionPoint提供了addProcessor、setFieldValidators、setEntityDataParser、addModelSource等扩展方法plugins/catalog-node/src/extensions.ts。后端服务Backend Services默认后端开箱即用地提供多个核心服务覆盖配置读取、日志、数据库访问等能力。服务的完整清单与定义见 packages/backend-plugin-api/src/services/definitions/coreServices.ts主要包括ServiceRef作用域职责coreServices.rootConfigroot访问静态配置coreServices.loggerplugin插件级日志带插件标签coreServices.rootLoggerroot根日志实现coreServices.databaseplugin基于 knex 的数据库访问与管理coreServices.cacheplugin键值缓存coreServices.httpRouterplugin插件 HTTP 路由注册coreServices.rootHttpRouterroot根级 HTTP 路由注册coreServices.httpAuthpluginHTTP 请求认证coreServices.authplugin令牌认证与凭据管理coreServices.userInfoplugin已认证用户信息获取coreServices.discoveryplugin插件间通信的服务发现coreServices.schedulerplugin分布式后台任务调度coreServices.urlReaderplugin从外部系统读取内容coreServices.lifecycle/rootLifecycleplugin / root启动与关闭生命周期钩子coreServices.permissionsplugin权限系统集成coreServices.pluginMetadataplugin当前插件元数据coreServices.rootHealth/rootInstanceMetadata/rootSystemMetadataroot健康检查与实例/系统元数据coreServices.auditorplugin审计服务依赖通过ServiceRef在插件/模块的deps中声明实现随后被注入到init方法中。Service References服务引用ServiceRef是指向某个接口的具名引用后端据此解析具体的服务实现——概念上与前端的ApiRef非常相似。服务提供了原先位于PluginEnvironment中的公共工具Config、Logging、Database 等。后端在启动时会确保服务先于依赖它们的插件/模块完成初始化。ServiceRef包含一个scope作用域用于决定服务工厂创建实例的方式plugin作用域每个插件/模块创建一次独立实例root作用域整个后端实例只创建一个共享实例。定义一个服务import { createServiceFactory, coreServices, } from backstage/backend-plugin-api; import { ExampleImpl } from ./ExampleImpl; export interface ExampleApi { doSomething(): Promisevoid; } export const exampleServiceRef createServiceRefExampleApi({ id: example, scope: plugin, // can be root or plugin // The defaultFactory is optional to implement but it will be used if no other factory is provided to the backend. // This is allows for the backend to provide a default implementation of the service without having to wire it beforehand. defaultFactory: async service createServiceFactory({ service, deps: { logger: coreServices.logger, plugin: coreServices.pluginMetadata, }, // Logger is available directly in the factory as its a root scoped service and will be created once per backend instance. async factory({ logger, plugin }) { // plugin is available as its a plugin scoped service and will be created once per plugin. return async ({ plugin }) { // This block will be executed once for every plugin that depends on this service logger.info(Initializing example service plugin instance); return new ExampleImpl({ logger, plugin }); }; }, }), });要点说明defaultFactory是可选的当后端没有为服务提供其他工厂时会使用默认工厂自动提供实现无需预先接线在示例中外层factory拿到 root 作用域的logger与 plugin 作用域的pluginMetadata整个后端各创建一次内层返回的函数则会在每个依赖该服务的插件初始化时执行一次据此创建该插件专属的ExampleImpl实例——这正是plugin作用域服务的典型写法。覆盖服务Overriding Services下面的例子用自定义实现替换默认的 root logger 服务把日志流式传输到 GCP。rootLoggerServiceRef是root作用域因此不存在插件专属实例import { createServiceFactory, rootLoggerServiceRef, LoggerService, } from backstage/backend-plugin-api; // This custom implementation would typically live separately from // the backend setup code, either nearby such as in // packages/backend/src/services/logger/GoogleCloudLogger.ts // Or you can let it live in its own library package. class GoogleCloudLogger implements LoggerService { static factory createServiceFactory({ service: rootLoggerServiceRef, deps: {}, async factory() { return new GoogleCloudLogger(); }, }); // custom implementation here ... } // packages/backend/src/index.ts const backend createBackend(); // supplies additional or replacement services to the backend backend.add(GoogleCloudLogger.factory);覆盖服务是后端定制的最深层次手段。例如在 docs/backend-system/building-backends/01-index.md 中还展示了更精细的用法覆盖coreServices.logger时可以依赖coreServices.rootLogger、coreServices.pluginMetadata、coreServices.rootConfig在默认的“插件标签”之外从配置中读取并附加自定义标签const backend createBackend(); backend.add( createServiceFactory({ service: coreServices.logger, deps: { rootLogger: coreServices.rootLogger, plugin: coreServices.pluginMetadata, config: coreServices.rootConfig, }, factory({ rootLogger, plugin, config }) { const labels readCustomLogLabelsForPlugin(config, plugin); // custom logic return rootLogger.child(labels); }, }), );需要注意框架内置的PluginMetadataService由框架提供且不可覆盖这是唯一例外。组装与启动后端最小后端一个最小的后端就是一个含package.json与src/index.ts的包通常放在 monorepo 的packages/backend目录由backstage/create-app创建。其入口结构为import { createBackend } from backstage/backend-defaults; // Omitted in the examples below const backend createBackend(); backend.add(import(backstage/plugin-app-backend)); backend.add(import(backstage/plugin-catalog-backend)); backend.add(import(backstage/plugin-scaffolder-backend)); backend.add( import(backstage/plugin-catalog-backend-module-scaffolder-entity-model), ); backend.start();createBackend来自backstage/backend-defaults它会默认安装全部必需的核心服务日志、数据库、HTTP 等这也是标准安装无需手动注册服务的原因。特性加载器Feature Loader当特性数量较多、需要按条件分组加载时可以使用createBackendFeatureLoader。仓库的真实入口 packages/backend/src/index.ts 提供了一个典型示例将所有搜索相关特性聚合到一个 loader 中并依赖 root 作用域的coreServices.rootConfig仅当配置存在search.elasticsearch时才加载 Elasticsearch 模块const searchLoader createBackendFeatureLoader({ deps: { config: coreServices.rootConfig, }, *loader({ config }) { yield import(backstage/plugin-search-backend); yield import(backstage/plugin-search-backend-module-catalog); yield import(backstage/plugin-search-backend-module-explore); yield import(backstage/plugin-search-backend-module-techdocs); if (config.has(search.elasticsearch)) { yield import(backstage/plugin-search-backend-module-elasticsearch); } }, }); backend.add(searchLoader);拆分多个后端部署更高级的部署方式是把后端按插件拆分到多个部署单元。做法是复制一份后端包目前yarn new尚未提供后端模板按需裁剪src/index.ts并同步清理package.json中的依赖。例如packages/ backend-a/ src/ index.ts package.json - name: backend-a backend-b/ src/ index.ts package.json - name: backend-bbackend-a只保留基础插件const backend createBackend(); backend.add(import(backstage/plugin-app-backend)); backend.add(import(backstage/plugin-catalog-backend)); backend.add( import(backstage/plugin-catalog-backend-module-scaffolder-entity-model), ); backend.start();backend-b只运行 Scaffolderconst backend createBackend(); backend.add(import(backstage/plugin-scaffolder-backend)); backend.start();拆分后两个后端需要互相通信由于 Backstage 目前没有开箱即用的跨后端解决方案你需要为两个后端分别提供自定义的DiscoveryService实现返回对方正确的 URL并在前端提供对应的DiscoveryApi实现或将两个后端通过反向代理统一路由。拆分部署的完整架构说明可参考 docs/backend-system/building-backends/01-index.md 与 docs/deployment/scaling.md。启动失败行为配置Startup Configurationbackend.startup配置块允许控制插件或模块启动失败时的行为。默认情况下任何插件/模块启动失败都会导致整个后端中止启动该配置可让特定插件/模块变为可选或翻转全局默认值。单插件失败继续启动backend: startup: plugins: catalog: onPluginBootFailure: continue单模块失败继续启动modules下的键必须是createBackendModule({ moduleId: ... })声明的moduleId而非插件名或 Entity Provider 名backend: startup: plugins: catalog: modules: github: # moduleId as declared in createBackendModule({ moduleId: ... }) onPluginModuleBootFailure: continue翻转全局默认值所有插件默认失败继续仅auth必须成功backend: startup: default: onPluginBootFailure: continue plugins: auth: onPluginBootFailure: abort完整配置参考backend: startup: # Global defaults applied when not specified per-plugin or per-module default: # Defaults to abort. Set to continue to make all plugins optional by default. onPluginBootFailure: abort # or continue # Defaults to abort. Set to continue to make all plugin modules optional by default. onPluginModuleBootFailure: abort # or continue # Per-plugin and per-module overrides plugins: pluginId: # Override the default boot failure behavior for this specific plugin. onPluginBootFailure: abort # or continue modules: moduleId: # Override the default boot failure behavior for this specific plugin module. onPluginModuleBootFailure: abort # or continue测试插件与模块Testing测试工具位于backstage/backend-test-utils。startTestBackend返回一个真实启动的 HTTP serverTestBackend见 packages/backend-test-utils/src/wiring/TestBackend.ts可以配合supertest对插件发起真实的 HTTP 请求进行端到端测试import { startTestBackend } from backstage/backend-test-utils; import request from supertest; describe(My plugin tests, () { it(should return 200, async () { const { server } await startTestBackend({ features: [myPlugin()], }); const response await request(server).get(/api/example/hello); expect(response.status).toBe(200); }); });从源码看startTestBackend会将传入的features支持 Promise因此可以直接传import(...)动态导入统一解包为BackendFeature通过一个自定义的rootHttpRouter服务工厂以随机端口port: 0创建真实 HTTP server并注册到测试后端的生命周期中支持传入额外的extensionPoints通过内部生成的“测试扩展点注册模块”把测试用扩展点注入后端。包结构Package Structure完整的包架构说明见 docs/overview/architecture-overview.md。对于新后端系统最重要的四类包是包职责plugin-pluginId-backend插件本身的实现。仓库示例plugins/example-todo-list-backendplugin-pluginId-node插件的扩展点以及模块/其他插件所需的公共工具。仓库示例plugins/catalog-node其中 extensions.ts 定义了catalogProcessingExtensionPoint等扩展点plugin-pluginId-backend-module-moduleId通过扩展点扩展插件的模块。仓库示例plugins/catalog-backend-module-logsbackend后端本身把一切接线为可部署产物。仓库示例packages/backend这种分包约定保证了“实现”与“扩展 API”的隔离-backend包承载实现-node包承载扩展点与共享工具模块只依赖-node包即可完成扩展而不必引入整个插件实现。总结新后端系统通过依赖注入将插件的安装、扩展与定制统一为“注册”模型显著降低了后端搭建与插件集成的复杂度。本文覆盖了从createBackend启动后端、createBackendPlugin创建插件、createBackendModule编写模块、createExtensionPoint定义扩展点、createServiceFactory定义与覆盖服务到startTestBackend编写测试、以及按需拆分多后端部署的完整链路。若需继续深入建议依次阅读 docs/backend-system/architecture/01-index.md、docs/backend-system/building-plugins-and-modules/01-index.md 与 docs/backend-system/core-services/01-index.md并结合 packages/backend/src/index.ts 对照学习真实项目中的组装方式。【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表