首页
/ Ghost Admin(React):新一代发布后台如何通过 Ember Bridge 渐进式替换 Ember 管理界面

Ghost Admin(React):新一代发布后台如何通过 Ember Bridge 渐进式替换 Ember 管理界面

2026-09-07 09:38:41作者:齐冠琰

Ghost Admin 是 Ghost 博客平台的新一代后台管理界面,它基于 React 重写,目标是在不大规模"大爆炸式重写"的前提下,逐步替换掉历史悠久的 Ember.js 管理后台(ghost-admin)。本文以其官方说明文档 apps/admin/README.md 为主线,结合仓库内路由、桥接、CSS 与测试体系的源码实现,讲解这套渐进式迁移架构、部署兼容策略,以及单元测试、验收测试与生产构建的全套工程实践。

背景:为什么需要一套"桥"而非"重写"

Ghost 的后台沉淀了大量成熟功能(成员管理、设置、编辑器、仪表盘等),全部推倒重写风险极高。因此仓库采取了渐进式替换路线:新的 React 管理界面与旧 Ember 管理界面长期共存,直到所有路由逐一迁移完成。apps/admin/ 目录就是这个新的 React 应用,官方文档称之为 "New React-based Ghost admin interface, gradually replacing the existing Ember admin"。

这一共存机制的核心是一套称为 Ember Bridge 的桥接系统,其工作方式可以概括为:

  • 已移植到 React 的路由直接渲染 React 组件;
  • 尚未移植的路由回退(fallback)到仍在运行的 Ember 管理后台;
  • 两者共享同一片 UI 空间,用户几乎感知不到切换的边界。

从源码看,"共享 UI 空间"并非比喻:React 应用与 Ember 应用事实上挂载在同一个页面里协同工作。

架构解剖:React、admin-x-framework、Shade 与 Ember 如何共存

应用入口 src/main.tsx 非常简短:它创建一个 React root 渲染 AdminAppRoot,并把一份 framework 配置传进去,其中包括导航回调(externalNavigate)、Unsplash 配置(defaultUnsplashConfig)、Sentry DSN 以及来自 ember-bridgeemberMutationHandlers

AdminAppRoot(见 src/app-root.tsx)则拼出完整的 Provider 金字塔:

StrictMode
└── FrameworkProvider        (来自 admin-x-framework,注入 API 与桥接回调)
    └── RouterProvider       (前缀 "/",路由表来自 src/routes.tsx)
        └── ThemeProvider
            └── ShadeApp    (design system 外壳,darkMode 主题切换)

注释中特别说明:这个 Provider 栈被生产入口 src/main.tsx 与验收测试入口 renderAdminApp 原样共享,因此"测试渲染的就是与生产一致的组件栈"在构造层面就是成立的,两边只在 framework props(导航/桥接回调、QueryClient)上存在差异。

Ember Bridge 的状态桥:StateBridge

桥接的底层契约定义在 src/ember-bridge/ember-bridge.tsx。Ember 侧会把一个全局对象挂到 window.EmberBridge 上,其中核心是 stateStateBridge),它提供:

  • 数据同步:onUpdate(dataType, response)onInvalidate(dataType)onDelete(dataType, id)
  • 事件订阅:on(event, callback) / off(event, callback),事件类型见 StateBridgeEventMap,包括 emberDataChangeemberAuthChangesubscriptionChangesidebarVisibilityChangerouteChangeopenGiftLinkModalfeatureFlagsChange
  • 状态读取:sidebarVisibleisFeatureEnabled(name)
  • 路由互操作:getRouteUrl(routeName, queryParams)isRouteActive(...)
  • 主题联动:preloadAdminThemeStylesheet()applyAdminThemePreference(mode)

由于 Ember 应用可能尚未加载完成,桥接代码提供 waitForStateBridge——以 100ms 间隔轮询 window.EmberBridge,拿到后再注册订阅(源码注释表明未来可能懒加载 Ember,因此轮询是"无限期"的)。

基于这层契约,仓库暴露了一整套 React Hook(useEmberDataSyncuseEmberAuthSyncuseEmberFeatureFlaguseSidebarVisibilityuseEmberRoutinguseForceUpgrade 等),供 React 侧响应 Ember 状态:

  • useEmberDataSync / useEmberAuthSync:监听 Ember Data 的增删改与登录态变化,自动使对应的 React Query 缓存失效,保证两端数据一致。这里有一张模型名映射表 EMBER_TO_REACT_TYPE_MAPPING,把 Ember Data 模型(integrationsettingpostmember…)翻译成 React 的 ResponseType 字符串。代码里还有一个值得注意的细节:保存 post/page 会连带使 TagsResponseType 失效,因为编辑器里新建的 tag 是作为 post 内嵌关联一起写入的,Ember 只会上报 post 变更——若不同步失效,帖子列表的 tag 过滤就会一直命中旧缓存。
  • useSidebarVisibility:以 Ember 为权威来源,通过 useSyncExternalStore 订阅 sidebarVisible(例如进入编辑器时 Ember 会隐藏侧栏)。
  • useEmberRouting:把 Ember 的路由 URL 生成与激活判断暴露给 React。值得留意的是,源码对 isRouteActive 加了条件:React 自有的导航走 pushState,Ember 观察不到,因此只有在当前正渲染 Ember fallback 时才信任 Ember 的路由状态。
  • useForceUpgrade:综合 config.hostSettings.forceUpgrade 与订阅状态判断站点是否处于强制升级(欠费)模式。
  • emberMutationHandlers:反向通道——React 侧的 mutation 成功后,通过 window.EmberBridge?.state.onUpdate/onInvalidate/onDelete 通知 Ember 的 store 同步,无桥接时安全地 no-op。

路由分工:谁渲染什么,一目了然

真正的"渐进"体现在路由表 src/routes.tsx 中。文件顶部的 EMBER_ROUTES 数组集中声明了仍由 Ember 处理的路由:

const EMBER_ROUTES: string[] = [
  '/site', '/setup', '/signin/*', '/signout', '/signup/*', '/reset/*',
  '/pro/*', '/posts/analytics/:postId/debug', '/restore', '/migrate/*',
  '/members-activity',
];

这些路径统一挂到 EmberFallback 组件。EmberFallback(见 src/ember-bridge/ember-fallback.tsx)本身渲染 null,只通过 registerFallback() 在挂载/卸载时通知 Ember 显示/隐藏自身——相当于一个"我接管了这块屏幕"的占位信号。

另一批路由则由 Gate(网关)组件根据 Labs 功能开关决定交给 React 还是 Ember:

  • /posts/pagesPostsListGate / PagesListGate,依据 postsListReact 开关;
  • /editor/*EditorGate,依据 editorReact 开关;
  • /tags/:tagSlugTagDetailGate,依据 tagDetailsReact 开关。

辅助路由还包括负责角色鉴权的 RouteAccessGuardhandle.requiresAccess 规则:如 /tags 要求 canManageTags/members 要求 canManageMembers)、负责欠费强制升级的 ForceUpgradeGuard、以及 /posts 下的帖子分析(/posts/analytics/:postId)、自动化的全屏编辑器(/automations/:id,带 hideAdminSidebar)、设置模块(settings,同样隐藏侧栏)等。未被 React 或 Ember 覆盖的路径最终落入 NotFound 404 兜底。

一个工程细节:因为 Ember 的 router 只通过 hashchange 感知 URL 变化,而 React Router 的 pushState 导航不会触发该事件,所以链接进 Ember 管辖路由时必须使用原生 hash 锚点——useIsEmberOwnedRoute 就是为此而生的判断工具。

CSS 策略:单一 Tailwind 入口与无分层级联

Admin 的样式只有一个入口:src/index.css。它是 "single Tailwind CSS entry point",其职责可归纳为四块:

  1. @source 声明 Tailwind 的源码扫描范围,让原子类工具能够覆盖多个工作区:
@source "../../shade/src/**/*.{ts,tsx}";
@source "../../activitypub/src/**/*.{ts,tsx}";
@source "../../admin-x-framework/src/**/*.{ts,tsx}";
@source "../node_modules/@tryghost/kg-unsplash-selector/dist/**/*.js";

这保证了 Admin、Shade、ActivityPub、admin-x-framework 以及内嵌的 Koenig Unsplash 选择器都能被 Tailwind 识别。整个仓库只有这个应用为内嵌 Admin 的 CSS 流水线加载 @tailwindcss/vite 插件(见 apps/admin/package.json 的 devDependencies)。

  1. 入口导入@import '@tryghost/shade/styles.css' 引入设计系统样式,再导入 Inter Admin 字体;随后以 @theme 块声明主题字体族变量(从 Cardo、Manrope 到 JetBrains Mono 等二十余种供设置页字体选择器使用)。

  2. 定义 .admin7 作用域下的排版与字体特性:CSS 代码中大量使用 @custom-variant admin7body.react-admin:has(> #root .admin7) 这类选择器,把字体规则镜像到 Shade portal 与 Ember 遗留 overlay(#ember-basic-dropdown-wormhole#ember-modal-wormhole 等)。

  3. 排版 Ember 与 React 共存的页面网格body.react-admin 使用 height: 100svh; display: grid; overflow: hidden 建立 auto 1fr 的网格,alerts wormhole 占第一行、#root 下的 .shade.shade-admin 占第二行;#ember-appisolation: isolate 隔离旧 Ember 的 z-index,防止其(如 billing overlay 的 9999)盖过 React 外壳的侧栏抽屉。

文档给出了两条刚性约束

  • 内嵌的 Admin 应用不得自行 @import '@tryghost/shade/styles.css'——否则会生成重复的 utility 类,与 Ember 遗留 CSS 产生级联冲突;
  • Shade 的 Tailwind 导入必须保持无分层(unlayered),因为 Ember 遗留 CSS 也是无分层的,依赖源码顺序解决重叠工具类的覆盖关系。在未考虑遗留级联的前提下,不要把 Shade 导入挪进某个 CSS layer。

这种"谁的样式跟谁走"的分界,是迁移期新旧 UI 在同一页面共存而不互相污染的关键。

部署兼容:Admin 与 Core 可以不同步上线

一个极易踩坑的工程原则:Ghost Admin(前端)与 Ghost Core(后端)允许在不同时间部署。因此,依赖某个新 setting、endpoint 或 config 值的新 UI,必须主动探测后端是否支持,不支持时隐藏或安全禁用该功能。

文档特别警告:仅靠 Labs 开关不能作为兼容性判断——Labs 开关可能在支撑它的后端版本上线之前就存在了。与此同时,官方要求为"旧后端"场景补一个验收测试;settings 的社会账号设置与 membership tiers 相关测试里保留着"在支撑性设置出现前先隐藏控件"的现行范例。这是保证灰度发布安全性的最后一道防线。

本地开发:从 monorepo 根目录一键启动

开发命令:

# 从 monorepo 根目录启动开发服务器
pnpm dev

pnpm dev 会联动启动多个工作区。在 apps/admin/package.json 的 Nx 配置里可以看到 dev target 的 dependsOn:它会先拉起 ghost-monorepo 的 docker(ghost-monorepo:docker:up)、Ember admin(ghost-admin:dev)、admin-x-framework 与 Shade 的 dev server,最后才启动 Vite。

开发新功能时文档给出三条纪律:

  1. 在这个 React 应用里开发新后台功能
  2. API 访问统一走 admin-x-framework,UI 统一用 Shade,不再新增 admin-x-design-system 组件;
  3. 产品文案放在 ghost i18n namespace 下,遵循 国际化指南。该指南说明:翻译文件位于 packages/i18n/localesghost namespace 覆盖 Ghost Core(含服务端、前台与会员邮件模板),英文源字符串本身就是翻译 key。

测试体系:单元 / 验收 / 浏览器 e2e 三层

Admin 的测试按成本与保真度分三层,各有归属:

单元测试(Unit)

pnpm test:unit

基于 Vitest + jsdom,测试文件与被测组件同目录共存(*.test.ts(x))。从 package.json 的脚本可见:test 默认就是 test:unit,且 Nx 配置里 unit 与 acceptance 都 dependsOn ^build(先构建依赖的工作区)。

验收测试(Acceptance)

pnpm test:acceptance        # 运行一次
pnpm test:acceptance:watch  # watch 模式

这套体系的机制很特别,详见 apps/admin/test-utils/acceptance/README.md:它用 Vitest Browser Mode 启动真实的 Chromium,跑的是真实 Admin 应用(与 src/main.tsx 相同 Provider 栈),但 API 层由浏览器内的 MSW 提供一份"简化但可用"的假 Ghost Admin API(与 e2e 中 fake-stripe-server 同族的 test double)。外壳的启动请求(settings/config/site/me、成员计数、当前主题、changelog feed)由默认的 boot.ts 处理,测试用例无需关心。

验收测试还建立了一套颇具设计感的约定:

  • 418 循环:任何未被假 API 处理的请求都会被回 418,并在 afterEach 中使测试失败、列出缺失的路由——与其猜测应用的网络图,不如让测试告诉你还差什么 fake。
  • Boot 表browseSettings/browseConfig/browseSite/browseMe/browseMembersCount/browseActiveTheme/editUserPreferences 默认已处理;renderAdminApp(route, {boot: {...}}) 可覆盖其中某项的响应,{labs: {someFlag: true}} 则是同步覆盖 setting + config 的语法糖。Boot 响应是无状态的,唯一例外是 editUserPreferences 会把 PUT 的 user 字段回显——否则框架会把缓存用户整个覆盖掉。
  • THE RULE:fake 永不实现 NQL 语义——声明响应、断言发出的过滤字符串,而不是去重实现一次查询。
  • 断言风格统一为三种:元素状态(expect.element(...).toBeVisible() 等)、元素数量(toHaveCount(n))、以及被捕获的请求(expect(membersApi).toHaveSentFilter(...))。
  • 每个测试只 render 一次(每轮渲染配一个全新 QueryClient),需要跨刷新持久化真实服务器状态的旅程归属 e2e 层;由 Ember state-bridge(window.EmberBridge)喂数据的 UI(如 nav 激活态、upgrade banner)也无法在这一层覆盖,应留在 e2e。

浏览器 e2e

针对真实 Ghost 实例的浏览器端到端测试位于仓库顶层 e2e 工作区,由 Playwright 驱动,页面对象与验收测试共享来自 packages/testing/test-data 的 selector 常量注册表,保证 UI 一旦改动两层测试同步失效。

生产构建:产物落地与 Nx 编排

pnpm nx run @tryghost/admin:build

根据 apps/admin/package.json 的 Nx target 定义,build 的输出有两个:{projectRoot}/dist(即 apps/admin/dist/)以及 {workspaceRoot}/ghost/core/core/built/admin。也就是说——tsc -b && vite build(见 package.jsonbuild script)先产出 React 应用的静态资源,Ember Admin 的 asset-delivery addon 再把产物与 Admin 资源一起拷贝进 ghost/core/core/built/admin/,最终由 Ghost Core 对外提供服务。整个构建链的依赖顺序是:先构建依赖工作区(admin-x-framework、shade、checkout 等),再构建 Ember admin(ghost-admin:build),最后打包 React Admin。

这也解释了 CSS 章节的拓扑约束:只有 apps/admin 需要为"内嵌 Admin"产出独立 CSS 流水线,其余应用按各自能力被集成进这一个入口。

小结:一条可借鉴的"渐进式重构"工程样板

apps/admin/README.md 出发可以看到,Ghost 的 React Admin 迁移并非简单的"新技术替换旧技术",而是一整套精密的共存工程:

  • 路由级灰度EMBER_ROUTES 集中登记未迁移路由,*Gate 组件配合 Labs 开关逐页放量 React 版本;
  • 状态桥与数据同步window.EmberBridge.state 契约 + 映射表 + React Query 失效,让两套框架共享同一份数据真相;
  • CSS 隔离共存:单一 Tailwind 入口、无分层级联策略、z-index 隔离,解决新旧 UI 同屏渲染的样式战争;
  • 前后端独立部署的兼容意识:用能力探测 + 旧后端验收测试保证灰度安全;
  • 三层测试与共享 selector 注册表:unit 保快速、acceptance 用真实应用+MSW 假 API 保高保真、e2e 对真实实例兜底。

对于任何面临"大型遗留系统渐进迁移"挑战的团队,这套以"桥"为核心、以测试为护栏的架构,都是一份值得反复研读的参考资料。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389