新的后端系统
状态
新的后端系统已经发布,可以投入生产使用,许多插件和模块已经迁移。 我们建议所有插件和部署迁移到新系统。
后端设置示例请参见后端-下一步软件包.
概览
建立新的 Backstage 后端系统是为了帮助更简单地安装后端插件,并保持项目的 最新状态。 它还改变了基础,使插件和系统本身的演进变得更加容易,同时将中断或导致破坏性更改的情况降至最低。 您可以在《Backstage》中阅读更多有关原因的信息。原 RFC.
新系统的目标之一是减少建立Backstage后端和安装插件所需的代码。 这是一个如何在新系统中创建、添加功能和启动Backstage的示例:
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
await backend.start();
帮助实现后端设置更加精简的一个显著变化是引入了依赖注入系统,该系统与 Backstage 前端的依赖注入系统非常相似。
积木
本节将介绍构建新系统的高层次构件,这些构件都以某种方式存在于我们当前的系统中,但在新系统中都被提升为一级关注点。
后端
这就是后端实例本身,可以将其视为部署单元。 它本身没有任何功能,只是负责将各种东西连接起来。
您可以自行决定部署多少个不同的后端。 您可以将所有 功能集中在一个后端,也可以将其拆分成多个较小的部署。 这一切都取决于您对扩展和隔离单个功能的需求。
插件
插件提供实际功能,就像我们现有的系统一样。 它们完全独立运行。 如果插件之间要通信,它们必须通过线路通信。 插件之间不能通过代码直接通信。 由于这种限制,每个插件都可以被视为自己的微服务。
服务
服务提供实用程序,帮助简化插件的实施,这样每个插件就不需要从头开始实施一切。 既有许多内置服务,如用于日志记录、数据库访问和读取配置的服务,也可以导入第三方服务或创建自己的服务。
服务也是单个后端安装的定制点。 您既可以用自己的实现覆盖服务,也可以对现有服务进行较小的定制。
扩展点
许多插件都有可以扩展的方式,例如目录的实体提供者或 Scaffolder 的自定义操作。 这些扩展模式现在都被编码为扩展点(Extension Points)。
扩展点看起来有点像服务,因为你依赖它们就像依赖服务一样。 一个关键的区别是,扩展点是由插件自己注册和提供的,基于每个插件想要公开的自定义内容。
扩展点也是与插件实例本身分开输出的,而且一个插件还可以同时暴露多个不同的扩展点。 这样,随着时间的推移,单个扩展点的演进和废弃都会变得更容易,而不是处理单一的 大型 API 表面。
模块
模块使用插件扩展点为插件添加新功能。 例如,它们可以添加一个单独的目录实体提供程序或一个或多个 Scaffolder 操作。 模块基本上是插件的插件。
每个模块只能扩展一个插件,而且模块必须与该插件一起部署在同一个后端实例中。 不过,模块只能通过其注册的扩展点与其插件通信。
与插件一样,模块也可以访问服务,并依赖于自己的服务实现。 不过,它们将与自己扩展的插件共享服务,不存在特定于模块的服务实现。
创建插件
使用createBackendPlugin
所有插件都必须有一个 ID 和一个注册方法。 插件还可以接受一个选项对象,该对象可以是可选的,也可以是必选的。 选项会传递给注册方法的第二个参数,然后推断出选项类型,并将其转发给返回的插件工厂函数。
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',
// It's 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());
如果我们想让插件也接受选项,我们就会接受选项作为注册方法的第二个参数:
export 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 }));
创建模块
关于模块的一些事实
- 一个模块只能扩展一个插件,但可以与该插件注册的多个 "扩展点 "交互。
模块取决于ExtensionPoint
目标插件的库包导出的内容,例如@backstage/plugin-catalog-node
并不直接声明对插件包本身的依赖性。
下面是一个示例,说明如何创建一个模块,使用catalogProcessingExtensionPoint
:
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());
},
});
},
});
扩展点
模块对扩展点的依赖与普 通依赖一样,只需在deps
节。
定义扩展点
import { createExtensionPoint } from '@backstage/backend-plugin-api';
export interface ScaffolderActionsExtensionPoint {
addAction(action: ScaffolderAction): void;
}
export const scaffolderActionsExtensionPoint =
createExtensionPoint<ScaffolderActionsExtensionPoint>({
id: 'scaffolder.actions',
});
注册扩展点
扩展点由插件注册并由模块扩展。
后端服务
默认后端提供多个核心服务服务依赖关系使用其ServiceRef
中的deps
插件或模块的部分,然后将这些实现转发给init
方法。
服务参考
AServiceRef
是对一个接口的命名引用,随后用来解析具体的服务实现。 在概念上,这与ApiRef
服务提供了以前位于PluginEnvironment
如配置、日志和数据库。
启动时,后端将确保服务在传递给依赖于它们的插件/模块之前已被初始化。 ServiceRefs 包含一个作用域,用于确定创建服务的 serviceFactory 是否会为每个插件/模块创建一个新实例,或者是否会共享该实例。plugin
范围的服务将为每个插件/模块创建一次,而root
范围的服务将在每个后端实例中创建一次。
定义服务
import {
createServiceFactory,
coreServices,
} from '@backstage/backend-plugin-api';
import { ExampleImpl } from './ExampleImpl';
export interface ExampleApi {
doSomething(): Promise<void>;
}
export const exampleServiceRef = createServiceRef<ExampleApi>({
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 it's a root scoped service and will be created once per backend instance.
async factory({ logger, plugin }) {
// plugin is available as it's 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 });
};
},
}),
});
覆盖服务
在此示例中,我们用自定义的日志流向 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());
测试
用于测试后端插件和模块的实用程序可在@backstage/backend-test-utils
.startTestBackend
返回 HTTP,可与supertest
来测试插件。
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);
});
});
包裹结构
有关软件包架构的详细说明,请参阅Backstage架构概述该系统需要考虑的最重要的软件包是backend
,plugin-<pluginId>-backend
,plugin-<pluginId>-node
和plugin-<pluginId>-backend-module-<moduleId>
.
plugin-<pluginId>-backend
包含插件本身的实现。 *plugin-<pluginId>-node
包含扩展点和模块或其他插件可能需要的任何其他实用程序。 *plugin-<pluginId>-backend-module-<moduleId>
包含通过扩展点扩展插件的模块。 *backend
是后端本身,它将所有东西连接到你可以部署的东西上。