Skip to main content

前端扩展

**注意:新的前台系统处于alpha阶段,只有少数插件支持。

正如上一节本节将详细介绍什么是扩展、如何创建和使用扩展,以及如何创建自己的扩展模式。

扩展结构

每个扩展都有许多不同的属性,这些属性定义了扩展的行为方式以及与其他扩展和应用程序其他部分的交互方式。 其中一些属性是固定的,而其他一些属性则可以由集成商自定义。 下图说明了扩展的结构。

frontend extension structure diagram

ID

扩展的 ID 用于唯一标识,最好在整个 backstage 生态系统中都是唯一的。 对于每个前端应用程序实例,任何给定的 ID 只能有一个扩展。 安装具有相同 ID 的多个扩展要么会导致错误,要么其中一个扩展会覆盖其他扩展。 ID 还用于在其他扩展、配置以及开发人员工具和分析等其他地方引用扩展。

输出

扩展的输出是它向其父扩展提供的数据,最终是它对应用程序的贡献。 输出本身以任意值集合的形式出现,任何值都可以表示为 TypeScript 类型。 然而,每个单独的输出值都必须与称为扩展数据引用的共享引用相关联。 您还必须使用这些相同的引用才能访问扩展的单独输出值。

输入

扩展的输入定义了它从子扩展接收的数据。 每个扩展可以有多个不同的输入,由输入名称标识。 这些输入都有自己的数据集,它们被定义为扩展数据引用的集合。 扩展只能访问它从每个输入明确请求的数据。

连接点

扩展的附着点决定了它在应用程序扩展树中的位置。 它由父扩展的 ID 以及要附着的输入名称定义。 通过附着点,扩展将共享自己的输出作为父扩展的输入。 扩展只能附着到与其自身输出相匹配的输入上,如果尝试将扩展附着到需要数据的输入上,而扩展在其输出中并未提供这些数据,则会出错。

附加点是扩展的可配置属性之一,可由集成器重写。 在重写时,必须注意确保不会将扩展附加到不兼容的输入上。 扩展一次只能附加到一个输入和父节点上,这意味着应用程序扩展树不能包含任何循环,因为扩展祖先要么在根节点终止,要么从根节点分离。

###禁用

应用程序中的每个扩展都可以禁用,这意味着它不会被实例化,其父级程序也不会在输入中看到它。 创建扩展时,您还可以指定扩展是否应默认禁用。 这样就可以在应用程序中安装多个扩展,但根据环境只启用其中一个或几个。

扩展的排序有时非常重要,例如可能会影响它们在用户界面中的显示顺序。 当通过配置将扩展从禁用切换为启用时,会重置扩展的排序,将其推到列表末尾。 如果扩展的排序很重要,一般建议将其默认为禁用,允许根据配置中启用扩展的顺序来决定它们在应用程序中的排序。

配置和配置模式

每个扩展都可以定义一个配置模式,描述其接受的配置。 该模式用于验证集成者提供的配置,也可用于填写默认配置值。 配置本身由集成者提供,以便自定义扩展。 不可能为扩展提供默认配置,而必须通过配置模式中的默认值来实现。 这使得配置逻辑更加简单,同一扩展的多个配置可以完全相互替代,而不是合并。

###工厂

扩展工厂是扩展本身的实现。 它是一个函数,提供扩展接收到的任何输入和配置,并必须产生它所定义的输出。 当应用程序实例启动时,它将调用应用程序中每个扩展的工厂函数,从叶节点开始,一直到应用程序扩展树的根节点。 只有活动扩展才会调用工厂,活动扩展是指未禁用且有活动父节点的扩展。

扩展工厂应该精简,不做任何繁重的工作或异步工作,因为它们是在应用程序初始化过程中被调用的。 例如,如果您需要进行昂贵的计算来生成输出,那么最好输出一个回调来代替计算。 这允许父扩展将计算推迟到以后进行,从而避免阻塞应用程序的启动。

创建扩展名

使用createExtension@backstage/frontend-plugin-api您至少需要提供一个 ID、连接点、输出定义和工厂函数。 下面的示例显示了创建最小扩展的过程:

const extension = createExtension({
name: 'my-extension',
// This is the attachment point, `id` is the ID of the parent extension,
// while `input` is the name of the input to attach to.
attachTo: { id: 'my-parent', input: 'content' },
// The output map defines the outputs of the extension. The object keys
// are only used internally to map the outputs of the factory and do
// not need to match the keys of the input.
output: {
element: coreExtensionData.reactElement,
},
// This factory is called to instantiate the extensions and produce its output.
factory() {
return {
element: <div>Hello World</div>,
};
},
});

请注意,虽然createExtension相反,核心 API 和插件都导出了许多扩展创建函数,这使得为更多特定用途创建扩展变得更加容易。

... TODO ...

扩展数据

扩展之间的通信是单向的,即子扩展通过附加点与其父扩展进行通信。 子扩展输出数据,然后将这些数据作为输入传递给父扩展。 这些数据称为扩展数据,其中每条数据的形状由扩展数据引用描述。 这些引用与扩展本身分开创建,可在多个不同类型的扩展之间共享。 每个引用由 ID 和数据需要符合的 TypeScript 类型组成,代表扩展之间可共享的一种数据类型。

扩展数据参考

要创建一个新的扩展数据引用来表示一种共享扩展数据类型,可以使用createExtensionDataRef例如,在定义新引用时,您需要提供 ID 和 TypeScript 类型:

export const reactElementExtensionDataRef =
createExtensionDataRef<React.JSX.Element>('my-plugin.reactElement');

ExtensionDataRef这将强制对扩展工厂的返回值进行键入:

const extension = createExtension({
// ...
output: {
element: reactElementExtensionDataRef,
},
factory() {
return {
element: <div>Hello World</div>,
};
},
});

扩展数据唯一性

请注意,在本例中,输出映射中使用的键是element在被其他扩展程序使用时,数据的实际标识符是引用的 ID,在本例中为核心.反应元素这意味着不能为同一扩展数据引用输出多个不同的值,因为它们会相互冲突。 这反过来又使过于通用的扩展数据引用成为一个坏主意,例如通用的 "字符串 "类型。 相反,应为每种要共享的数据类型创建单独的引用。

const extension = createExtension({
// ...
output: {
// ❌ Bad example - outputting values of same type
element1: reactElementExtensionDataRef,
element2: reactElementExtensionDataRef,
},
factory() {
return {
element1: <div>Hello World</div>,
element2: <div>Hello World</div>,
};
},
});

核心扩展数据

我们提供默认coreExtensionData提供常用的ExtensionDataRef例如React.JSX.ElementRouteRef它们可以在创建自己的扩展时使用。 例如,我们上面定义的 React 元素扩展数据已经作为coreExtensionData.reactElement.

可选扩展数据

默认情况下,所有扩展数据都是必需的,这意味着扩展工厂必须为每个输出提供一个值。.optional()方法时,工厂函数可选择返回一个值作为其输出的一部分。 当调用.optional()方法创建扩展数据引用的新副本时,不会更改现有引用。

const extension = createExtension({
// ...
output: {
element: coreExtensionData.reactElement.optional(),
},
factory() {
return {
element:
Math.random() < 0.5 ? <img src="./assets/logo.png" /> : undefined,
};
},
});

扩展输入

扩展数据可以通过扩展输入传递给其他扩展。 与之前看到的输出类似,让我们创建一个带有扩展输入的扩展示例:

const navigationExtension = createExtension({
// ...
inputs: {
// [1]: Input
logo: createExtensionInput(
{
element: coreExtensionData.reactElement,
},
{ singleton: true, optional: true },
),
},
factory({ inputs }) {
return {
element: (
<nav>{inputs.logo.output?.element ?? <span>Backstage</span>}</nav>
),
};
},
// ...
});

输入(见[1]是我们使用createExtensionInput第一个参数是我们通过此输入接受的扩展数据集,其工作原理与output第二个参数是可选的,它允许我们对输入的扩展名进行限制。singleton: true选项时,一次只能附加一个扩展名,除非optional: true如果设置了该选项,则还需要有确切的附加扩展名。

那么,我们现在如何将输出附加到父扩展的输入上呢? 如果我们考虑一下导航组件,如 Backstage 中的侧边栏,可能会有插件希望将其插件的链接附加到该导航组件上。 在这种情况下,插件只需要知道扩展的id和扩展名的名称input以连接延长线output返回的factory到指定的扩展名:

const navigationItemExtension = createExtension({
// ...
attachTo: { id: 'app/nav', input: 'items' },
factory() {
return {
element: <Link to="/home">Home</Link>,
};
},
});

const navigationExtension = createExtension({
// ...
// [2]: Extension `id` will be `app/nav` following the extension naming pattern
namespace: 'app',
name: 'nav',
output: {
element: coreExtensionData.reactElement,
},
inputs: {
items: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
factory({ inputs }) {
return {
element: (
<nav>
<ul>
{inputs.items.map(item => {
return <li>{item.output.element}</li>;
})}
</ul>
</nav>
),
};
},
// ...
});

在这种情况下,扩展输入items是一个数组,其中每个项都是附加到此id.

随着inputs不仅output会传递给扩展项,同时也会将node但是,我们不鼓励食用node如果我们要查看的是factory函数,我们可以访问node比如说

// ...
factory({ inputs }) {
return {
element: (
<nav>
<ul>
{inputs.items.map(({output, node}) => {
const _node: AppNode = node;
return <li>{output.element}</li>;
})}
</ul>
</nav>
),
};
},

扩展配置

随着app-config.yaml已经有了向插件或应用程序传递配置的选项,例如定义baseURL对于扩展来说,这个概念是有局限性的,因为扩展可以独立于插件,也可以多次启动。 因此,我们创建了一种可能性,可以通过配置对每个扩展进行单独配置。 扩展配置模式是使用库,除了 TypeScript 类型检查外,它还提供运行时验证和强制。 如果我们继续以navigationExtension现在希望它包含一个可配置的标题,我们可以像下面这样使它可用:

const navigationExtension = createExtension({
// ...
namespace: 'app',
name: 'nav',
// [3]: Extension `id` will be `app/nav` following the extension naming pattern
configSchema: createSchemaFromZod(z =>
z.object({
title: z.string().default('Sidebar Title'),
}),
),
factory({ config }) {
return {
element: (
<nav>
<span>{config.title}</span>
<ul>{/* ... */}</ul>
</nav>
),
};
},
// ...
});

现在要将标题文字从 "侧边栏标题 "改为 "Backstage",我们可以查看id的扩展名,并在app-config.yaml:

app:
# ...
extensions:
# ...
- app/nav:
config:
title: 'Backstage'

扩展创建者

通过使用createExtension(...)我们意识到,这样做的代价是必须为类似的构建模块重复模板代码。 扩展创建者在此发挥作用,覆盖Backstage中的常见构建模块,如页面使用createPageExtension,主题使用createThemeExtension或使用createNavItemExtension.

如果按照上面的示例,所有导航项目都有相似之处,比如它们都希望以相同的输入连接到相同的扩展名,并呈现相同的导航项目组件。createExtension可以针对此用例抽象为createNavItemExtension如果我们将扩展添加到应用程序中,它就会出现在正确的位置,看起来就像我们期望的导航项一样。

export const HomeNavIcon = createNavItemExtension({
routeRef: routeRefForTheHomePage,
title: 'Home',
icon: HomeIcon,
});

扩展类型

示例HomeNavIcon最终将在扩展输入items的扩展名的app/nav这就提出了一个问题idHomeNavIcon导航项的扩展创建者有一个定义的kind,按照惯例,它与自己的名称相匹配。 因此,在这个例子中createNavItemExtension将种类设置为nav-item.

id然后再用namespace,name&kind就像下面这样--其中namespace&name是可传递给扩展创建者的可选属性:

id: kind:namespace/name

有关扩展名命名的更多信息,请参阅命名模式文档.

###库中的扩展创建器

扩展创建者应从前端库软件包中导出(例如*-react)而不是插件包。

如果扩展只用于内部调整,那么将其放在插件包中是可以的。 但如果您希望其他开源插件使用它,或者您已经有了一个-react软件包,总是将扩展创建者放在-react包装

扩展边界

ExtensionBoundary用多个 React 上下文包装扩展,用于不同目的

悬念

扩展创建者渲染的所有 React 元素都应包裹在扩展边界中。Suspense它还允许懒加载整个扩展,类似于目前 Backstage 中插件的懒加载方式。

错误界限

与插件类似ErrorBoundary该扩展允许在组件内部出现未捕获错误时传递回退组件。 这样可以隔离错误,并防止插件的其他部分崩溃。

分析

分析信息通过AnalyticsContext从而得出extensionId&pluginId作为扩展内部触发的分析事件的上下文。 此外RouteTracker将捕捉可路由扩展的分析事件,以告知当导航到的路由是一个已收集的路由时,哪些扩展元数据会与导航事件相关联mountPoint.

ExtensionBoundary可以在扩展创建器中如下使用:

export function createSomeExtension<
TConfig extends {},
TInputs extends AnyExtensionInputMap,
>(options): ExtensionDefinition<TConfig> {
return createExtension({
// ...
factory({ config, inputs, node }) {
const ExtensionComponent = lazy(() =>
options
.loader({ config, inputs })
.then(element => ({ default: () => element })),
);

return {
path: config.path,
routeRef: options.routeRef,
element: (
<ExtensionBoundary node={node} routable>
<ExtensionComponent />
</ExtensionBoundary>
),
};
},
});
}