Skip to main content

前端路由

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

简介

每个 Backstage 插件都是一个孤立的功能片段,通常不会与其他插件直接通信。 为了实现这一点,前台系统中有许多部分为跨插件通信提供了一层间接层,路由系统就是其中之一。

通过Backstage路由系统,可以实现跨插件边界的导航,每个插件都不需要知道其他插件在路由层次结构中的具体路径或位置,甚至不需要知道自己的路径或位置。 这是通过路由引用的概念实现的,路由引用是不透明的参考值,可以共享并用于创建到应用程序不同部分的具体链接。 路由引用路径可以在插件级别(由插件开发人员)和应用程序级别(由集成商)进行配置。 插件开发人员可以为其插件中的任何页面内容创建路由引用,并希望可以链接到或链接自这些页面内容。

路线参考

插件开发人员创建一个RouteRef在Backstage的路由系统中暴露路径。 你将在下文中看到如何以编程方式定义路由,但在深入研究代码之前,让我们先解释一下如何在应用程序级别配置路由。 尽管插件开发者会为其插件提供的路由选择默认的路由路径,但路径是可配置的,因此应用程序集成者可以随时为路由设置自定义路径(更多信息将在下文中介绍)。

路由引用有三种类型:常规路由、子路由和外部路由,我们将介绍每种路由引用的概念和代码定义。

创建路由参考

路由引用也称为 "绝对 "或 "常规 "路由,创建方法如下:

plugins/catalog/src/routes.ts
import { createRouteRef } from '@backstage/frontend-plugin-api';

// Creates a route reference, which is not yet associated with any plugin page
export const indexRouteRef = createRouteRef();

需要注意的是,通常情况下,路由引用本身需要在与创建插件实例的文件不同的文件中创建,例如一个顶级的routes.ts这是为了避免在使用同一插件其他部分的路由引用时出现循环导入。

Route refs 本身没有任何行为。 它们是一个不透明的值,代表应用程序中的路由目标,在运行时与特定路径绑定。 它们的作用是提供一定程度的间接性,帮助将不同的页面连接起来,否则这些页面就不知道如何相互路由。

为插件提供路由引用

上一节的代码片段没有指明路由属于哪个插件,因此必须在创建任何路由扩展(如页面扩展)时使用:

plugins/catalog/src/plugin.tsx
import React from 'react';
import {
createPlugin,
createPageExtension,
} from '@backstage/frontend-plugin-api';
import { indexRouteRef } from './routes';

const catalogIndexPage = createPageExtension({
// The `name` option is omitted because this is an index page
defaultPath: '/entities',
routeRef: indexRouteRef,
loader: () => import('./components').then(m => <m.IndexPage />),
});

export default createPlugin({
id: 'catalog',
routes: {
index: indexRouteRef,
},
extensions: [catalogIndexPage],
});

在上面的例子中,我们将indexRouteRefcatalogIndexPage因此,在应用程序中安装该插件后,索引页面将与新创建的RouteRef这样就可以使用路径参考来导航页面扩展。

我们为什么要配置routes选项,因为路由已经传递给了扩展。 我们这样做是为了让其他插件能够路由到我们的页面,这在结合航线节。

使用路径参数定义引用

路由引用可选择接受params选项,这就要求路由路径中必须有列出的参数名。 下面是如何为需要有kind,namespacename参数,如以下路径/entities/:kind/:namespace/:name:

plugins/catalog/src/routes.ts
import { createRouteRef } from '@backstage/frontend-plugin-api';

export const detailsRouteRef = createRouteRef({
// The parameters that must be included in the path of this route reference
params: ['kind', 'namespace', 'name'],
});

使用路由参考

路由引用可用于链接到同一插件中的页面或不同插件中的页面。 本节将介绍第一种情况。 如果您有兴趣链接到不同插件中的页面,请转到外部路由见下文。

假设我们要创建一个插件,用于渲染带有指向 "Foo "组件详情页链接的目录索引页。 以下是索引页的代码:

plugins/catalog/src/components/IndexPage.tsx
import React from 'react';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { detailsRouteRef } from '../routes';

export const IndexPage = () => {
const getDetailsPath = useRouteRef(detailsRouteRef);
return (
<div>
<h1>Index Page</h1>
<a
href={getDetailsPath({
kind: 'component',
namespace: 'default',
name: 'foo',
})}
>
See "Foo" details
</a>
</div>
);
};

我们使用useRouteRef钩子来创建一个链接生成器函数,返回详细信息页面的路径。 然后,我们调用链接生成器,向其传递一个包含种类、命名空间和名称的对象。 这些参数用于构建指向 "Foo "详细信息页面的具体路径。

让我们看看详情页如何从 URL 获取参数:

plugins/catalog/src/components/DetailsPage.tsx
import React from 'react';
import { useRouteRefParams } from '@backstage/frontend-plugin-api';
import { detailsRouteRef } from '../routes';

export const DetailsPage = () => {
const params = useRouteRefParams(detailsRouteRef);
return (
<div>
<h1>Details Page</h1>
<ul>
<li>Kind: {params.kind}</li>
<li>Namespace: {params.namespace}</li>
<li>Name: {params.name}</li>
</ul>
</div>
);
};

在上面的代码中,我们使用useRouteRefParams参数对象包含三个值:种类、命名空间和名称。 我们可以显示这些值或使用它们调用 API。

由于我们链接的是同一个软件包的页面,所以直接使用路由 ref。 不过,在下面的章节中,你将看到如何链接到不同插件的页面。

外部路由参考

外部路由用于链接到外部插件的页面。 在本节示例中,假设我们要从目录实体列表页面链接到 Scaffolder 创建组件页面。

我们不想直接引用 Scaffolder 插件,因为这样会产生不必要的依赖关系。 这样也无法灵活地让应用程序将插件绑定在一起,而是由插件本身决定链接。 为了解决这个问题,我们使用一个ExternalRouteRef与普通路由引用一样,它们可以传递给useRouteRef来创建具体的 URL,但这些 URL 不能在页面扩展中使用,而必须通过应用程序中的路由绑定与目标路由相关联。

我们创建一个新的RouteRef在 Scaffolder 插件内,使用一个中性的名称来描述它在插件中的角色,而不是它可能链接到的特定插件页面,让应用程序决定最终目标。 例如,如果目录实体列表页面希望链接页眉中的 Scaffolder 创建组件页面,它可以声明一个ExternalRouteRef与此类似:

plugins/catalog/src/routes.ts
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';

export const createComponentExternalRouteRef = createExternalRouteRef();

外部路由的使用方式也与常规路由类似:

plugins/catalog/src/components/IndexPage.tsx
import React from 'react';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { createComponentExternalRouteRef } from '../routes';

export const IndexPage = () => {
const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef);
return (
<div>
<h1>Index Page</h1>
<a href={getCreateComponentPath()}>Create Component</a>
</div>
);
};

鉴于上述绑定,使用useRouteRef(createComponentExternalRouteRef)会让我们创建一个指向 Scaffolder 创建组件页面所安装路径的链接。 请注意,Catalog 插件和 Scaffolder 之间没有直接的依赖关系,也就是说,我们没有导入createComponentExternalRouteRef从 Scaffolder 软件包中获取。

现在只剩下通过插件提供页面和外部路径了:

plugins/catalog/src/plugin.tsx
import React from 'react';
import {
createPlugin,
createPageExtension,
useRouteRef,
} from '@backstage/frontend-plugin-api';
import { indexRouteRef, createComponentExternalRouteRef } from './routes';

const catalogIndexPage = createPageExtension({
defaultPath: '/entities',
routeRef: indexRouteRef,
loader: () => import('./components').then(m => <m.IndexPage />),
});

export default createPlugin({
id: 'catalog',
routes: {
index: indexRouteRef,
},
externalRoutes: {
createComponent: createComponentExternalRouteRef,
},
extensions: [catalogIndexPage],
});

外部路由也可以有参数。 例如,如果要从 Scaffolder 链接到实体的详细信息页面,就需要创建一个外部路由,接收与目录详细信息页面所期望的相同的参数:

plugins/scaffolder/src/routes.ts
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';

export const entityDetailsExternalRouteRef = createExternalRouteRef({
params: ['kind', 'namespace', 'name'],
});

现在,让我们继续配置应用程序,以解析这些外部路由,从而使 Scaffolder 链接到目录实体页面,而目录链接到 Scaffolder 页面。

绑定外部路由引用

外部路由的关联由应用程序控制。ExternalRouteRef应绑定到一个实际的RouteRef绑定过程在应用程序启动时进行一次,然后在应用程序的整个生命周期内使用,以帮助解决具体的路由路径问题。

使用上面的 "目录实体列表 "页面到 "Scaffolder 创建组件 "页面的示例,我们可以在应用程序配置文件中执行类似的操作:

app-config.yaml
app:
routes:
bindings:
# point to the Scaffolder create component page when the Catalog create component ref is used
catalog.createComponent: scaffolder.index
# point to the Catalog details page when the Scaffolder component details ref is used
scaffolder.componentDetails: catalog.details

我们还可以用代码将其表达为以下选项createApp当然,您只需使用这两种方法中的一种:

packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-app-api';
import catalog from '@backstage/plugin-catalog';
import scaffolder from '@backstage/plugin-scaffolder';

const app = createApp({
bindRoutes({ bind }) {
bind(catalog.externalRoutes, {
createComponent: scaffolder.routes.createComponent,
});
bind(scaffolder.externalRoutes, {
componentDetails: catalog.routes.details,
});
},
});

export default app.createRoot();

请注意,我们并没有导入和使用RouteRef这提供了更好的命名间隔和路由的可发现性,并减少了每个插件包单独导出的数量。

另一点需要注意的是,路由中的这种间接性对于需要灵活集成的开源插件特别有用。 对于在内部为自己的 Backstage 应用程序构建的插件,可以选择直接导入,甚至直接使用具体的路由路径字符串。 不过,即使在内部插件中使用完整的路由系统也有一些好处:它可以帮助你构建路由结构,而且正如你将进一步看到的那样,它还可以帮助你管理路由参数。

可选外部路由参考

可以定义一个ExternalRouteRef为可选项,因此无需在应用程序中绑定。

plugins/catalog/src/routes.ts
import { createExternalRouteRef } from '@backstage/frontend-plugin-api';

export const createComponentExternalRouteRef = createExternalRouteRef({
optional: true,
});

呼叫时useRouteRef的返回签名改为RouteFunc | undefined返回的值可用于决定是否要显示某个链接或采取某个行动:

plugins/catalog/src/components/IndexPage.tsx
import React from 'react';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { createComponentExternalRouteRef } from '../routes';

export const IndexPage = () => {
const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef);
return (
<div>
<h1>Index Page</h1>
{/* Rendering the link only if the getCreateComponentPath is defined */}
{getCreateComponentPath && (
<a href={getCreateComponentPath()}>Create Component</a>
)}
</div>
);
};

子路由参考

可以创建的最后一种路由 ref 是SubRouteRef可用于创建路由 ref,其固定路径相对于绝对路径RouteRef如果你有一个内部安装在页面扩展组件子路由上的页面,而你又希望其他插件能够路由到该页面,那么它们就非常有用。 它们也是处理插件内部路由的有用工具。

例如

import {
createRouteRef,
createSubRouteRef,
} from '@backstage/frontend-plugin-api';

export const indexRouteRef = createRouteRef();
export const detailsSubRouteRef = createSubRouteRef({
parent: indexRouteRef,
path: '/details',
});

创建子路由与创建常规路由或外部路由有很大不同,因为子路由与常规路由相关联,而且必须指定子路由路径。 如果子路由有参数,路径字符串必须包括参数:

// Omitting rest of the previous example file
export const detailsSubRouteRef = createSubRouteRef({
parent: indexRouteRef,
path: '/:name/:namespace/:kind',
});

在页面扩展中使用子路由就是这么简单:

plugins/catalog/src/components/IndexPage.ts
import React from 'react';
import { Routes, Route, useLocation } from 'react-router-dom';
import { useRouteRef } from '@backstage/frontend-plugin-api';
import { indexRouteRef, detailsSubRouteRef } from '../routes';
import { DetailsPage } from './DetailsPage';

export const IndexPage = () => {
const { pathname } = useLocation();
const getIndexPath = useRouteRef(indexRouteRef);
const getDetailsPath = useRouteRef(detailsSubRouteRef);
return (
<div>
<h1>Index Page</h1>
{/* Linking to the details sub route */}
{pathname === getIndexPath() ? (
<a
{/* Setting the details sub route params */}
href={getDetailsPath({
kind: 'component',
namespace: 'default',
name: 'foo',
})}
>
Show details
</a>
) : (
<a href={getIndexPath()}>Hide details</a>
)}
{/* Registering the details sub route */}
<Routes>
<Route path={detailsSubRouteRef.path} element={<DetailsPage />} />
</Routes>
</div>
);
};

这就是获取子路由 URL 参数的方法:

plugins/catalog/src/components/DetailsPage.ts
import React from 'react';
import { useParams } from 'react-router-dom';

export const DetailsPage = () => {
const params = useParams();
return (
<div>
<h1>Details Sub Page</h1>
<ul>
<li>Kind: {params.kind}</li>
<li>Namespace: {params.namespace}</li>
<li>Name: {params.name}</li>
</ul>
</div>
);
};

最后,看看插件如何提供子路线:

plugins/catalog/src/plugin.ts
import React from 'react';
import {
createPlugin,
createPageExtension,
} from '@backstage/frontend-plugin-api';
import { indexRouteRef, detailsSubRouteRef } from './routes';

const catalogIndexPage = createPageExtension({
defaultPath: '/entities',
routeRef: indexRouteRef,
loader: () => import('./components').then(m => <m.IndexPage />),
});

export default createPlugin({
id: 'catalog',
routes: {
index: indexRouteRef,
details: detailsSubRouteRef,
},
extensions: [catalogIndexPage],
});