Backstage 插件结构详解:从目录骨架到扩展接入的完整实战指南
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本篇技术指南聚焦 Backstage 前端插件的标准目录结构与组成要素,以官方
structure-of-a-plugin文档为骨架,结合仓库内真实插件(plugins/example-todo-list)、CLI 模板(packages/cli-module-new/templates/legacy-frontend-plugin)以及组合系统文档(docs/plugins/composability.md)进行源码级印证。读完本篇,你将掌握插件目录中每个文件的作用、createPlugin与createRoutableExtension的用法、插件扩展在应用中的接入方式,以及插件与外部服务通信的代理方案。
:::note[文档适用范围说明] 本文描述的是旧前端系统(legacy frontend system)下的插件结构。Backstage 已推出新的前端系统,相关说明见 Building Frontend Plugins。新旧系统的总体目录结构相似,但plugin.ts中的插件接线方式差异显著——旧系统使用createPlugin+createRoutableExtension,新系统则基于扩展蓝图(Extension Blueprints)。理解旧系统结构仍然是阅读大量存量插件源码的基础。 :::
一、插件目录结构:一个自包含的迷你项目
使用 Backstage CLI(如yarn new --select plugin或backstage-cli new --select plugin)创建新插件后,得到的目录大致如下:
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 包。它自带
package.json和src目录,看起来像一个迷你项目。这样做的目的有两个:一是插件可以独立发布到 npm,二是可以在隔离环境中单独开发某个插件,而无需在大型 Backstage 应用中加载所有其他插件。每个目录下的
index.ts充当导出聚合层。通过index.ts,其他代码可以从文件夹路径导入(import { ExampleComponent } from './components/ExampleComponent'),而不是逐个文件精确导入。这样可以将每个文件夹的对外导出集中控制在单个文件中。
仓库中的真实案例完全印证了这一结构:
- 官方示例插件 plugins/example-todo-list 的
src/下包含components/TodoList/、components/TodoListPage/、index.ts、plugin.test.ts、plugin.ts、routes.ts、setupTests.ts,与文档描述一一对应; - CLI 的插件生成模板 packages/cli-module-new/templates/legacy-frontend-plugin 中保留了
dev/index.tsx.hbs、src/components/ExampleComponent/、src/components/ExampleFetchComponent/、src/routes.ts.hbs、src/plugin.ts.hbs等全部文件,说明上文目录正是脚手架实际产出的形态。
二、基础文件:README 与 package.json
生成插件时,你会拿到一个待填充的README.md和一个package.json:
README.md:用于记录插件的信息、功能说明、配置方法等,发布到 npm 或接入目录(plugin directory)时是重要的元信息载体;package.json:声明插件的依赖、元数据和脚本。以 plugins/example-todo-list/package.json 为例,它包含:backstage字段:标记"role": "frontend-plugin"、pluginId以及关联的pluginPackages(前端、后端、公共包三件套);scripts:build、clean、lint、start、test等全部通过backstage-cli package ...系列命令驱动;dependencies:通常包含@backstage/core-plugin-api(核心插件 API)、@backstage/core-components(UI 组件),以及渲染层所需的 React 与 Material UI 相关库;peerDependencies:声明react、react-dom、react-router-dom等宿主运行时依赖,@types/react被标记为 optional。
三、插件文件 plugin.ts:插件的创建与扩展导出
src目录下的核心文件是plugin.ts,示例内容如下:
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创建插件实例:传入id(全局唯一标识)和routes(插件对外暴露的路由引用)。CLI 模板中的实现与之完全一致(见 plugin.ts.hbs),只是插件名、扩展名、id由脚手架参数动态填充。plugin.provide()创建并导出扩展:这里导出的是一个routable extension(可路由扩展)。createRoutableExtension要求提供:name:扩展名称;component:一个懒加载函数,返回import(...).then(m => m.xxx);mountPoint:该组件对应的RouteRef,是外部世界访问此组件的句柄,其他组件或插件通过它来链接到这个可路由组件。
可路由扩展强制使用懒加载,这也是保证插件按需加载、隔离插件内部崩溃的关键。在 plugins/example-todo-list/src/plugin.ts 中可以看到真实写法:todoListPlugin通过routes: { root: rootRouteRef }注册路由,TodoListPage通过todoListPlugin.provide(createRoutableExtension({...}))导出,component懒加载./components/TodoListPage。
除了createRoutableExtension,核心 API 还提供了createComponentExtension,用于导出没有路由要求的普通 React 组件(例如实体概览页上的卡片)。组件扩展同样支持开箱即用的懒加载:
export const EntityFooCard = plugin.provide( createComponentExtension({ component: { lazy: () => import('./components/FooCard').then(m => m.FooCard), }, }), );组件扩展和可路由扩展在导出时都会被包装,以提供错误边界(error boundary)、懒加载和插件上下文。推荐将扩展的创建集中放在顶层plugin.ts或专门的extensions.ts(或.tsx)文件中,而把具体实现放在懒加载的组件目录里,保持plugin.ts轻量。
关于createPlugin的完整 API 细节以及新组合系统的介绍,可参见仓库中的 组合系统文档。
3.1 routes.ts:路由引用的独立文件
plugin.ts中引用的rootRouteRef来自同目录下的routes.ts:
import { createRouteRef } from '@backstage/core-plugin-api'; export const rootRouteRef = createRouteRef({ id: 'example', });将路由引用单独放在routes.ts中是一个重要约定:避免在插件其他部分使用路由引用时产生循环导入(circular imports)。仓库示例 plugins/example-todo-list/src/routes.ts 中rootRouteRef的id为'todo-list'。
3.2 扩展的命名模式
为了让插件导出的符号意图清晰、避免导入别名,组合系统约定了一套命名模式:
| 描述 | 模式 | 示例 |
|---|---|---|
| 顶层页面 | *Page | CatalogIndexPage、SettingsPage、LighthousePage |
| 实体页签内容 | Entity*Content | EntityJenkinsContent、EntityKubernetesContent |
| 实体概览卡片 | Entity*Card | EntitySentryCard、EntityPagerDutyCard |
| 实体条件判断 | is*Available | isPagerDutyAvailable、isJenkinsAvailable |
| 插件实例 | *Plugin | jenkinsPlugin、catalogPlugin |
| 工具 API 引用 | *ApiRef | configApiRef、catalogApiRef |
存量插件向新组合系统迁移时,旧的Router、*Card、plugin等导出名需要按此表重命名(详见 组合系统文档中的迁移章节)。
四、组件目录:页面组件与数据获取组件
生成器会附带两个示例组件,用于演示插件的组件组织方式:
ExampleComponent:一个示例 Backstage 页面组件,演示如何用Page、Header、Content等核心组件搭起页面骨架;ExampleFetchComponent:演示最常见的任务——向公共 API 发起异步请求,并用 Material UI 的Table组件把响应数据渲染成表格(见 ExampleFetchComponent.tsx.hbs)。
仓库中的真实页面 TodoListPage.tsx 展示了完整的页面组织模式:
- 外层使用
Page(带themeId)、Header(含HeaderLabel)、Content、ContentHeader、SupportButton等@backstage/core-components组件; - 通过
useApi获取discoveryApiRef、fetchApiRef、alertApiRef等工具 API,用discoveryApi.getBaseUrl('todolist')得到后端基础地址后发起 fetch 请求; - 数据展示组件 TodoList.tsx 使用
react-use的useAsync管理加载状态,加载中显示Progress,出错显示Alert,成功则渲染Table。
一个插件通常有一个或多个页面组件,旁边可以根据需要把 UI 拆分任意多个组件。这些示例组件可以随意修改、重命名或整体替换。
五、把插件接入 Backstage 应用
要让 Backstage 应用真正使用一个插件,需要两步:
- 在
app/package.json中把插件声明为依赖(例如"@internal/plugin-todo-list": "workspace:^"); - 在
app/src/App.tsx中导入并使用一个或多个插件扩展,例如将ExamplePage挂载到路由:
import { ExamplePage } from 'new-plugin'; const routes = ( <FlatRoutes> ... <Route path="/example" element={<ExamplePage />} /> ... </FlatRoutes> );好消息是,使用 Backstage CLI 创建插件时,这两个步骤都会自动完成。此外,插件扩展必须位于从根AppProvider延伸出的同一棵 React 元素树中,不要在应用里插入中间组件,否则扩展无法正确解析。
5.1 路由如何被解析
组合系统的路由机制是:每个RouteRef在运行时被绑定到一个具体的path,而这个path是根据应用中的元素树发现的。例如<Route path="/foo" element={<FooPage />} />中的/foo会被关联到FooPage的挂载点fooPageRouteRef。之后其他组件可以通过useRouteRef钩子生成具体链接:
const MyComponent = () => { const fooRoute = useRouteRef(fooPageRouteRef); return <a href={fooRoute()}>Link to Foo</a>; };为避免插件之间产生不必要的直接依赖,插件间跳转应使用ExternalRouteRef,并在应用侧通过bindRoutes绑定(详见 组合系统文档)。从 Backstage 1.28 起,ExternalRouteRef还支持defaultTarget默认目标和optional可选绑定。RouteRef还支持带命名参数的参数化路由(如params: ['name'])以及相对固定路径的SubRouteRef。
六、与外部世界通信:代理(Proxy)方案
如果你的插件需要与 Backstage 环境之外的服务通信,通常会遇到两类问题:
- CORS 策略:浏览器直接跨域请求第三方服务会被同源策略拦截;
- 后端侧授权:外部服务可能要求携带凭证或鉴权信息。
为了平滑处理这些问题,可以使用代理:
- 复用已有代理:如 Nginx、HAProxy 等基础设施代理;
- 使用 Backstage 提供的 proxy-backend 插件:这是官方为 Backstage 后端提供的代理方案,插件通过
/api/proxy路径转发请求,从而绕开浏览器的跨域限制,并在后端统一注入鉴权。
详细配置与用法参见仓库内的 proxy-backend 插件说明 以及 代理文档。
七、组合系统核心概念速览
理解插件结构后,再补充几个组合系统的核心概念(完整说明见 组合系统文档),它们直接决定了插件扩展的设计方式:
- 组件数据(Component Data):通过
attachComponentData给 React 组件附加带 key 的数据,渲染前可用getComponentData从 JSX 元素读取,是路由与插件发现机制的底层支撑; - 扩展(Extensions):插件为应用导出的产物,绝大多数是 React 组件,通过
create*Extension创建并用plugin.provide()包装,类型本质是{ expose(plugin: BackstagePlugin): T }; - 路由引用(RouteRef):路由目标的间接句柄,避免不同开源插件互相硬编码路径;
- 实体条件渲染:catalog 插件提供
EntitySwitch/EntitySwitch.Case,配合isKind、isComponentType、isResourceType、isEntityWith、isNamespace等内置条件函数,可为不同实体类型渲染不同内容。
八、小结
Backstage 插件本质是一个自包含的 npm 包:package.json声明元数据与依赖,src/plugin.ts通过createPlugin创建插件实例并provide出懒加载的扩展,src/routes.ts独立维护路由引用以避免循环依赖,src/components/存放页面与可复用组件,src/index.ts收敛对外导出。接入应用只需两步——声明依赖并在App.tsx中挂载扩展;与外部服务通信则通过 proxy-backend 代理解决跨域与鉴权问题。
想动手实践,可以直接参考仓库中的 example-todo-list 示例插件,或使用backstage-cli new生成一个全新的插件脚手架,逐步替换其中的示例组件,即可完成第一个可运行页面的开发。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考