TT Lab
开始
学习 学习路径 课程

CBA — Backstage 认证助理

yarn tsc 产出什么,以及 Dockerfile 为何解两次 tar

在 TT Lab 中继续学习

一句话总结

Backstage 应用通过 npx @backstage/create-app@latest 创建,用 yarn start 同时启动前端(3000)和后端(7007)。yarn tsc 把整个仓库作为一个编译单元进行类型检查,并把结果留在 dist-types/ 中;yarn build:backend 会在 packages/backend/dist/ 中生成 skeleton.tar.gz 和 bundle.tar.gz 两个归档文件。Docker 镜像按顺序解开这两个文件,以缓存依赖安装。主题通过 themes 注册到 packages/app/src/App.tsx 的 createApp 中;插件的 React 组件放在 plugins/<id>/src/components/ 中,由 plugin.ts 作为扩展(extension)导出。来源文档有:入门、构建系统、构建 Docker 镜像、自定义 UI 和 插件结构。

为什么需要它

Backstage 不是产品而是框架,所以针对你所在组织定制的应用仓库就是最终交付物。这个仓库是用 Yarn 工作区组织的 monorepo,前端、后端和插件各自是一个软件包。由于这种结构,“构建”并不只有一种——类型检查、软件包构建、前端打包、后端打包以及容器镜像,各自有不同的工具和产物。考试把这套工作流归为一个领域(24%)来考查,是因为如果不知道哪一步生成什么、哪一步使用它,就既读不懂 CI 流水线,也读不懂 Dockerfile。

工作原理

创建并启动

npx @backstage/create-app@latest 会询问应用名称,在同名目录中生成文件,然后一直执行到 yarn install 和 yarn tsc。生成物的骨架如下。

app
├── app-config.yaml      # 앱 설정
├── catalog-info.yaml    # 카탈로그 엔티티 기술자
├── package.json         # 루트. 여기에 npm 의존성을 넣지 말 것
└── packages
    ├── app              # 프론트엔드 앱
    └── backend          # 백엔드

yarn start 会把前端和后端作为 [0]、[1] 两个进程在同一个窗口中启动,看到“Rspack compiled successfully”后,就可以在 http://localhost:3000 查看应用。如果系统是隔离的,就需要开放 3000 和 7007 端口。这种独立安装使用内存 SQLite 和演示数据,仅用于评估,不适合生产。环境要求是 Node.js Active LTS(文档推荐 22 或 24)、Yarn 4.4.1(执行 corepack enable 后再执行 yarn set version 4.4.1)、20GB 磁盘和 6GB 内存。

类型检查——整个仓库是一个单元

构建系统文档最强调的特点是:整个项目是一个 TypeScript 编译单元。这是因为如果按软件包拆开,配置会变复杂,整体类型检查还会慢一个数量级。因此每个软件包的入口都指向 TypeScript 源码。本地默认使用增量(incremental)检查,结果累积在仓库根目录的 dist-types/ 中。它还会跳过 node_modules 中库的类型检查来换取速度,而在 CI 中则建议使用关闭这两项优化的 yarn tsc:full。dist-types/ 不只是缓存——package build 生成的类型声明文件以这个文件夹为入口,所以在构建带类型声明的软件包之前,必须先运行类型检查。

三种构建及其产物

命令 工具 产物 对象
backstage-cli package build Rollup 软件包的 dist/ 中的 CJS、ESM 和类型声明 除 frontend、backend 角色之外的软件包(插件、库)
前端打包 Webpack(以文档为准;启动日志中显示的是 Rspack) dist/ 中的普通资源(短缓存)+ dist/static/ 中的哈希资源(长缓存) packages/app
yarn build:backend / backend:bundle 自行收集 packages/backend/dist/bundle.tar.gz + skeleton.tar.gz packages/backend

后端打包不使用 Webpack。它把后端软件包及其本地依赖按与 monorepo 相同的目录布局收集起来,打成 bundle.tar.gz,并放入根目录的 package.json 和 yarn.lock。旁边的 skeleton.tar.gz 布局相同,但只有 package.json 文件。为什么要拆成这两个,是读懂 Dockerfile 的钥匙——仅凭骨架就可以执行 yarn install,所以即使源码变了,只要依赖不变,安装层就会被缓存。生成打包之前,必须先构建好后端软件包;如果加上 --build-dependencies 标志,打包命令会代为构建。

Docker 镜像——host build 与 multi-stage

Docker 文档把方式分为两种,并推荐第一种。

Host build:构建的大部分在 Docker 之外(主机或 CI)完成。顺序是 yarn install --immutable → yarn tsc → yarn build:backend,然后用 packages/backend/Dockerfile 构建镜像。这个 Dockerfile 必须以仓库根目录作为构建上下文来运行,才能访问根目录的 yarn.lock 和 package.json。

docker image build . -f packages/backend/Dockerfile --tag backstage
docker run -it -p 7007:7007 backstage

create-app 放入的 Dockerfile 的流程如下。它基于 node:24-trixie-slim,用 USER node 降权之后,复制 .yarn、.yarnrc.yml 和 backstage.json,再复制并解开 yarn.lock、package.json 和 skeleton.tar.gz,用 yarn workspaces focus --all --production 只安装生产依赖,最后复制并解开 bundle.tar.gz 和 app-config*.yaml。启动命令是 node packages/backend --config app-config.yaml --config app-config.production.yaml。同时生成的 .dockerignore 会排除 packages/*/src、plugins、node_modules 和 *.local.yaml 来缩小上下文——因为这种方式放入的是构建产物而不是源码。文档警告说,主机上的 Node 版本必须与基础镜像一致,原生模块才不会在运行时损坏。

Multi-stage build:整个构建都在 Docker 内完成。通常更慢,但在构建环境要求必须在 Docker 内构建或有其他限制时使用。它分为三个阶段——第 1 阶段用 find 只保留 package.json,生成用于缓存 yarn install 的骨架层;第 2 阶段通过 yarn install --immutable → yarn tsc → yarn --cwd packages/backend build 完成与 host build 相同的工作,并把两个归档文件解开备用;第 3 阶段生成最终镜像。这种方式的 .dockerignore 需要访问源码,所以与 host build 的不同,只排除 dist-types、node_modules 和 packages/*/dist 这样的产物。

两种方式都有前提——默认的 Guest 认证 provider 不适用于容器环境,所以要先配置好认证 provider,并准备好 Postgres。如果要单独提供前端服务,必须把 @backstage/plugin-app-backend 从后端中去掉,这样后端就失去了向前端注入配置的功能。如果构建出现异常,还有建议说可以加上 --progress=plain 和 --no-cache 试试。

主题——Material UI 与 Backstage UI 两套体系

自定义 UI 文档说明,Backstage 目前同时存在两套 UI 体系。原有的 Material UI(MUI) 是基于 JS 的主题,通过 UnifiedThemeProvider 应用,大多数现有插件都在使用。新的 Backstage UI(BUI) 基于 CSS 变量和令牌,类名以 bui- 开头。应该改哪一套,要看组件的类名来决定。

注册的位置只有一个——给 packages/app/src/App.tsx 的 createApp 传入 themes 数组。每一项包含 id、显示在设置界面中的 title、值为 light 或 dark 的 variant(会以 data-theme-mode 属性写入 body)、icon,以及供 MUI 使用的 Provider。这个数组会替换默认主题,所以必须同时放入 light 和 dark;如果需要默认值,可以使用 @backstage/theme 的 themes.light 和 themes.dark。

import { createBaseThemeOptions, createUnifiedTheme, palettes } from '@backstage/theme';

export const lightTheme = createUnifiedTheme({
  ...createBaseThemeOptions({ palette: palettes.light }),
  fontFamily: 'Comic Sans MS',
  defaultPageTheme: 'home',
});

MUI 主题是把 createBaseThemeOptions({ palette }) 展开放入 createUnifiedTheme 来创建的。先展开 palettes.light,再覆盖 primary.main、navigation.background 之类的值;用 pageTheme 中的 genPageTheme({ colors, shape: shapes.wave }) 来设定页面标题栏的颜色和形状;在 typography 中展开 defaultTypography 后只修改 h1,以这样的方式进行局部重写。自定义字体则是把 @font-face 放进 components 的 MuiCssBaseline 的 styleOverrides 中。BUI 一侧,在 App.tsx 中导入 packages/app/src/styles.css,并在 :root 以及 [data-theme-mode='light']、[data-theme-mode='dark'] 之下覆盖 --bui-bg-app、--bui-fg-primary 之类的变量。

React 组件放在插件的什么位置

在 yarn new 中选择 frontend-plugin 会生成插件软件包,并自动连接到应用——app/package.json 的依赖和 app/src/App.tsx 的 import 会一并添加,所以应用运行时,可以直接在 http://localhost:3000/my-plugin 看到。插件是拥有 package.json 和 src/ 的独立软件包,所以可以通过 npm 发布,也可以不启动整个应用,而用 dev/ 目录中的配置单独运行。

plugins/my-plugin/
  dev/index.ts                      # 플러그인만 따로 띄우는 설정
  src/
    components/ExampleComponent/    # 페이지 컴포넌트 (React)
    components/ExampleFetchComponent/  # 외부 API 를 부르고 MUI 표로 그림
    plugin.ts                       # createPlugin + createRoutableExtension
    routes.ts                       # rootRouteRef
    index.ts                        # 폴더 단위 export

plugin.ts 是连接的核心。用 createPlugin({ id, routes: { root: rootRouteRef } }) 创建插件,并把 createRoutableExtension({ name, component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent), mountPoint: rootRouteRef }) 用 plugin.provide() 包起来导出。应用导入这个扩展并挂到路由上。也就是说,React 组件的修改在 src/components/ 中进行,而要让新页面在应用中可见,必须在 plugin.ts 中把它作为扩展导出。这份文档以旧版前端系统为准,并附有提示:在新的前端系统中,plugin.ts 的连接方式有很大不同。

在现场相遇的样子

有一个团队的 CI 镜像构建因“packages/backend/dist/skeleton.tar.gz not found”而失败。原因是 Dockerfile 被以 packages/backend/ 为上下文来运行,而且在此之前还漏掉了 yarn build:backend。host build 是把在主机上生成的产物放进镜像的方式,所以构建顺序和上下文根目录就是契约。

另一个团队在本地 yarn tsc 能通过,只有 CI 失败。本地开启了增量检查和跳过库类型检查,而 CI 用的是 tsc:full。文档建议在 CI 中使用 tsc:full,正是因为这种差异。

下一项测验要确认什么

测验会考查:yarn start 启动的内容和端口,yarn tsc 的产物以及为什么需要 dist-types/,skeleton.tar.gz 与 bundle.tar.gz 的区别,host build 与 multi-stage build 的区别,createApp 的 themes 条目,以及 plugin.ts 的作用。