Backstage 主题定制完全指南:从创建自定义 Theme 到替换 Logo 与图标
导读
本文围绕 Backstage 官方教程《Customize your App's theme》展开,系统地讲解如何在新前端系统(new frontend system)下为你的 Backstage 应用定制主题外观:包括基于内置亮色/暗色主题派生自定义主题、通过 ThemeBlueprint 把主题注册为前端扩展、定制排版(Typography)与自定义字体、覆盖 Backstage 与 Material UI 组件样式、替换 Logo,以及通过 IconBundleBlueprint 覆盖/新增应用图标。读完本文,你将掌握一套完整、可直接落地的 Backstage 品牌化(white-labeling)方案,并理解这些 API 背后的源码实现原理。
说明:本文针对默认使用新前端系统的新建 Backstage 应用。若你的应用仍在使用旧前端系统,请参考仓库中的旧版指南 customize-theme--old.md。
一、主题体系与核心概念
Backstage 自带一套默认主题,包含亮色(light)与暗色(dark)两种变体。这套主题由 @backstage/theme 包提供,该包同时导出了大量用于定制默认主题、甚至从零创建全新主题的工具函数。
在仓库中,内置主题的定义位于 packages/theme/src/unified/themes.ts,可以看到它们本质上就是 createUnifiedTheme 的直接调用:
export const themes = {
light: createUnifiedTheme({ palette: palettes.light }),
dark: createUnifiedTheme({ palette: palettes.dark }),
};
也就是说,官方内置的亮/暗主题并不是什么"魔法",而是 createUnifiedTheme 结合 palettes.light / palettes.dark 两个预设调色板的产物。理解这一点后,你就明白:自定义主题本质上就是换个调色板、换套字体、换组页面主题,再交给同一个工厂函数去生成。
UnifiedTheme:一份主题,同时支撑 MUI v4 与 v5
从源码 packages/theme/src/unified/UnifiedTheme.tsx 可以看出,createUnifiedTheme 返回的是一个 UnifiedThemeHolder 实例,它内部用 Map 同时保存了 Material UI v4 与 v5 两套主题对象:
export class UnifiedThemeHolder implements UnifiedTheme {
#themes = new Map<SupportedVersions, SupportedThemes>();
getTheme(version: SupportedVersions): SupportedThemes | undefined {
return this.#themes.get(version);
}
}
其核心实现(packages/theme/src/unified/UnifiedTheme.tsx)会:
- 调用
createBaseThemeOptions生成基础主题选项; - 将
defaultComponentThemes(MUI v5 默认组件主题)与用户传入的components合并; - 用 MUI v5 的
createTheme生成 v5 主题; - 通过
transformV5ComponentThemesToV4把 v5 组件主题转换回 v4,再配合 MUI v4 的createTheme生成 v4 主题。
这意味着:你在自定义主题里写的 components 覆盖,会自动同时作用于 MUI v4 与 v5 两套组件体系,这也是 Backstage 生态从 v4 向 v5 平滑过渡的基础设施。
二、创建自定义主题:从内置主题派生
@backstage/theme 包导出的 createUnifiedTheme 函数是创建新主题最便捷的入口。你可以用它覆盖默认主题中的基础参数,例如调色板与字体。
官方建议在 packages/app/src 下新建一个 theme 目录存放主题文件,保持工程结构清晰。
下面是一个基于内置亮色主题派生的自定义主题示例,文件位于 packages/app/src/theme/myTheme.ts:
import {
createBaseThemeOptions,
createUnifiedTheme,
palettes,
} from '@backstage/theme';
export const myTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
}),
fontFamily: 'Comic Sans MS',
defaultPageTheme: 'home',
});
几个关键点:
createBaseThemeOptions负责把palette、fontFamily、htmlFontSize、defaultPageTheme、pageTheme、typography整理成 MUI 可消费的主题选项;defaultPageTheme: 'home'指定默认页面主题(决定每个页面的头部渐变与背景形状),默认值就是'home'(见 packages/theme/src/base/createBaseThemeOptions.ts 中的DEFAULT_PAGE_THEME);- 若传入的
defaultPageTheme不在pageTheme中,createBaseThemeOptions会直接抛错(packages/theme/src/base/createBaseThemeOptions.ts)。
如果你不想基于内置主题派生,也可以从零构造一个满足 BackstageTheme 类型(@backstage/theme 导出)的完整主题对象,此时可参考 Material UI 官方的 Theming 文档了解底层机制。
三、安装自定义主题:ThemeBlueprint 扩展机制
在新前端系统中,主题是以扩展(extension)的形式安装的。具体流程是:用 @backstage/plugin-app-react 提供的 ThemeBlueprint 创建主题扩展 → 把扩展打包进一个前端模块(frontend module)→ 通过 createApp 传入应用。
首先安装所需依赖(在 Backstage 根目录执行):
yarn --cwd packages/app add @backstage/frontend-plugin-api @backstage/plugin-app-react
然后在 packages/app/src/App.tsx 中创建主题扩展并安装:
import { createApp } from '@backstage/frontend-defaults';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { ThemeBlueprint } from '@backstage/plugin-app-react';
import { UnifiedThemeProvider } from '@backstage/theme';
import LightIcon from '@material-ui/icons/WbSunny';
import { myTheme } from './theme/myTheme';
const myThemeExtension = ThemeBlueprint.make({
name: 'my-theme',
params: {
theme: {
id: 'my-theme',
title: 'My Custom Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={myTheme} children={children} />
),
},
},
});
const app = createApp({
features: [
createFrontendModule({
pluginId: 'app',
extensions: [myThemeExtension],
}),
],
});
export default app.createRoot();
从源码层面看,ThemeBlueprint 定义于 plugins/app-react/src/blueprints/ThemeBlueprint.ts,其关键特征包括:
kind: 'theme':扩展种类为theme;attachTo: { id: 'api:app/app-theme', input: 'themes' }:主题扩展会挂载到应用主题 API 的themes输入上,这正是它能够与内置亮/暗主题并列出现的原因;- 通过
themeDataRef输出一个AppTheme对象,包含id、title、variant、icon、Provider等字段。
Provider 使用 UnifiedThemeProvider(见 packages/theme/src/unified/UnifiedThemeProvider.tsx),它会根据当前应用运行的 MUI 版本(v4 或 v5)从 UnifiedTheme 中取出对应版本的主题对象提供给组件树。
禁用内置主题
你的自定义主题扩展会与内置亮/暗主题同时出现在主题切换器中。如果希望自定义主题替换默认主题,可以在 app-config.yaml 中禁用它们:
app:
extensions:
- theme:app/light: false
- theme:app/dark: false
注意这里的 theme:app/light、theme:app/dark 是内置扩展的完整 ID(kind 为 theme,插件为 app,扩展名为 light/dark),禁用后应用将只保留你注册的主题。
四、完整示例:一套完整的自定义调色板与页面主题
官方教程给出了一个相当完整的自定义主题示例,它同时覆盖了调色板(palette)、页面主题(pageTheme)与字体。文件位于 packages/app/src/theme/myTheme.ts:
import {
createBaseThemeOptions,
createUnifiedTheme,
genPageTheme,
palettes,
shapes,
} from '@backstage/theme';
export const myTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: {
...palettes.light,
primary: {
main: '#343b58',
},
secondary: {
main: '#565a6e',
},
error: {
main: '#8c4351',
},
warning: {
main: '#8f5e15',
},
info: {
main: '#34548a',
},
success: {
main: '#485e30',
},
background: {
default: '#d5d6db',
paper: '#d5d6db',
},
banner: {
info: '#34548a',
error: '#8c4351',
text: '#343b58',
link: '#565a6e',
},
errorBackground: '#8c4351',
warningBackground: '#8f5e15',
infoBackground: '#343b58',
navigation: {
background: '#343b58',
indicator: '#8f5e15',
color: '#d5d6db',
selectedColor: '#ffffff',
},
},
}),
defaultPageTheme: 'home',
fontFamily: 'Comic Sans MS',
/* below drives the header colors */
pageTheme: {
home: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
documentation: genPageTheme({
colors: ['#8c4351', '#343b58'],
shape: shapes.wave2,
}),
tool: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.round }),
service: genPageTheme({
colors: ['#8c4351', '#343b58'],
shape: shapes.wave,
}),
website: genPageTheme({
colors: ['#8c4351', '#343b58'],
shape: shapes.wave,
}),
library: genPageTheme({
colors: ['#8c4351', '#343b58'],
shape: shapes.wave,
}),
other: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
app: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
apis: genPageTheme({ colors: ['#8c4351', '#343b58'], shape: shapes.wave }),
},
});
这个示例非常有参考价值,它揭示了主题系统几个重要层次:
- 调色板(palette):
...palettes.light先展开内置亮色调色板,再逐项覆盖primary、secondary、error、warning、info、success、background、banner、navigation等语义色。内置调色板完整定义可查看 packages/theme/src/base/palettes.ts,其中除了上述字段,还包含status(ok/warning/error/running/pending/aborted)、bursts、border、textSubtle、link、gold等大量语义 token,均可按需覆盖。 - 页面主题(pageTheme):
genPageTheme({ colors, shape })用于生成每个页面类型(home、documentation、tool、service、website、library、other、app、apis、card)的头部背景。其实现位于 packages/theme/src/base/pageTheme.ts:colors会被拼成 90 度线性渐变,shape是预置的 SVG 形状(wave、wave2、round、square),二者合成backgroundImage,并默认使用白色前景字(可通过options.fontColor覆盖)。内置的形状定义与配色变体(colorVariants)也在该文件中,例如内置home页面主题使用teal配色与wave形状。 - 默认页面主题:
defaultPageTheme: 'home'决定所有未单独指定页面主题的页面使用哪一个主题,createBaseThemeOptions还会生成getPageTheme函数,让任何页面都能按themeId查找到对应主题(未命中时回退到默认页面主题,见 packages/theme/src/base/createBaseThemeOptions.ts)。
更完整、包含 Backstage 与 Material UI 组件覆盖的示例可以参考 Backstage demo 站点中的 Aperture 主题。
五、自定义排版(Typography)
createBaseThemeOptions 也支持传入完整的 typography 配置,用来定制各级标题与正文字体。
5.1 完整覆盖排版
下面的示例基于简化主题定制全部标题样式,文件位于 packages/app/src/theme/myTheme.ts:
import {
createBaseThemeOptions,
createUnifiedTheme,
palettes,
} from '@backstage/theme';
export const myTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
typography: {
htmlFontSize: 16,
fontFamily: 'Arial, sans-serif',
h1: {
fontSize: 54,
fontWeight: 700,
marginBottom: 10,
},
h2: {
fontSize: 40,
fontWeight: 700,
marginBottom: 8,
},
h3: {
fontSize: 32,
fontWeight: 700,
marginBottom: 6,
},
h4: {
fontWeight: 700,
fontSize: 28,
marginBottom: 6,
},
h5: {
fontWeight: 700,
fontSize: 24,
marginBottom: 4,
},
h6: {
fontWeight: 700,
fontSize: 20,
marginBottom: 2,
},
},
defaultPageTheme: 'home',
}),
});
可以看到,这里 h1~h6 的默认值与 defaultTypography(packages/theme/src/base/createBaseThemeOptions.ts)完全一致——也就是说,默认排版配置就是上面这套参数,其中 htmlFontSize 默认 16,默认字体族为 "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif。
5.2 只覆盖部分排版
如果只想覆盖某一级标题(例如只改 h1),可以先用 defaultTypography 展开默认值再局部覆盖,避免丢失其余标题的样式:
import {
createBaseThemeOptions,
createUnifiedTheme,
defaultTypography,
palettes,
} from '@backstage/theme';
export const myTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
typography: {
...defaultTypography,
htmlFontSize: 16,
fontFamily: 'Roboto, sans-serif',
h1: {
fontSize: 72,
fontWeight: 700,
marginBottom: 10,
},
},
defaultPageTheme: 'home',
}),
});
一个小细节:createBaseThemeOptions 内部会直接修改 defaultTypography 对象的 htmlFontSize 与 fontFamily(packages/theme/src/base/createBaseThemeOptions.ts),因此显式展开 ...defaultTypography 再覆盖,能保证你的修改精确生效、且不会遗漏默认字段。
六、自定义字体(Custom Fonts)
要使用自定义字体,首先需要把字体文件放到可被导入的位置。官方建议在应用 src 目录下创建 assets/fonts 目录存放字体文件。
然后按照 Material UI Typography 的 @font-face 语法声明字体,再通过 MuiCssBaseline 组件下的 styleOverrides 把字体挂载到 @font-face 数组中:
import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2';
const myCustomFont = {
fontFamily: 'My-Custom-Font',
fontStyle: 'normal',
fontDisplay: 'swap',
fontWeight: 300,
src: `
local('My-Custom-Font'),
url(${MyCustomFont}) format('woff2'),
`,
};
export const myTheme = createUnifiedTheme({
fontFamily: 'My-Custom-Font',
palette: palettes.light,
components: {
MuiCssBaseline: {
styleOverrides: {
'@font-face': [myCustomFont],
},
},
},
});
注意这里把 fontFamily 同时设在了主题顶层(作用于正文),并通过 components.MuiCssBaseline.styleOverrides['@font-face'] 注入字体定义——这也是 MUI v5 组件覆盖标准用法的一个典型示例。
多字体混用
如果希望正文与标题使用不同字体,可以这样组织:顶层 fontFamily 控制正文字体,typography 里再为各级标题单独指定 fontFamily:
import MyCustomFont from '../assets/fonts/My-Custom-Font.woff2';
import myAwesomeFont from '../assets/fonts/My-Awesome-Font.woff2';
const myCustomFont = {
fontFamily: 'My-Custom-Font',
fontStyle: 'normal',
fontDisplay: 'swap',
fontWeight: 300,
src: `
local('My-Custom-Font'),
url(${MyCustomFont}) format('woff2'),
`,
};
const myAwesomeFont = {
fontFamily: 'My-Awesome-Font',
fontStyle: 'normal',
fontDisplay: 'swap',
fontWeight: 300,
src: `
local('My-Awesome-Font'),
url(${myAwesomeFont}) format('woff2'),
`,
};
export const myTheme = createUnifiedTheme({
fontFamily: 'My-Custom-Font',
components: {
MuiCssBaseline: {
styleOverrides: {
'@font-face': [myCustomFont, myAwesomeFont],
},
},
},
...createBaseThemeOptions({
palette: palettes.light,
typography: {
...defaultTypography,
htmlFontSize: 16,
fontFamily: 'My-Custom-Font',
h1: {
fontSize: 72,
fontWeight: 700,
marginBottom: 10,
fontFamily: 'My-Awesome-Font',
},
},
defaultPageTheme: 'home',
}),
});
这里两个 @font-face 声明都被放进数组,正文使用 My-Custom-Font,h1 标题则使用 My-Awesome-Font。
七、覆盖 Backstage 与 Material UI 组件样式
自定义主题本质上是给组件的 CSS 规则提供不同的取值。以 Backstage 头部组件为例,其样式可能长这样:
const useStyles = makeStyles<BackstageTheme>(
theme => ({
header: {
padding: theme.spacing(3),
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
backgroundImage: theme.page.backgroundImage,
},
}),
{ name: 'BackstageHeader' },
);
这里 padding 取自 theme.spacing、backgroundImage 取自 theme.page.backgroundImage,因此通过自定义主题修改 spacing 或 pageTheme 就能影响这些属性。但 boxShadow 是硬编码值,不引用任何主题变量——仅靠主题无法改变它,也无法新增主题中不存在的 CSS 规则(例如 margin)。这类情况就需要创建组件覆盖(override)。
通过 createUnifiedTheme 的 components 字段即可实现:
import {
createBaseThemeOptions,
createUnifiedTheme,
palettes,
} from '@backstage/theme';
export const myTheme = createUnifiedTheme({
...createBaseThemeOptions({
palette: palettes.light,
}),
fontFamily: 'Comic Sans MS',
defaultPageTheme: 'home',
components: {
BackstageHeader: {
styleOverrides: {
header: ({ theme }) => ({
width: 'auto',
margin: '20px',
boxShadow: 'none',
borderBottom: `4px solid ${theme.palette.primary.main}`,
}),
},
},
},
});
styleOverrides.header 接收一个回调,参数 theme 即为当前主题对象,因此你可以引用 theme.palette.primary.main 等任意主题 token 来构建动态样式。同时,createUnifiedTheme 内部会把 defaultComponentThemes 与你的 components 合并(见 packages/theme/src/unified/UnifiedTheme.tsx),所以你只需写增量覆盖即可。
八、自定义 Logo
除了主题,你还可以替换站点最左上角显示的 Logo。在应用前端中找到 src/components/Root/ 目录,里面有两个组件:
LogoFull.tsx:侧边栏展开时使用的大 Logo;LogoIcon.tsx:侧边栏收起时使用的小 Logo。
替换图片最简单的方式是直接在这两个组件中用 SVG 定义替换原有代码。也可以导入 PNG 等其他 web 图片格式:把新图片放到类似 src/components/Root/logo/my-company-logo.png 的子目录,然后:
import MyCustomLogoFull from './logo/my-company-logo.png';
const LogoFull = () => {
return <img src={MyCustomLogoFull} />;
};
九、自定义与新增图标
9.1 覆盖内置图标(Custom Icons)
@backstage/plugin-app-react 提供了 IconBundleBlueprint,可创建扩展来覆盖内置图标。例如把 github 图标替换为自定义的 ExampleIcon:
import { createApp } from '@backstage/frontend-defaults';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { IconBundleBlueprint } from '@backstage/plugin-app-react';
import { ExampleIcon } from './assets/customIcons';
const customIconBundle = IconBundleBlueprint.make({
name: 'custom-icons',
params: {
icons: {
github: ExampleIcon,
},
},
});
const app = createApp({
features: [
createFrontendModule({
pluginId: 'app',
extensions: [customIconBundle],
}),
],
});
export default app.createRoot();
IconBundleBlueprint 与 ThemeBlueprint 同位于 plugins/app-react/src/blueprints,遵循相同的扩展蓝图机制。
9.2 新增图标(Adding Icons)
可以注册额外的图标,供实体链接(entity links)等场景使用。例如注册一个 alert 图标:
import { createApp } from '@backstage/frontend-defaults';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import AlarmIcon from '@material-ui/icons/Alarm';
import { IconBundleBlueprint } from '@backstage/plugin-app-react';
const extraIcons = IconBundleBlueprint.make({
name: 'extra-icons',
params: {
icons: {
alert: AlarmIcon,
},
},
});
const app = createApp({
features: [
createFrontendModule({
pluginId: 'app',
extensions: [extraIcons],
}),
],
});
export default app.createRoot();
然后在实体定义的 links 中通过 icon 字段引用它:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: artist-lookup
description: Artist Lookup
links:
- url: https://example.com/alert
title: Alerts
icon: alert
效果如下图所示:
另一种使用方式是从 AppContext 中获取图标。如果你需要在多个位置复用某个图标,可以这样做:
import { useApp } from '@backstage/core-plugin-api';
const app = useApp();
const alertIcon = app.getSystemIcon('alert');
getSystemIcon 由 App 上下文实现(见 packages/core-plugin-api/src/app/useApp.tsx),它会按名称从已注册的系统图标集合中取出对应的 IconComponent。
注意:如果请求的图标既不是默认图标、也不是你注册的自定义图标,系统会回退到 Material UI 的
LanguageIcon。
十、自定义侧边栏与首页
侧边栏
在新前端系统中,侧边栏由内置的 app/nav 扩展管理。你可以创建 NavContentBlueprint 扩展来自定义它。关于如何创建带子菜单、自定义分组的侧边栏布局,详见 08-migrating.md 文档中的 "App root sidebar" 一节。
首页
除了自定义主题与 Logo,你还可以自定义应用的首页,完整指南见 homepage.md。
十一、关于 Material UI v5 迁移
Backstage 现已支持 Material UI v5。如果你的应用仍在使用 v4,可以参考迁移指南 migrate-to-mui5.md 开始升级。正如前文所述,UnifiedTheme 的设计使得同一份主题配置可以同时产出 v4 与 v5 主题对象,这为渐进式迁移提供了坚实保障。
总结
Backstage 的主题定制是一条"分层覆盖"的路径:先用 createUnifiedTheme + createBaseThemeOptions 从调色板、字体、排版、页面主题四个维度定义主题本体;再用 ThemeBlueprint 把主题注册为新前端系统的扩展;随后按需通过 components 覆盖组件样式、替换 Logo、用 IconBundleBlueprint 覆盖或新增图标,最终在 app-config.yaml 中决定内置主题的去留。所有自定义代码都集中在 packages/app/src/theme 与应用根组件中,结构清晰、易于维护,且全部基于 @backstage/theme 与 @backstage/plugin-app-react 的公开 API,可放心长期使用。
关键文件索引
- 主题工厂与 UnifiedTheme 实现:packages/theme/src/unified/UnifiedTheme.tsx、packages/theme/src/unified/themes.ts
- 基础主题选项、默认排版:packages/theme/src/base/createBaseThemeOptions.ts
- 内置调色板:packages/theme/src/base/palettes.ts
- 页面主题、形状与配色变体:packages/theme/src/base/pageTheme.ts
- 主题扩展蓝图:plugins/app-react/src/blueprints/ThemeBlueprint.ts、plugins/app-react/src/blueprints/IconBundleBlueprint.ts
- 图标系统接口:packages/core-plugin-api/src/app/useApp.tsx
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