ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

Backstage 插件开发指南:从创建、组合到与软件目录集成 Backstage 插件开发指南从创建、组合到与软件目录集成【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstageBackstage 的核心设计理念之一是通过插件把各类基础设施与软件研发工具无缝集成到一个统一的开发者门户中。本文以仓库 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.json与src目录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 () h1This is my component/h1; attachComponentData(MyComponent, my.data, 5); const element MyComponent /; const myData getComponentData(element, my.data); // myData 5这种渲染前检查元素的模式在react-router、material-ui等库中很常见但组件数据提供了更结构化的访问方式并允许多个版本的数据同时被解释从而简化演进。它的一个重要用途是支持基于 App 元素树的插件与路由发现让 React 元素树成为插件使用情况与顶层路由的事实来源。扩展Extensions扩展是插件导出给 App 使用的对象最常见的是 React 组件也可以是任意 JavaScript 值。其类型定义十分简单export type ExtensionT { expose(plugin: BackstagePlugin): T; };核心 API 目前提供两种扩展创建函数createComponentExtension普通 React 组件无特殊要求如实体概览页的卡片导出时会被包装以提供错误边界、懒加载与插件上下文createRoutableExtension在组件扩展之上构建用于渲染在特定路由路径上的组件如顶层页面、实体页签内容创建时必须提供一个RouteRef作为mountPoint。除了核心库部分插件还提供自己的扩展创建函数例如backstage/plugin-scaffolder的createScaffolderFieldExtension。扩展并不绑定 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 );导出命名规范为明确导出符号的意图与用途应遵循以下命名模式描述模式示例顶层页面*PageCatalogIndexPage、SettingsPage、LighthousePage实体页签内容Entity*ContentEntityJenkinsContent、EntityKubernetesContent实体概览卡片Entity*CardEntitySentryCard、EntityPagerDutyCard实体条件判断is*AvailableisPagerDutyAvailable、isJenkinsAvailable插件实例*PluginjenkinsPlugin、catalogPlugin工具 API 引用*ApiRefconfigApiRef、catalogApiRef路由系统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, }); }, });插件的路由引用通过createPlugin的routes/externalRoutes字段暴露// 在 foo-plugin 中 export const fooPlugin createPlugin({ routes: { root: fooPageRouteRef, }, ... }) // 在 bar-plugin 中 export const barPlugin createPlugin({ externalRoutes: { headerLink: headerLinkRouteRef, }, ... })路由引用本身应放在routes.ts之类的独立文件中避免循环导入。也可以使用静态配置完成绑定无需改 App 代码但失去类型安全配置位于app-config.yaml的app.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 | undefinedconst 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*PageCatalogIndexPage、SettingsPage、LighthousePage实体页签内容RouterEntity*ContentEntityJenkinsContent、EntityKubernetesContent实体概览卡片*CardEntity*CardEntitySentryCard、EntityPagerDutyCard实体条件判断isPluginApplicableToEntityis*AvailableisPagerDutyAvailable、isJenkinsAvailable插件实例plugin*PluginjenkinsPlugin、catalogPlugin将插件集成到软件目录如果你的插件服务于软件目录例如作为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-react的useEntity访问当前选中的实体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/ titleOverview Grid container spacing{3} alignItemsstretch Grid item md{6} EntityAboutCard / /Grid Grid item md{6} EntityHasComponentsCard variantgridItem / /Grid Grid item md{6} EntityHasApisCard variantgridItem / /Grid Grid item md{6} EntityHasResourcesCard variantgridItem / /Grid /Grid /EntityLayout.Route EntityLayout.Route path/diagram titleDiagram EntityCatalogGraphCard variantgridItem height{400} / /EntityLayout.Route {/* 给 system 视图新增页签 */} EntityLayout.Route path/your-custom-route titleCustomTitle 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 /EntitySwitchEntitySwitch渲染第一个if返回true的 Case 的 children若都不匹配则不渲染任何内容未指定if的 Case 永远匹配。backstage/catalog内置了isKind、isComponentType、isResourceType、isEntityWith、isNamespace等条件。EntityLayout则是EntityPageLayout的替代品用于组织实体页签。值得注意的是新前端系统下实体页面集成改用EntityCardBlueprint与EntityContentBlueprint可参考 docs/frontend-system/building-plugins/03-common-extension-blueprints.md。与外部 API 通信的三种策略前端插件与已存在服务 API 通信有三种选择见 docs/plugins/call-existing-api.md。以下以虚构的 FrobsCo API 为例。策略一直接请求最基础的方式是在插件前端直接用fetch或axios等库向 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 暴露并正确处理 CORSAPI 在网络上易于被终端用户触达请求无需传递机密OAuth token 除外前端可以自行协商使用。策略二使用 Backstage 代理Backstage 后端自带可选的代理插件可轻松为下游 API 添加代理路由。先配置app-config.yamlproxy: /frobs: http://api.frobsco.com/v1前端通过discoveryApiRef与fetchApiRef访问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.yaml的proxy根键下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 prefixthe string/: / credentials: require对象形式会原样传给http-proxy-middleware但有三个便利默认值changeOrigin未指定时设为truepathRewrite未指定时添加一条移除整个前缀与路由的重写规则例如上例中/api/proxy/larger-example/v1/some/path会被翻译为http://larger.example.com:8080/svc.v1/some/pathcredentials未指定时设为require。其他可选项allowedMethods限制转发的 HTTP 方法例如allowedMethods: [GET]可强制只读访问allowedHeaders允许转发/接收的头部列表。默认只转发 CORS 安全头如content-type、last-modified以及代理自身设置的头部要转发authorization等头部必须显式配置allowedHeaders: [Authorization]以免把cookie、X-Auth-Request-User等机密头意外转发给第三方。设置proxy.reviveConsumedRequestBodies: true可解决请求体被代理消费后无法转发给目标的问题此时会启用http-proxy-middleware的fixRequestBody处理器并需把Content-Type设为application/json或application/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.tsxyarn test MyComponent同时运行多个测试套件yarn test MyComponent MyControl测试文件应命名为[filename].test.ts若包含 JSX如 React 组件测试则用[filename].test.tsx。脚手架生成的插件已经包含ExampleComponent.test.tsx、ExampleFetchComponent.test.tsx与plugin.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),仅供参考
返回列表