首页
/ Backstage 旧版前端系统搜索插件实战:SearchApi 定制、索引字段扩展与结果渲染扩展指南

Backstage 旧版前端系统搜索插件实战:SearchApi 定制、索引字段扩展与结果渲染扩展指南

2026-09-09 19:27:15作者:鲍丁臣Ursa

本文面向仍在使用 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/apppackages/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>;
}

SearchQuerySearchResultSet 均来自 @backstage/plugin-search-common,前者包含 termtypesfilters 等查询参数,后者包含 resultsSearchResult[])与 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 的提供方都由 createAppapis 数组统一管理,因此在应用的 App.tsx 中使用 createApiFactory 注册你的实现即可完成覆盖:

const app = createApp({
  apis: [
    // SearchApi
    createApiFactory({
      api: searchApiRef,
      deps: { discovery: discoveryApiRef },
      factory({ discovery }) {
        return new SearchClient({ discoveryApi: discovery });
      },
    }),
  ],
});

createApiFactorydeps 声明了工厂依赖(这里注入 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 支持 entityTransformerDefaultTechDocsCollatorFactory 则额外支持 documentTransformer。你可以简单修改默认行为,也可以写出一个全新的文档对象(但仍需遵循索引文档的基本结构)。

注意约束authorizationlocation 字段无法通过 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,接收一个 Catalog Entity,返回索引文档对象。
  • defaultCatalogCollatorEntityTransformer 是系统提供的默认转换函数,通过展开运算符 ... 继承默认字段,再覆盖或追加自定义字段(如上例把 text 替换为自定义文案)。
  • entityTransformerdocumentTransformer 可以同时使用:前者处理实体(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/SearchResultListplugins/search-react/src/components/SearchResultGroup,同样复用 SearchResultListItemExtensions 完成谓词匹配与渲染分发。

五、小结与迁移提示

本文覆盖了旧版前端系统下搜索插件的四类核心定制:

定制诉求 关键 API / 配置 主要位置
替换搜索数据源 SearchApi 接口 + createApiFactory 覆盖 searchApiRef packages/app/src/App.tsx
控制索引字段 entityTransformer / documentTransformer / locationTemplate packages/backend/src/plugins/search.ts
定制命中词高亮 主题覆盖 BackstageHighlightedSearchResultTexthighlight 样式 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 定制(第二节)在新旧两版中完全一致,无需改动。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
932
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.95 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23