Backstage 旧版前端系统搜索插件实战:SearchApi 定制、索引字段扩展与结果渲染扩展指南
本文面向仍在使用 Backstage 旧版前端系统(legacy frontend system,通过
createApp({ apis: [...] })方式组织应用)的开发者。旧版前端系统是早期 Backstage 应用(包括大量存量项目)的默认形态;如果你的应用已经迁移到新版前端系统,请改读 Search How-To guides(新版前端系统)。本文将围绕搜索插件的四个高频自定义场景展开:实现自己的SearchApi、定制 Catalog/TechDocs 搜索索引字段、定制搜索结果高亮样式、以及通过扩展(Extensions)渲染搜索结果。
导读
搜索是 Backstage 开发者门户中用户触达频率最高的能力之一。官方搜索插件(@backstage/plugin-search)与搜索 React 插件(@backstage/plugin-search-react)默认提供了开箱即用的搜索体验,但在真实落地中你几乎总会遇到四类定制需求:对接自研搜索后端、控制进入索引的字段、让命中词高亮更醒目、让不同来源的结果以不同组件呈现。本文以旧版前端系统为背景,逐一给出可复制到 packages/app 与 packages/backend 的完整方案,并结合仓库源码解释每个配置项背后的实现原理。
一、实现你自己的 SearchApi
搜索插件默认实现了一个核心 API:SearchApi,它负责与 search-backend 通信、发起查询并返回结果集。官方默认实现为 SearchClient,其完整源码位于 plugins/search/src/apis.ts,核心逻辑非常简洁:通过 discoveryApi.getBaseUrl('search') 拼接后端地址,把 SearchQuery 序列化为查询字符串后请求 /query 接口,非 2xx 响应抛出 ResponseError,成功则直接 response.json() 返回 SearchResultSet。
当你需要对接自己的搜索后端、或对查询行为做深度定制时,可以完全替换这个实现。整个过程分两步。
第 1 步:实现 SearchApi 接口
SearchApi 接口定义在 plugins/search-react/src/api.ts,契约非常轻量,只有一个方法:
export interface SearchApi {
query(
query: SearchQuery,
options?: { signal?: AbortSignal },
): Promise<SearchResultSet>;
}
SearchQuery 与 SearchResultSet 均来自 @backstage/plugin-search-common,前者包含 term、types、filters 等查询参数,后者包含 results(SearchResult[])与 nextPageCursor 等分页信息。据此实现自己的客户端:
export class SearchClient implements SearchApi {
// your implementation
}
如果你的实现需要 discovery(服务发现)或 fetch(HTTP 请求)能力,可以参考默认实现 SearchClient 的构造方式——它接收 { discoveryApi, fetchApi } 两个依赖,其中 fetchApi 会自动携带鉴权头,这对访问受保护的 search-backend 至关重要。另外,仓库在 plugins/search-react/src/api.ts 还提供了一个 MockSearchApi,可用于测试与 Storybook 场景,值得作为你自定义实现的最小参照。
第 2 步:通过 ApiFactories 覆盖 searchApiRef
searchApiRef 是搜索 API 的引用标识(定义于 plugins/search-react/src/api.ts,id 为 plugin.search.queryservice)。在旧版前端系统中,所有 API 的提供方都由 createApp 的 apis 数组统一管理,因此在应用的 App.tsx 中使用 createApiFactory 注册你的实现即可完成覆盖:
const app = createApp({
apis: [
// SearchApi
createApiFactory({
api: searchApiRef,
deps: { discovery: discoveryApiRef },
factory({ discovery }) {
return new SearchClient({ discoveryApi: discovery });
},
}),
],
});
createApiFactory 的 deps 声明了工厂依赖(这里注入 discoveryApiRef),factory 负责用这些依赖构造出实现实例。注册之后,整个应用中通过 useApi(searchApiRef) 获取到的就都是你的自定义实现了。关于 App API 的更多机制(API Ref、工厂、依赖注入),可参阅 前端系统 Utility APIs 文档。从源码调用链看,SearchResult 组件正是通过 useApi(searchApiRef) 获取该实现并发起查询的(见 plugins/search-react/src/components/SearchResult/SearchResult.tsx),因此只要替换 searchApiRef 的工厂,所有搜索结果组件会自动切换到你的实现。
二、定制 Software Catalog 或 TechDocs 索引字段
默认情况下,Catalog 与 TechDocs 的文档收集器(Collator)会按内置规则把实体/文档转换为索引条目。你往往需要控制哪些数据进入索引——例如为某个特定 kind 定制输出,或给索引补充额外字段。旧版前端系统的后端部分与此无关,因此本节方案在新旧两套前端系统下同样适用。
实现方式是向收集器工厂传入 transformer 回调:DefaultCatalogCollatorFactory 支持 entityTransformer,DefaultTechDocsCollatorFactory 则额外支持 documentTransformer。你可以简单修改默认行为,也可以写出一个全新的文档对象(但仍需遵循索引文档的基本结构)。
注意约束:
authorization和location字段无法通过entityTransformer修改;location只能通过locationTemplate调整(见下文源码印证)。documentTransformer同样不负责改写location。
以下代码位于后端搜索模块 packages/backend/src/plugins/search.ts:
const catalogEntityTransformer: CatalogCollatorEntityTransformer = (
entity: Entity,
) => {
if (entity.kind === 'SomeKind') {
return {
// customize here output for 'SomeKind' kind
};
}
return {
// and customize default output
...defaultCatalogCollatorEntityTransformer(entity),
text: 'my super cool text',
};
};
indexBuilder.addCollator({
collator: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
/* highlight-add-next-line */
entityTransformer: catalogEntityTransformer,
}),
});
const techDocsEntityTransformer: TechDocsCollatorEntityTransformer = (
entity: Entity,
) => {
return {
// add more fields to the index
tags: entity.metadata.tags,
};
};
const techDocsDocumentTransformer: TechDocsCollatorDocumentTransformer = (
doc: MkSearchIndexDoc,
) => {
return {
// add more fields to the index
bost: doc.boost,
};
};
indexBuilder.addCollator({
collator: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
/* highlight-add-next-line */
entityTransformer: techDocsEntityTransformer,
/* highlight-add-next-line */
documentTransformer: techDocsDocumentTransformer,
}),
});
要点说明:
CatalogCollatorEntityTransformer类型定义于 plugins/search-backend-module-catalog/src/collators/CatalogCollatorEntityTransformer.ts,接收一个 CatalogEntity,返回索引文档对象。defaultCatalogCollatorEntityTransformer是系统提供的默认转换函数,通过展开运算符...继承默认字段,再覆盖或追加自定义字段(如上例把text替换为自定义文案)。entityTransformer与documentTransformer可以同时使用:前者处理实体(Entity),后者处理 TechDocs 搜索索引文档(MkSearchIndexDoc),分工明确。- 关于
locationTemplate的约束,在 DefaultCatalogCollatorFactory 中可以得到印证:该工厂的fromConfig选项包含locationTemplate,用于控制索引文档location字段的生成规则(例如配置collators.catalog.locationTemplate: '/software/:name'),测试用例 DefaultCatalogCollatorFactory.test.ts 中也对自定义locationTemplate的行为做了专门验证。因此,如果你需要修改结果跳转地址,请通过locationTemplate实现,而不是在 transformer 里改location。
三、定制搜索结果高亮的样式
默认情况下,搜索结果中命中词的“高亮”效果来自浏览器对 <mark> HTML 标签的原生样式。如果想自定义高亮外观,可以按照 自定义应用 UI 指南 创建主题覆盖。
高亮的底层实现
在深入主题配置前,先理解高亮是如何渲染的。DefaultResultListItem(默认结果项组件)在渲染标题与摘要时,会使用 HighlightedSearchResultText 组件处理带高亮标记的文本(见 DefaultResultListItem.tsx)。该组件实现位于 HighlightedSearchResultText.tsx:它用 preTag/postTag(高亮前后标记)把文本切分成片段,命中部分包进 <mark> 标签,并应用类名 highlight。这段样式通过 makeStyles 注册,样式名称(style name)为 BackstageHighlightedSearchResultText,这正是主题覆盖的挂载点。
通过统一主题(MUI V4+V5)覆盖
使用新版 MUI V4+V5 统一主题方案,下面配置会让命中词变为加粗 + 下划线(同时去掉 <mark> 默认的背景色与文字色)。主题文件位于 packages/app/src/theme/theme.ts:
import {
createBaseThemeOptions,
createUnifiedTheme,
palettes,
UnifiedTheme,
} from '@backstage/theme';
export const myLightTheme: UnifiedTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
}),
defaultPageTheme: 'home',
components: {
/** @ts-ignore This is temporarily necessary until MUI V5 transition is completed. */
BackstageHighlightedSearchResultText: {
styleOverrides: {
highlight: {
color: 'inherit',
backgroundColor: 'inherit',
fontWeight: 'bold',
textDecoration: 'underline',
},
},
},
},
});
然后在应用入口 packages/app/src/App.tsx 中把主题注册进 createApp:
const app : BackstageApp = createApp({
...
themes: [{
id: 'my-light-theme',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (<UnifiedThemeProvider theme={myLightTheme} children={children } />)
}]
});
如果还需要暗色主题,同样再提供一个 variant: 'dark' 的主题条目即可。主题切换后,所有使用 HighlightedSearchResultText 的结果项(包括默认结果项与基于默认项派生的扩展)都会立即应用新样式。
四、使用扩展(Extensions)渲染搜索结果
扩展机制让你可以完全掌控“每一条搜索结果用什么组件渲染”。你可以提供自己的搜索结果项扩展,也可以直接复用其他插件包提供的现成扩展。本节所有示例均面向旧版前端系统。
关键约定:必须使用
plugin.provide()函数把搜索项渲染器注册为扩展。与在标准 MUI Table 或类似组件中直接传入渲染函数不同,你不能简单地把渲染函数塞给<SearchResult />组件。
4.1 在你的插件包中提供扩展
在插件源码 plugins/your-plugin/src/plugin.ts 中,使用 createSearchResultListItemExtension 创建扩展并交给 plugin.provide():
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
component 采用动态 import,实现按需加载。如果你的列表项接收 props,可以用 SearchResultListItemExtensionProps 泛型扩展自己的 props 类型:
export const YourSearchResultListItemExtension: (
props: SearchResultListItemExtensionProps<YourSearchResultListItemProps>,
) => JSX.Element | null = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
}),
);
还可以定义一个 predicate(谓词函数):它接收一条 SearchResult,返回布尔值,决定该扩展是否用于渲染这条结果:
import { createPlugin } from '@backstage/core-plugin-api';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react';
const plugin = createPlugin({ id: 'YOUR_PLUGIN_ID' });
export const YourSearchResultListItemExtension = plugin.provide(
createSearchResultListItemExtension({
name: 'YourSearchResultListItem',
component: () =>
import('./components').then(m => m.YourSearchResultListItem),
// Only results matching your type will be rendered by this extension
predicate: result => result.type === 'YOUR_RESULT_TYPE',
}),
);
记得通过插件的入口文件 plugins/your-plugin/src/index.ts 导出新扩展,使其对应用可见:
export { YourSearchResultListItem } from './plugin.ts';
源码印证:createSearchResultListItemExtension 的实现位于 plugins/search-react/src/extensions.tsx。它默认的 predicate 是 () => true(即匹配所有结果),并把谓词以组件数据键 search.results.list.items.extensions.v1 挂到扩展元素上;渲染时 useSearchResultListItemExtensions 会遍历所有扩展元素,按顺序找到第一个谓词命中该结果的扩展来渲染(见同文件 findSearchResultListItemExtensionElement)。因此,谓词为真的匹配顺序决定了最终渲染器。
4.2 在 SearchPage 中替换搜索结果项渲染器
扩展暴露出来后,你就可以在自定义的 SearchPage 中覆盖默认渲染器,并告诉 <SearchResult> 组件该用哪些渲染器。注意:渲染器的顺序很重要! 第一个通过谓词匹配的渲染器会被采用。
以下示例位于 packages/app/src/components/searchPage.tsx(为突出重点,省略了过滤器与分页等组件):
import { Grid, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import {
Page,
Header,
Content,
DocsIcon,
CatalogIcon,
} from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
// This example omits other components, like filter and pagination
const SearchPage = () => (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper>
<SearchBar />
</Paper>
</Grid>
<Grid item xs={12}>
<SearchResult>
<YourSearchResultListItem />
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
export const searchPage = <SearchPage />;
重要提示:一个默认结果项扩展(即没有定义
predicate的扩展)应放在最后一个子元素位置,这样它只会在没有其他扩展匹配该结果时兜底使用。如果指定了非默认扩展,则会使用DefaultResultListItem组件兜底。这一兜底逻辑在 SearchResult.tsx 所调用的useSearchResultListItemExtensions中实现:没有扩展命中时,渲染器回退为内建DefaultResultListItem。
4.3 在 SidebarSearchModal 中定制
如果你使用的是侧边栏搜索弹窗 SidebarSearchModal,可以通过它的 resultItemComponents 属性直接传入要使用的渲染扩展,示例位于 packages/app/src/components/Root/Root.tsx:
import { SidebarSearchModal } from '@backstage/plugin-search';
...
export const Root = ({ children }: PropsWithChildren<{}>) => {
const styles = useStyles();
return <SidebarPage>
<Sidebar>
...
<SidebarSearchModal resultItemComponents={[
/* Provide a custom Extension search item renderer */
<CustomSearchResultListItem icon={<CatalogIcon />} />,
/* Provide an existing search item renderer */
<TechDocsSearchResultListItem icon={<DocsIcon />} />
]} />
...
</Sidebar>
{children}
</SidebarPage>;
};
4.4 在自定义 SearchModal 中定制
如果你已经完整自定义了自己的 SearchModal,同样可以在 SearchResult 中组合扩展。以下示例位于 packages/app/src/components/searchModal.tsx:
import { DialogContent, DialogTitle, Paper } from '@material-ui/core';
import BuildIcon from '@material-ui/icons/Build';
import { DocsIcon, CatalogIcon } from '@backstage/core-components';
import { SearchBar, SearchResult } from '@backstage/plugin-search-react';
// Your search result item extension
import { YourSearchResultListItem } from '@backstage/your-plugin';
// Extensions provided by other plugin developers
import { ToolSearchResultListItem } from '@backstage/plugin-explore';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { CatalogSearchResultListItem } from '@internal/plugin-catalog-customized';
export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => (
<>
<DialogTitle>
<Paper>
<SearchBar />
</Paper>
</DialogTitle>
<DialogContent>
<SearchResult onClick={toggleModal}>
<CatalogSearchResultListItem icon={<CatalogIcon />} />
<TechDocsSearchResultListItem icon={<DocsIcon />} />
<ToolSearchResultListItem icon={<BuildIcon />} />
{/* As a "default" extension, it does not define a predicate function,
so it must be the last child to render results that do not match the above extensions */}
<YourSearchResultListItem />
</SearchResult>
</DialogContent>
</>
);
4.5 更细粒度的布局组件
除 SearchResult 外,还有更专用的搜索结果布局组件同样支持结果项扩展:
SearchResultList:以列表形式渲染一组结果;SearchResultGroup:把结果按类型/分组渲染,适合“按来源分组展示”的场景。
这两个组件的扩展用法可参考其 Storybook 文档(with-result-item-extensions 场景)。它们的实现位于 plugins/search-react/src/components/SearchResultList 与 plugins/search-react/src/components/SearchResultGroup,同样复用 SearchResultListItemExtensions 完成谓词匹配与渲染分发。
五、小结与迁移提示
本文覆盖了旧版前端系统下搜索插件的四类核心定制:
| 定制诉求 | 关键 API / 配置 | 主要位置 |
|---|---|---|
| 替换搜索数据源 | SearchApi 接口 + createApiFactory 覆盖 searchApiRef |
packages/app/src/App.tsx |
| 控制索引字段 | entityTransformer / documentTransformer / locationTemplate |
packages/backend/src/plugins/search.ts |
| 定制命中词高亮 | 主题覆盖 BackstageHighlightedSearchResultText 的 highlight 样式 |
packages/app/src/theme/theme.ts |
| 定制结果项渲染 | createSearchResultListItemExtension + plugin.provide() + 谓词顺序 |
插件包与 SearchPage/SidebarSearchModal/自定义弹窗 |
几点通用提醒:
- 谓词顺序即渲染优先级:第一个命中结果的扩展胜出,默认扩展放最后兜底;
location字段只能通过locationTemplate修改,不要试图在 transformer 中改写它;- 如果你正在评估从旧版前端系统迁移到新版,注意新版中
SearchApi的覆盖方式改为createApiExtension(来自@backstage/frontend-plugin-api),结果项扩展改为SearchResultListItemBlueprint(来自@backstage/plugin-search-react/alpha),具体差异见 新版前端系统的 How-To 指南 与 前端系统 Utility APIs 文档。本文涉及的后端 Collator 定制(第二节)在新旧两版中完全一致,无需改动。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python270
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java311
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java220
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript220
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python300