Backstage 插件开发指南:从创建、组合到与软件目录集成
2026/9/12 5:10:08 网站建设 项目流程

Backstage 插件开发指南:从创建、组合到与软件目录集成

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

Backstage 的核心设计理念之一,是通过插件把各类基础设施与软件研发工具无缝集成到一个统一的开发者门户中。本文以仓库 docs/plugins/index.md 为主线,系统讲解旧前端系统(Legacy Frontend System)下插件的创建流程、目录结构、组合系统、路由体系、与软件目录的集成方式以及对外部 API 的通信策略,并给出对应的源码与配置佐证,帮助你在当前仓库 README.md 所描述的项目中快速上手插件开发。阅读完本文,你将掌握从零创建插件、按命名规范导出扩展、将插件嵌入实体页面、以及通过代理或后端插件安全访问外部服务的完整实战能力。

插件生态与设计理念

Backstage 通过将各个插件无缝集成,编排出一个凝聚的单页应用(SPA)。插件生态的核心愿景是"灵活",让你可以把范围广泛的基础设施与软件开发工具以插件的形式纳入 Backstage。为了在所有插件之间保证一致、直观的用户体验,插件开发必须遵循严格的设计规范,详见 docs/dls/design.md。

从架构上看,每个插件都被视为一个自包含的 Web 应用,几乎可以承载任何类型的内容。插件共享同一套平台 API 和可复用的 UI 组件,既可以用浏览器原生 API 从外部拉取数据,也可以依赖外部模块完成工作。在开发规范上,官方建议:

  • 优先使用 TypeScript 编写插件;
  • 提前规划插件目录结构,便于后续维护;
  • 优先使用 Backstage 自带组件,其次才考虑 Material UI;
  • 在新建 API 之前,先查阅已有的共享 Backstage API,避免重复造轮子。

注意:本仓库中docs/plugins/目录下的文档均标记为 Legacy(旧前端系统)文档。对于新开发,应参考 docs/frontend-system/index.md(新前端系统)与 docs/backend-system/index.md(新后端系统)的文档。本文内容用于指导存量插件维护以及尚未迁移的插件开发。

创建插件

使用脚手架生成插件

创建前端插件的前提是已经执行过yarn install安装依赖,然后在项目根目录运行:

yarn new

这是调用backstage-cli new --select plugin的快捷方式。在交互式提示中选择frontend-plugin类型:

随后 CLI 会根据你提供的插件 ID 生成一个新的 Backstage 插件,并自动完成构建与注册:

  • 将插件作为依赖加入packages/app/package.json
  • packages/app/src/App.tsx中导入并使用插件扩展。

如果 Backstage App 已经通过yarn start运行,你可以直接访问http://localhost:3000/my-plugin看到新插件的默认页面:

在隔离环境中开发插件

除了在完整 App 中查看插件,你还可以在插件目录内独立运行yarn start来单独服务该插件,或用 yarn workspace 命令:

yarn workspace @backstage/plugin-my-plugin start # 也支持 --check

这种隔离开发方式启动更快、热更新更迅速,适合本地高频迭代,其配套设置位于插件的dev/目录中。除了frontend-pluginyarn new还提供其他插件库包类型(如 backend-plugin、backend-module 等)可供选择。

脚手架的产物:插件目录结构

生成的新插件是一个"迷你项目",包含独立的package.jsonsrc目录:

new-plugin/ dev/ index.ts node_modules/ src/ components/ ExampleComponent/ ExampleComponent.test.tsx ExampleComponent.tsx index.ts ExampleFetchComponent/ ExampleFetchComponent.test.tsx ExampleFetchComponent.tsx index.ts index.ts plugin.test.ts plugin.ts routes.ts setupTests.ts .eslintrc.js package.json README.md

这种设计让插件可以作为独立包发布到 npm,也允许你在不加载整个大型 Backstage App 的情况下单独开发。每个目录下的index.ts用于从文件夹路径导入而非具体文件,从而在单一文件中统一控制导出内容。package.json声明插件依赖、元数据与脚本,README.md用于记录插件信息。

插件核心:plugin.ts 与扩展

src/plugin.ts是插件最核心的文件,它创建插件实例,并通过plugin.provide()导出扩展:

import { createPlugin, createRoutableExtension, } from '@backstage/core-plugin-api'; import { rootRouteRef } from './routes'; export const examplePlugin = createPlugin({ id: 'example', routes: { root: rootRouteRef, }, }); export const ExamplePage = examplePlugin.provide( createRoutableExtension({ name: 'ExamplePage', component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), mountPoint: rootRouteRef, }), );

这里演示了两种核心原语:

  • createPlugin:创建插件实例,routes字段将RouteRef暴露给 App 使用;
  • createRoutableExtension:创建可路由扩展(通常是整页内容),component必须懒加载,mountPoint绑定一个RouteRef,作为外部组件与插件链接到该页面的句柄。

脚手架生成的ExampleComponent演示了一个典型的 Backstage 页面组件,ExampleFetchComponent则演示了常见的异步请求场景——调用公共 API 并用 Material UI 表格展示响应数据。这两个组件都可以按需改名、调整或整体替换。仓库中大量真实插件遵循同一模式,例如 plugins/catalog 中的catalogPlugin、plugins/search 中的searchPlugin,可作参考。

组合系统(Composability System)

组合系统是让众多插件的内容汇聚到一个 Backstage App 的机制。其核心原则是:插件之间应有清晰的边界与连接——隔离单插件内的崩溃,同时允许插件间导航;插件按需加载;插件可以为其他插件提供扩展点。它并非单一 API,而是模式、原语与 API 的集合,主要包括扩展(Extensions)、组件数据(Component Data)与RouteRef

组件数据(Component Data)

组件数据为 React 组件提供了一维新的数据维度:用键把数据挂到组件上,再用同一键从 JSX 元素读取:

const MyComponent = () => <h1>This is my component</h1>; attachComponentData(MyComponent, 'my.data', 5); const element = <MyComponent />; const myData = getComponentData(element, 'my.data'); // myData === 5

这种"渲染前检查元素"的模式在react-routermaterial-ui等库中很常见,但组件数据提供了更结构化的访问方式,并允许多个版本的数据同时被解释,从而简化演进。它的一个重要用途是支持基于 App 元素树的插件与路由发现,让 React 元素树成为插件使用情况与顶层路由的"事实来源"。

扩展(Extensions)

扩展是插件导出给 App 使用的对象,最常见的是 React 组件,也可以是任意 JavaScript 值。其类型定义十分简单:

export type Extension<T> = { expose(plugin: BackstagePlugin): T; };

核心 API 目前提供两种扩展创建函数:

  • createComponentExtension:普通 React 组件,无特殊要求(如实体概览页的卡片),导出时会被包装以提供错误边界、懒加载与插件上下文;
  • createRoutableExtension:在组件扩展之上构建,用于渲染在特定路由路径上的组件(如顶层页面、实体页签内容),创建时必须提供一个RouteRef作为mountPoint

除了核心库,部分插件还提供自己的扩展创建函数,例如@backstage/plugin-scaffoldercreateScaffolderFieldExtension。扩展并不绑定 React,未来可建模通用 JavaScript 概念或桥接到其他渲染框架。

官方推荐把导出的扩展放在顶层plugin.ts或专门的extensions.ts(或.tsx)中,但实现主体应放在其他文件,并通过懒加载引入。组件扩展的懒加载示例:

export const EntityFooCard = plugin.provide( createComponentExtension({ component: { lazy: () => import('./components/FooCard').then(m => m.FooCard), }, }), );

可路由扩展则强制懒加载,这是唯一的组件提供方式(见上文plugin.ts示例)。

在 App 中使用扩展

所有扩展必须同处于一棵从根AppProvider出发的 React 元素树中。因此以下写法不可行

const AppRoutes = () => ( <Routes> <Route path="/foo" element={<FooPage />} /> <Route path="/bar" element={<BarPage />} /> </Routes> ); const App = () => ( <AppProvider> <AppRouter> <Root> <AppRoutes /> </Root> </AppRouter> </AppProvider> );

修复方式是不要在 App 中创建中间组件,直接使用元素:

const appRoutes = ( <Routes> <Route path="/foo" element={<FooPage />} /> <Route path="/bar" element={<BarPage />} /> </Routes> ); const App = () => ( <AppProvider> <AppRouter> <Root>{appRoutes}</Root> </AppRouter> </AppProvider> );

导出命名规范

为明确导出符号的意图与用途,应遵循以下命名模式:

描述模式示例
顶层页面*PageCatalogIndexPageSettingsPageLighthousePage
实体页签内容Entity*ContentEntityJenkinsContentEntityKubernetesContent
实体概览卡片Entity*CardEntitySentryCardEntityPagerDutyCard
实体条件判断is*AvailableisPagerDutyAvailableisJenkinsAvailable
插件实例*PluginjenkinsPlugincatalogPlugin
工具 API 引用*ApiRefconfigApiRefcatalogApiRef

路由系统:RouteRef 与 ExternalRouteRef

基本路由

每个插件可导出一个RouteRef作为扩展的挂载点。官方建议把路由引用放在独立的顶层src/routes.ts中以避免循环导入:

/* src/routes.ts */ import { createRouteRef } from '@backstage/core-plugin-api'; // 注意:此路由引用仅供内部使用,不要从插件包导出 export const rootRouteRef = createRouteRef({ id: 'Example Page', });

RouteRef在运行时会被绑定到一个具体的path,但通过一层间接寻址让互不相识的插件可以互相路由。例如:

const appRoutes = ( <Routes> <Route path="/foo" element={<FooPage />} /> <Route path="/bar" element={<BarPage />} /> </Routes> );

假设FooPage是可路由扩展,其 mount point 为fooPageRouteRef,则fooPageRouteRef会被关联到/foo路由。可以用useRouteRef钩子生成具体链接:

const MyComponent = () => { const fooRoute = useRouteRef(fooPageRouteRef); return <a href={fooRoute()}>Link to Foo</a>; };

外部路由引用

如果barPlugin想链接到fooPlugin的页面,直接引用fooPageRouteRef会制造不必要的跨插件依赖,也缺乏灵活性。解决方案是使用ExternalRouteRef——它同样可以传给useRouteRef生成 URL,但不能作为可路由组件的 mount point,而是由 App 在启动时通过路由绑定(route bindings)把它关联到某个目标RouteRef。命名上应使用描述"角色"的中性名称:

const headerLinkRouteRef = createExternalRouteRef({ id: 'header-link' });

App 端的绑定通过createApp完成:

createApp({ bindRoutes({ bind }) { bind(barPlugin.externalRoutes, { headerLink: fooPlugin.routes.root, }); }, });

插件的路由引用通过createPluginroutes/externalRoutes字段暴露:

// 在 foo-plugin 中 export const fooPlugin = createPlugin({ routes: { root: fooPageRouteRef, }, ... }) // 在 bar-plugin 中 export const barPlugin = createPlugin({ externalRoutes: { headerLink: headerLinkRouteRef, }, ... })

路由引用本身应放在routes.ts之类的独立文件中,避免循环导入。也可以使用静态配置完成绑定(无需改 App 代码,但失去类型安全),配置位于app-config.yamlapp.routes.bindings键下:

app: routes: bindings: bar.headerLink: foo.root

自 Backstage 1.28 起,外部路由引用还支持默认目标:

export const createComponentExternalRouteRef = createExternalRouteRef({ defaultTarget: 'scaffolder.createComponent', });

可选外部路由

ExternalRouteRef可以标记为optional: true,此时不要求在 App 中绑定,可作为"是否显示某链接/执行某操作"的开关:

const headerLinkRouteRef = createExternalRouteRef({ id: 'header-link', optional: true, });

此时useRouteRef的返回签名变为RouteFunc | undefined

const MyComponent = () => { const headerLink = useRouteRef(headerLinkRouteRef); return ( <header> My Header {headerLink && <a href={headerLink()}>External Link</a>} </header> ); };

参数化路由与子路由

RouteRef支持命名且带类型的参数,参数在创建时声明,并在 App 路径与useRouteRef调用中强制校验:

// 创建参数化路由 const myRouteRef = createRouteRef({ id: 'myroute', params: ['name'] }) // 在 App 中,MyPage 是以 myRouteRef 为 mountPoint 的可路由扩展 <Route path='/my-page/:name' element={<MyPage />}/> // 在组件内使用 const myRoute = useRouteRef(myRouteRef) return ( <div> <a href={myRoute({name: 'a'})}>A</a> <a href={myRoute({name: 'b'})}>B</a> </div> )

目前参数化的ExternalRouteRef尚不支持,也无法把外部路由绑定到参数化路由。此外,SubRouteRef可创建相对于某个绝对RouteRef的固定路径路由引用,适合页面内部挂载在可路由扩展的子路由上、且需要被其他插件路由的场景:

// routes.ts const rootRouteRef = createRouteRef({ id: 'root' }); const detailsRouteRef = createSubRouteRef({ id: 'root-sub', parent: rootRouteRef, path: '/details', }); // plugin.ts export const myPlugin = createPlugin({ routes: { root: rootRouteRef, details: detailsRouteRef, }, }); export const MyPage = myPlugin.provide( createRoutableExtension({ name: 'MyPage', component: () => import('./components/MyPage').then(m => m.MyPage), mountPoint: rootRouteRef, }), ); // components/MyPage.tsx const MyPage = () => ( <Routes> <Route path="/" element={<IndexPage />} /> <Route path="/details" element={<DetailsPage />} /> </Routes> );

迁移存量插件到组合系统

将旧插件移植到组合系统的要点:

  • 移除createPlugin中的router.addRoute/router.registerRoute,改为导出可路由扩展;
  • Router导出改为可路由扩展;
  • 把普通组件导出(如目录概览卡片)改为组件扩展;
  • 停止导出RouteRef,改为传给createPlugin
  • 停止以 props 接收或从其他插件导入RouteRef,改用ExternalRouteRef并传给createPlugin
  • 按命名模式表重命名其余导出符号。

这些改动属于破坏性变更,若需向后兼容,应先废弃旧导出再逐步移除。命名模式对照如下:

描述旧模式新模式示例
顶层页面Router*PageCatalogIndexPageSettingsPageLighthousePage
实体页签内容RouterEntity*ContentEntityJenkinsContentEntityKubernetesContent
实体概览卡片*CardEntity*CardEntitySentryCardEntityPagerDutyCard
实体条件判断isPluginApplicableToEntityis*AvailableisPagerDutyAvailableisJenkinsAvailable
插件实例plugin*PluginjenkinsPlugincatalogPlugin

将插件集成到软件目录

如果你的插件服务于软件目录(例如作为"Overview"页签中的附加页签或卡片),可遵循 docs/plugins/integrating-plugin-into-software-catalog.md 的步骤。这是一个进阶用例,当前属于实验特性,API 可能随版本变化。

第一步:创建插件

与独立插件流程相同:

$ yarn new # 选择 frontend-plugin > ? Enter an ID for the plugin [required] my-plugin > ? Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional] Creating the plugin...

第二步:在插件内读取实体

@backstage/plugin-catalog-reactuseEntity访问当前选中的实体:

import { useEntity } from '@backstage/plugin-catalog-react'; export const MyPluginEntityContent = () => { const entity = useEntity(); // 使用实体数据... };

useEntity内部基于 React Context 实现,实体上下文由插件所嵌入的实体页面提供。

第三步:导入并嵌入实体页面

在 App 根包的packages/app/src/components/Catalog/EntityPage.tsx中导入插件组件:

import { MyPluginEntityContent } from '@backstage/plugin-my-plugin';

EntityPage.tsx通过EntitySwitch按实体 kind 分发到不同页面:

export const entityPage = ( <EntitySwitch> <EntitySwitch.Case if={isKind('component')} children={componentPage} /> <EntitySwitch.Case if={isKind('api')} children={apiPage} /> <EntitySwitch.Case if={isKind('group')} children={groupPage} /> <EntitySwitch.Case if={isKind('user')} children={userPage} /> <EntitySwitch.Case if={isKind('system')} children={systemPage} /> <EntitySwitch.Case if={isKind('domain')} children={domainPage} /> <EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case> </EntitySwitch> );

若扩展的是目录模型本身,需要给EntitySwitch增加新的 Case;若是给现有实体类型添加插件,则修改对应页面。例如给systemPage增加一个页签:

const systemPage = ( <EntityLayout> <EntityLayout.Route path="/" title="Overview"> <Grid container spacing={3} alignItems="stretch"> <Grid item md={6}> <EntityAboutCard /> </Grid> <Grid item md={6}> <EntityHasComponentsCard variant="gridItem" /> </Grid> <Grid item md={6}> <EntityHasApisCard variant="gridItem" /> </Grid> <Grid item md={6}> <EntityHasResourcesCard variant="gridItem" /> </Grid> </Grid> </EntityLayout.Route> <EntityLayout.Route path="/diagram" title="Diagram"> <EntityCatalogGraphCard variant="gridItem" height={400} /> </EntityLayout.Route> {/* 给 system 视图新增页签 */} <EntityLayout.Route path="/your-custom-route" title="CustomTitle"> <MyPluginEntityContent /> </EntityLayout.Route> </EntityLayout> );

EntitySwitch 与 EntityLayout 的目录组件

@backstage/catalog插件提供的EntitySwitch会从一组EntitySwitch.Case子元素中至多选择一个渲染。if属性是一个(entity: Entity) => boolean函数,例如isKind的实现:

function isKind(kind: string) { return (entity: Entity) => entity.kind.toLowerCase() === kind.toLowerCase(); }
<EntitySwitch> <EntitySwitch.Case if={isKind('template')}> <MyTemplate /> </EntitySwitch.Case> <EntitySwitch.Case> <MyOther /> </EntitySwitch.Case> </EntitySwitch>

EntitySwitch渲染第一个if返回true的 Case 的 children;若都不匹配则不渲染任何内容;未指定if的 Case 永远匹配。@backstage/catalog内置了isKindisComponentTypeisResourceTypeisEntityWithisNamespace等条件。EntityLayout则是EntityPageLayout的替代品,用于组织实体页签。值得注意的是,新前端系统下实体页面集成改用EntityCardBlueprintEntityContentBlueprint,可参考 docs/frontend-system/building-plugins/03-common-extension-blueprints.md。

与外部 API 通信的三种策略

前端插件与已存在服务 API 通信有三种选择,见 docs/plugins/call-existing-api.md。以下以虚构的 FrobsCo API 为例。

策略一:直接请求

最基础的方式是在插件前端直接用fetchaxios等库向 API 发起请求:

import { useAsync, useMountEffect } from '@react-hookz/web'; function AwesomeUsersTable() { const [{ status, result, error }, { execute }] = useAsync(async () => { const response = await fetch('https://api.frobsco.com/v1/list'); return response.json(); }); useMountEffect(execute); ... }

直接请求仅适用于以下场景:

  • API 已经暴露了你需要的精确能力;
  • 请求/响应模式符合实际使用需求(例如避免一次拉取 30MB 冗余数据拖垮移动端,或避免"每个单元格一次请求"淹没浏览器);
  • API 能在峰值速率下维持可交互的响应时间;
  • API 高可用(浏览器没有内置负载均衡、服务发现、重试、健康检查与熔断);
  • API 通过 HTTPS 暴露并正确处理 CORS;
  • API 在网络上易于被终端用户触达;
  • 请求无需传递机密(OAuth token 除外,前端可以自行协商使用)。

策略二:使用 Backstage 代理

Backstage 后端自带可选的代理插件,可轻松为下游 API 添加代理路由。先配置app-config.yaml

proxy: '/frobs': http://api.frobsco.com/v1

前端通过discoveryApiReffetchApiRef访问:

import { useApi, discoveryApiRef, fetchApiRef, } from '@backstage/core-plugin-api'; import { useAsync, useMountEffect } from '@react-hookz/web'; function FrobsAggregator() { const fetchApi = useApi(fetchApiRef); const discoveryApi = useApi(discoveryApiRef); const [{ status, result, error }, { execute }] = useAsync(async () => { const baseUrl = await discoveryApi.getBaseUrl('proxy'); const response = await fetchApi.fetch(`${baseUrl}/frobs`); return response.json(); }); useMountEffect(execute); // ... }

代理由http-proxy-middleware驱动,完整配置见 docs/plugins/proxying.md。相比直接请求,代理适用于:

  • API 自身未提供 HTTPS 终止或 CORS 处理;
  • 需要在请求中注入静态机密(如追加到请求头的 Authorization 头);
  • 需要代理设施(重试、故障转移、健康检查、路由、请求日志、重写等);
  • 希望 Backstage 后端作为唯一入口统一治理对外访问。

当前仓库的 app-config.yaml 中就内置了一个典型示例:

proxy: endpoints: '/pagerduty': target: https://api.pagerduty.com headers: Authorization: Token token=${PAGERDUTY_TOKEN}

这展示了如何把请求转发到 PagerDuty 并在转发时注入从环境变量读取的令牌。

策略三:创建 Backstage 后端插件

Backstage 后端同样有插件体系,前述代理本身就是其中一个后端插件。当集成比"直接访问 FrobsCo API"更复杂、或需要持有状态时,应创建后端插件。例如在frobs-aggregator后端插件中新增路由:

import Router from 'express-promise-router'; export async function createRouter() { const router = Router(); router.use(express.json()); router.get('/summary', async (req, res) => { const agg = await Promise.all([ fetch('https://api.frobsco.com/v1/list'), fetch('http://flerps.partnercompany.com:8080/flerp-batch'), database.currentThunk(), ]).then(async ([frobs, flerps, thunk]) => { return computeAggregate(await frobs.json(), await flerps.json(), thunk); }); res.status(200).json(agg); }); }

前端插件通过discoveryApi.getBaseUrl('frobs-aggregator')获取后端插件基地址并请求聚合接口:

const baseUrl = await discoveryApi.getBaseUrl('frobs-aggregator'); const response = await fetchApi.fetch(`${baseUrl}/summary`); return response.json();

后端插件方案适用于:

  • 需要代理无法处理的复杂模型转换或协议翻译;
  • 需要在后端而非前端做聚合或摘要;
  • 需要对较慢或不稳定的 API 做批处理或缓存;
  • 需要为插件维护状态(可借助后端内置数据库支持);
  • 需要注入机密或与其他服务协商;
  • 需要为 API 操作实施终端用户认证/授权、会话处理等。

仓库中 plugins/user-settings-backend 是一个在数据库中存储状态并为前端插件提供 API 的参考实现。

代理配置详解

代理配置位于app-config.yamlproxy根键下:

proxy: reviveConsumedRequestBodies: true endpoints: /simple-example: http://simple.example.com:8080 '/larger-example/v1': target: http://larger.example.com:8080/svc.v1 credentials: require headers: Authorization: ${EXAMPLE_AUTH_HEADER} # ...或把值插值进字符串的一部分 # Authorization: Bearer ${EXAMPLE_AUTH_TOKEN}
  • endpoints下每个键都是代理插件挂载前缀之下的一个路由;若不以斜杠开头会自动补上。例如代理插件挂载在/proxy,则上面的配置会让代理处理/api/proxy/simple-example/.../api/proxy/larger-example/v1/...的请求。
  • 每个路由的值既可以是简单的 URL 字符串,也可以是http-proxy-middleware接受的配置对象,外加可选的credentials键,取值如下:
行为
require调用方必须携带 Backstage 用户或服务凭据,但凭据转发给代理目标。默认值。
forward调用方必须携带 Backstage 用户或服务凭据,且这些凭据会转发给代理目标。
dangerously-allow-unauthenticated无需 Backstage 凭据即可访问该代理目标;若同时配置allowedHeaders: ['Authorization'],则提供的 Backstage token 会被转发。

如果设置了backend.auth.dangerouslyDisableDefaultAuthPolicy: truecredentials配置不生效,所有端点都按dangerously-allow-unauthenticated处理。

字符串形式等价于:

target: <the string> changeOrigin: true pathRewrite: '^<url prefix><the string>/': '/' credentials: require

对象形式会原样传给http-proxy-middleware,但有三个便利默认值:changeOrigin未指定时设为truepathRewrite未指定时添加一条移除整个前缀与路由的重写规则(例如上例中/api/proxy/larger-example/v1/some/path会被翻译为http://larger.example.com:8080/svc.v1/some/path);credentials未指定时设为require

其他可选项:

  • allowedMethods:限制转发的 HTTP 方法,例如allowedMethods: ['GET']可强制只读访问;
  • allowedHeaders:允许转发/接收的头部列表。默认只转发 CORS 安全头(如content-typelast-modified)以及代理自身设置的头部;要转发authorization等头部必须显式配置allowedHeaders: ['Authorization'],以免把cookieX-Auth-Request-User等机密头意外转发给第三方。

设置proxy.reviveConsumedRequestBodies: true可解决请求体被代理消费后无法转发给目标的问题,此时会启用http-proxy-middlewarefixRequestBody处理器,并需把Content-Type设为application/jsonapplication/x-www-form-urlencoded

代理插件还支持proxyEndpointsExtensionPoint,供代理模块以编程方式注册额外端点(配置格式与 app-config 相同,且 app-config 中的配置始终覆盖编程注册的端点)。创建方式为运行yarn new、选择backend-module、插件 ID 填proxy,生成plugins/proxy-backend-module-<moduleId>后添加依赖:

yarn --cwd plugins/proxy-backend-module-demo-additional-endpoints add @backstage/plugin-proxy-node

然后在src/module.ts中使用扩展点:

import { createBackendModule } from '@backstage/backend-plugin-api'; import { proxyEndpointsExtensionPoint } from '@backstage/plugin-proxy-node/alpha'; export const proxyModuleDemoAdditionalEndpoints = createBackendModule({ pluginId: 'proxy', moduleId: 'demo-additional-endpoints', register(reg) { reg.registerInit({ deps: { proxyEndpoints: proxyEndpointsExtensionPoint, }, async init({ proxyEndpoints }) { // 替换为你的环境获取凭据的方式 const largerExampleAuth = 'Bearer <token>'; proxyEndpoints.addProxyEndpoints({ '/simple-example': 'http://simple.example.com:8080', '/larger-example/v1': { target: 'http://larger.example.com:8080/svc.v1', credentials: 'require', headers: { Authorization: largerExampleAuth, }, }, }); }, }); }, });

src/index.ts导出该模块:

export { proxyModuleDemoAdditionalEndpoints as default } from './module';

最后在后端入口(通常为packages/backend/src/index.ts)同时安装代理插件与模块:

backend.add(import('@backstage/plugin-proxy-backend')); backend.add( import('@internal/plugin-proxy-backend-module-demo-additional-endpoints'), );

插件的单元测试

Backstage 使用 Jest 进行单元测试(相关说明见 docs/plugins/testing.md)。运行全部测试:

yarn test

运行单个测试文件(如MyComponent.test.tsx):

yarn test MyComponent

同时运行多个测试套件:

yarn test MyComponent MyControl

测试文件应命名为[filename].test.ts,若包含 JSX(如 React 组件测试)则用[filename].test.tsx。脚手架生成的插件已经包含ExampleComponent.test.tsxExampleFetchComponent.test.tsxplugin.test.ts,可作为测试的起点。

分享与发现插件

向社区提交插件

如果你在开发开源插件,官方鼓励在社区插件仓库提交 issue,向社区通告即将推出的插件并邀请协作与反馈。即使你只是有了一个可能有影响力的插件想法、但希望由其他贡献者来开发,这种方式同样适用。

发现现有插件

社区已有大量现成插件,可以通过 Backstage 插件目录查找。关于插件目录的更多信息,可阅读仓库内的 docs/plugins/plugin-directory-audit.md(插件目录审计)与 docs/plugins/add-to-directory.md(将插件加入目录)了解目录收录与审计机制。若希望自己的插件被收录,遵循 docs/plugins/add-to-directory.md 的提交要求即可。

延伸阅读

  • 新前端系统:创建前端插件请见 docs/frontend-system/building-plugins/01-index.md
  • 新后端系统:创建后端插件与模块请见 docs/backend-system/building-plugins-and-modules/01-index.md
  • 插件功能开关:docs/plugins/feature-flags.md
  • 插件国际化:docs/plugins/internationalization.md
  • 插件可观测性:docs/plugins/observability.md
  • 插件分析:docs/plugins/analytics.md
  • 将搜索集成进插件:docs/plugins/integrating-search-into-plugins.md
  • 新后端系统插件编写:docs/plugins/new-backend-system.md
  • 后端插件编写:docs/plugins/backend-plugin.md

【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询