3. 插件开发基础:Scaffolder模板、插件目录结构、package.json配置

在Backstage中,插件是功能扩展的基本单元。本章聚焦于插件开发的核心基础:Scaffolder模板(快速生成插件骨架)、插件目录结构(约定优于配置)以及package.json配置(元数据与依赖管理)。掌握这三部分,是构建隔离良好、可维护的插件的第一步。

3.1 使用Scaffolder模板创建插件

Backstage内置了强大的Scaffolder(脚手架)系统,用于快速生成标准化的插件项目结构。推荐使用npx @backstage/create-app或yarn create backstage-app后,通过以下命令创建插件:

# 在Backstage根目录下执行
yarn new --select plugin

# 或使用backstage-cli
yarn backstage-cli new plugin my-plugin-name

执行后,Scaffolder会交互式询问插件名称、是否包含后端、是否包含React组件等选项。生成的插件骨架遵循官方最佳实践,自动配置了TypeScript、ESLint、Jest等工具链。

最佳实践:始终使用Scaffolder创建插件,而非手动复制。这能确保目录结构、配置文件版本与当前Backstage版本兼容,减少因结构差异导致的隔离问题。

3.2 插件目录结构详解

一个标准的Backstage插件(以my-plugin为例)通常包含以下目录与文件:

路径 用途 是否必需
plugins/my-plugin/ 插件根目录 是
plugins/my-plugin/src/ 前端React源码(组件、路由、API) 是
plugins/my-plugin/src/index.ts 插件入口,导出插件实例与组件 是
plugins/my-plugin/src/plugin.ts 定义createPlugin实例,注册路由、API、外部路由 是
plugins/my-plugin/src/components/ React组件目录 推荐
plugins/my-plugin/src/routes.ts 定义插件内部路由路径常量 推荐
plugins/my-plugin/src/types.ts TypeScript类型定义 按需
plugins/my-plugin/dev/ 开发环境入口,用于独立运行插件 推荐
plugins/my-plugin/dev/index.ts 开发模式下的App实例,包含Mock数据 推荐
plugins/my-plugin/package.json 插件元数据、依赖、脚本 是
plugins/my-plugin/tsconfig.json TypeScript编译配置 是
plugins/my-plugin/README.md 插件说明文档 推荐
隔离视角:每个插件拥有独立的src/、package.json和tsconfig.json,这从物理上实现了代码隔离。插件之间通过Backstage的API系统和外部路由进行通信,而非直接引用对方源码,这是故障域隔离的基础。
插件目录结构与隔离边界 插件A (my-plugin-a) src/index.ts src/plugin.ts src/components/ src/routes.ts dev/index.ts package.json tsconfig.json README.md 隔离边界 插件B (my-plugin-b) src/index.ts src/plugin.ts src/components/ src/routes.ts dev/index.ts package.json tsconfig.json README.md 每个插件拥有独立的目录、package.json和tsconfig,物理隔离是故障域隔离的基础

3.3 package.json配置要点

插件的package.json是Backstage识别和加载插件的核心配置文件。除了标准的npm字段外,还包含Backstage特有的元数据。

3.3.1 核心字段说明

字段 说明 示例值
name 插件包名,遵循@backstage/plugin-*或@internal/plugin-*命名规范 @backstage/plugin-scaffolder
version 语义化版本号 1.0.0
main 前端入口文件,指向编译后的JS文件 dist/index.esm.js
types TypeScript类型声明入口 dist/index.d.ts
backstage.role Backstage特有,声明插件角色(frontend-plugin, backend-plugin, common-library等) frontend-plugin
backstage.pluginId Backstage特有,插件唯一标识符,用于路由和API注册 my-plugin
keywords 推荐包含backstage和plugin,便于搜索 ["backstage", "plugin"]

3.3.2 典型package.json示例

{
  "name": "@internal/plugin-capacity-planner",
  "version": "0.1.0",
  "main": "dist/index.esm.js",
  "types": "dist/index.d.ts",
  "license": "Apache-2.0",
  "backstage": {
    "role": "frontend-plugin",
    "pluginId": "capacity-planner",
    "pluginPackages": [
      "@internal/plugin-capacity-planner"
    ]
  },
  "scripts": {
    "start": "backstage-cli package start",
    "build": "backstage-cli package build",
    "lint": "backstage-cli package lint",
    "test": "backstage-cli package test",
    "clean": "backstage-cli package clean"
  },
  "dependencies": {
    "@backstage/core-plugin-api": "^1.9.0",
    "@backstage/plugin-catalog-react": "^1.12.0",
    "@material-ui/core": "^4.12.2",
    "react": "^17.0.2",
    "react-router-dom": "^6.3.0"
  },
  "devDependencies": {
    "@backstage/cli": "^0.26.0",
    "@testing-library/jest-dom": "^6.0.0",
    "@testing-library/react": "^12.1.0"
  },
  "peerDependencies": {
    "react": "^16.13.1 || ^17.0.0"
  }
}
隔离注意事项:
  • 依赖隔离:避免在dependencies中引入与Backstage核心库版本冲突的包。使用peerDependencies声明对React等共享库的版本要求。
  • 角色声明:backstage.role字段决定了插件在构建和运行时如何被处理。前端插件(frontend-plugin)和后端插件(backend-plugin)的构建目标不同,错误声明会导致加载失败。
  • 插件ID唯一性:backstage.pluginId在整个Backstage实例中必须唯一,否则会导致路由冲突和API覆盖,破坏故障域隔离。

3.4 插件入口与导出规范

每个插件必须通过src/index.ts导出核心内容,Backstage的App Builder通过扫描这些导出加载插件。

3.4.1 标准导出结构

// plugins/my-plugin/src/index.ts
export { myPluginPlugin } from './plugin';
export { MyPluginPage } from './components/MyPluginPage';
export { MyPluginCard } from './components/MyPluginCard';
export type { MyPluginApi } from './types';

3.4.2 plugin.ts 核心定义

// plugins/my-plugin/src/plugin.ts
import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';

export const myPluginPlugin = createPlugin({
  id: 'my-plugin',
  routes: {
    root: rootRouteRef,
  },
});

export const MyPluginPage = myPluginPlugin.provide(
  createRoutableExtension({
    name: 'MyPluginPage',
    component: () => import('./components/MyPluginPage').then(m => m.MyPluginPage),
    mountPoint: rootRouteRef,
  }),
);
故障域隔离设计:插件通过createPlugin定义独立的路由命名空间和API注册表。即使两个插件定义了相同的路由路径(如/dashboard),由于pluginId不同,Backstage会自动添加前缀(如/my-plugin/dashboard),避免路由冲突。这种机制确保了每个插件的故障域在路由层面是隔离的。
插件加载与路由隔离机制 App Builder 插件A (pluginId: "planner-a") 路由前缀: /planner-a/ API注册: planner-a-api 故障域A 插件B (pluginId: "planner-b") 路由前缀: /planner-b/ API注册: planner-b-api 故障域B pluginId唯一性确保路由前缀和API命名空间隔离,即使路径相同也不会冲突

3.5 开发环境与调试

插件开发时,建议使用dev/index.ts创建独立的开发入口,避免影响主应用:

// plugins/my-plugin/dev/index.ts
import { createDevApp } from '@backstage/dev-utils';
import { myPluginPlugin, MyPluginPage } from '../src';

createDevApp()
  .registerPlugin(myPluginPlugin)
  .addPage({
    path: '/my-plugin',
    title: 'My Plugin',
    element: <MyPluginPage />,
  })
  .render();

运行yarn start后,可在独立端口(通常为:3000)预览插件,与主应用完全隔离,便于快速迭代和故障排查。

开发效率提示:利用Backstage的模块热替换(HMR)机制,修改插件源码后无需重启整个Backstage实例。这在大规模插件开发中显著提升效率,同时保持故障域的独立性。