Ghost Core 运行时架构深度解析:单 Node 进程如何同时承载站点、Admin 与 API
【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost
Ghost 是一个为现代出版、会员订阅与通讯稿设计的开源发布平台。本篇技术指南基于仓库中 runtime-architecture.md 展开,逐层拆解 Ghost Core 这个单一 Node.js 应用如何在同一进程中同时提供公开站点、Admin 管理后台与两套 HTTP API,并运行支撑这些界面的服务与后台任务。读完后你将掌握 Ghost 的请求处理链路、Boot 启动序列、后端(backend)与前端的进程内边界,以及浏览器应用如何独立部署并通过 HTTP 与 Core 通信。
运行时边界全景:一个进程、一套配置、多种 Express 应用
从架构文档的定位看,Ghost Core 不是微服务,而是一个"单体应用 + 独立浏览器应用"的组合:
- Ghost Core是唯一的 Node.js 应用,承载公开站点、Admin、Content API、Admin API,并运行这些界面所需的 Service 与后台任务;
apps/下的浏览器应用(Admin、Portal、Comments、Search、Signup Form、Announcement Bar、Admin Toolbar 等)各自独立构建,运行时通过 HTTP 与 Ghost Core 通信;- 想了解按目录组织的代码地图,可参考 monorepo 结构指南。
Ghost 的完整运行时是一棵"共享父应用 + 两个子应用"的 Express 应用树,文档给出的结构如下:
Ghost server └── parent application ├── backend application │ ├── /ghost/api/content/ Content API │ ├── /ghost/api/admin/ Admin API │ └── /ghost/ Admin application └── frontend application ├── /members/ Member routes ├── /webmentions/ Webmention routes ├── /gift/ Gift preview └── / Public site and theme routes需要特别强调文档中的一个关键结论:backend 与 frontend 虽然可以在配置层面被挂载到不同的主机名(hostname)或子目录上,也构成了清晰的代码边界,但它们绝不是两个独立服务——两者共享同一个进程、同一条启动序列、同一份配置、同一个数据库以及大量服务端 Service。这意味着"拆开部署"是不成立的:它们是进程内边界,不是部署边界。
启动与请求处理:从维护模式到完整应用
三段式启动:先用最小应用挡住流量
Ghost 并不是把完整应用直接启动的。bootGhost()的注释将该文件定位为"唯一的大文件,避免到处追踪启动逻辑",其入口位于 ghost/core/core/boot.js。启动过程的关键步骤是:
- 加载地基:版本信息、配置(
./shared/config)、日志(@tryghost/logging),并挂上unhandledRejection处理器; - 启动一个处于全局维护模式的极简 Express 应用(即
rootApp),见 ghost/core/core/app.js; - 让数据库就绪:通过
DatabaseStateManager完成迁移,使 DB 处于可用状态; - 初始化 Core 与各类服务(adapter、settings、limits、jobs 等);
- 构建完整 Express 应用并挂到
rootApp下; - 关闭维护模式,通知"server ready",并在后台继续启动非阻塞的周期任务。
其中第 5、6 步在 ghost/core/core/boot.js 中表现为:
// Step 5 - Mount the full Ghost app onto the minimal root app & disable maintenance mode rootApp.disable('maintenance'); rootApp.use(config.getSubdir(), ghostApp);即:完整应用是在数据库与核心服务全部就绪之后才被挂载的,而在此之前对外表现是"维护中"。
维护模式的实现细节
为何要在启动阶段挡住流量?因为启动早期 URL 服务尚未完成构建。看 ghost/core/core/app.js 中维护模式的判定逻辑:
const isMaintenanceModeEnabled = (req) => { if ( req.app.get('maintenance') || config.get('maintenance').enabled || !urlService.hasFinished() ) { return true; } return false; };任一条件成立即进入维护模式:应用级维护开关被打开、配置中的maintenance.enabled为真、或urlService尚未完成(URL 服务未就绪时资源无法被正确解析)。此时请求会收到503响应,并渲染 ghost/core/core/server/views/maintenance.html,同时设置no-cache, private, no-store等禁止缓存的响应头。
启动阶段的实际耗时可以通过 BootLogger 观测——boot.js 顶部注释 提示可用DEBUG=ghost:boot* node ghost查看各步骤的计时日志,日志格式如Ghost server started in Xs、Ghost database ready in Xs。
挂载方式:按配置的挂载路径做 vhost 分流
两个子应用如何被挂到同一个父应用上?ghost/core/core/boot.js 中initExpressApps使用@tryghost/mw-vhost按配置的挂载路径把 backend 与 frontend 分流:
if (backend) { const backendApp = require('./server/web/parent/backend')(); parentApp.use(vhost(config.getBackendMountPath(), backendApp)); } if (frontend) { const urlService = require('./server/services/url'); const frontendApp = require('./server/web/parent/frontend')({ urlService }); parentApp.use(vhost(config.getFrontendMountPath(), frontendApp)); }getBackendMountPath()与getFrontendMountPath()由 ghost/core/core/shared/config/helpers.ts 提供,二者都基于 URL 配置推导出挂载路径。这就是文档所说"backend 与 frontend 可被挂载到不同的配置主机名或子目录"的代码落点;实际允许哪些值、如何组合子目录,可结合 configuration.md 阅读。bootGhost还支持只启动 backend 或 frontend 的选项(参数backend/frontend),这在测试与某些服务场景下非常有用。
共享父应用:统一请求管道
在请求进入 backend 或 frontend 之前,父应用先施加一层共享管道。见 ghost/core/core/server/web/parent/app.js,其职责按顺序包括:
| 中间件 | 作用 | 启停条件 |
|---|---|---|
requestId | 为每个请求生成请求 ID,便于追踪 | 始终启用 |
filterQueryParameters | 按白名单过滤 URL 查询参数,防止参数污染缓存键 | 配置queryParameterFiltering.enabled为真时启用 |
logRequest | 请求日志 | 始终启用 |
emitEvents | 在 req/res 上注册事件发射器,用于触发缓存失效等 webhook 事件 | 始终启用 |
compress() | gzip 压缩 | 默认启用,compress配置为false时关闭 |
ghostLocals | 注入各处需要的公共res.locals | 始终启用 |
queueRequest | 可选请求排队 | 配置了optimization:requestQueue时启用 |
这与文档"共享父应用在请求到达 backend/frontend 前添加请求 ID、请求日志、压缩、公共响应 locals 与可选的请求排队"的表述一一对应。
Backend 应用:两套 API 与 Admin 的挂载细节
父应用中 backend 分支的实现在 ghost/core/core/server/web/parent/backend.js:
const { BASE_API_PATH } = require('../../../shared/url-utils'); const backendApp = express('backend'); backendApp.lazyUse(BASE_API_PATH, require('../api')); backendApp.lazyUse('/ghost/.well-known', require('../well-known')); backendApp.use( '/ghost', require('../../services/auth/session').createSessionFromToken(), require('../admin')(), );三个关键挂载点:
BASE_API_PATH(即/ghost/api)之下挂载的是 API 入口;/ghost/.well-known用于暴露安全验证类端点;/ghost挂载 Admin 应用,其前置是createSessionFromToken()会话中间件(Admin 请求先完成会话/令牌解析,再进入 Admin 路由)。
API 入口 ghost/core/core/server/web/api/app.js 进一步把/ghost/api拆分为/content/与/admin/两组端点,并施加 API 级共享中间件:
apiApp.use(APIVersionCompatibilityService.versionRewrites); apiApp.use(APIVersionCompatibilityService.contentVersion); apiApp.use(middleware.maxLimitCap); // 对 limit 参数施加上限 apiApp.lazyUse('/content/', require('./endpoints/content/app')); apiApp.lazyUse('/admin/', require('./endpoints/admin/app')); apiApp.use(errorHandler.resourceNotFound); apiApp.use(errorHandler.handleJSONResponse(sentry));这里可以印证文档对 API 框架管线的描述:HTTP 路由进入后先做API 版本兼容处理(version rewrites),再经过请求校验、鉴权与权限判定,然后执行 endpoint,最后序列化响应;版本重写与统一 JSON 错误处理都在这一层完成。domain 与集成逻辑被委托给 ghost/core/core/server/services 下的服务,而模型与数据访问仍然留在 ghost/core/core/server 中。
文档特别提醒了一个现实约束:代码库在渐进式演进中,既有 service 并不都使用同一种构造方式、依赖注入或导出模式。因此扩展成熟模块时请"跟随邻近 service 的写法";若要新建独立 service,则应遵循 services/README.md 中的指南。
服务初始化归属:Boot 序列拥有最终决定权
架构文档强调:服务初始化由 Ghost 的 Boot 序列负责——凡是监听事件、调度任务或持有资源的服务,必须在恰当的启动阶段被初始化,而不是在首个请求时才懒加载。
这在 ghost/core/core/boot.js 中体现得相当彻底:initCore()启动期间校验 adapter、初始化 adapterManager、limits、settings、i18n、jobs 等基础服务;initServicesForFrontend()初始化 route settings、custom redirects、themes、offers;initServices()则通过Promise.all并发初始化 stripe、members、tiers、webhooks、email、comments、staff、mentions 等 40 余个服务。例如初始化 Stripe 的同时就把 shutdown 清理任务注册给ghostServer(boot.js L392-L396)。
启动完成后,后台任务进入initBackgroundServices():加载非活跃主题、恢复被中断的 newsletter 发送与 gift 投递、调度 token 清理类周期任务、初始化 ActivityPub 与 email analytics 的周期性任务等。注意这些任务不需要被 await——"起跑但不等待",因此 boot 主链路不被打断。
Frontend 应用:公开站点、主题与进程内代理
站点应用的组装顺序
frontend 分支的装配见 ghost/core/core/server/web/parent/frontend.js,把/members、/webmentions、/gift与根路径/挂到同一前端应用下。其中公开站点部分由 ghost/core/core/frontend/web/site.js 构建,其执行顺序与文档描述高度吻合:
- 静态资源与存储媒体:favicon、sitemap、公开文件,以及基于 storage adapter 提供的站点图片/媒体/文件(site.js L99-L107);
- 成员会话:
membersService.middleware.loadMemberSession确保访问公开站点的 member 会话被正确加载(site.js L144-L145); - 主题渲染:
themeEngine.middleware与后续的siteRoutes将页面请求交给 Ghost 的动态路由与当前激活主题,主题模板在服务端用 Handlebars 渲染(视图引擎view engine被设置为hbs,见 site.js L47)。
也就是说,公开站点的页面 HTML 由服务端生成,这与纯客户端渲染(CSR)的门户类应用有本质区别。
进程内代理而非 HTTP 回环
文档对"frontend helpers 与路由通过一个内部 proxy 模块访问服务端 API、settings、URL 生成等能力"做了专门澄清:这是进程内边界,而不是发往 Content API 的一次 HTTP 请求。这个模块真实存在于 ghost/core/core/frontend/services/proxy.js,它把主题 helper 对"取服务端 API、设置缓存、URL 生成"等能力的访问统一收敛到一个入口,既在源码树上保留了前后端界限,又避免了无谓的进程内 HTTP 回环开销。
动态路由、主题热更新与 bridge
routes.yaml、激活主题与站点设置共同决定公开 URL 如何被解析和渲染。文档强调routing 可以在 Ghost 运行期间被重载。对应机制有两处:
- 站点路由本身被封装为一个可替换的
SiteRouter,module.exports.reload会在不重启进程的情况下用新的siteRoutes(routerConfig)替换旧路由(site.js L218-L225); - ghost/core/core/bridge.js 承担服务端与 frontend 代码之间剩余的显式通信,包括主题与路由更新事件的下发。
一个值得注意的启动细节:动态路由即使在 backend-only 启动时也会被初始化(boot.js L282-L312 中的注释明确说明了原因)——因为 API、email 服务与 webhook 需要借助路由来构建公开 URL;如果 backend-only 启动跳过这一步,所有资源都会被解析成/404/。
浏览器应用:独立的构建与部署单元
Admin:React 与 Ember 并存
文档把 Admin 描述为"当前同时包含 React 应用与回退到历史 Ember 应用的路由"的状态。Admin 浏览器应用位于 apps/admin(React)与 apps/ember-admin(历史 Ember)目录,二者都通过Admin API(/ghost/api/admin/)取数。新的 Admin 功能以 React 为主,基于admin-x-framework(见 apps/admin-x-framework)与 Shade 设计系统构建;新旧边界的最新说明见 apps/admin/README.md。
公开浏览器应用通过主题 helper 注入
Portal(会员门户)、Comments(评论)、Search、Signup Form、Announcement Bar(公告栏)与 Admin Toolbar(工具栏)是另外一组独立浏览器应用。Ghost 通过主题 helper把它们的脚本配置注入公开页面,应用在运行时使用 Ghost 的公开 HTTP 接口(如/ghost/api/content/)工作。这意味着这些应用甚至不需要了解 Ghost Core 的内部实现——它们只在浏览器里通过公开协议与站点通信。
发布节奏解耦是硬约束
文档给出了架构层面最重要的工程纪律:
- 这些浏览器应用并非全部随 Ghost Core 一起发布——Admin 可以早于某个服务端版本上线,公开应用(Portal、Comments 等)也能独立发版;
- 因此,跨越浏览器应用与 Ghost Core 的代码,绝不能假设两边会同时变更;
- 当前的发布路径与节奏详见 shipping 指南。
当一项功能跨越多层边界时
文档最后给出了一个非常有实操价值的功能落地清单。一个功能可能同时涉及运行时的多个部分:
- schema 与数据访问(在 Ghost Core)
- 服务端的领域行为(Server Service)
- Content 或 Admin API 端点
- React Admin UI
- 公开主题渲染或某个公开浏览器应用
- 变更后触发的事件、任务、email 或 webhook
针对这六类组成,文档给出的两条工程原则值得开发者长期遵守:
- 把领域行为保留在服务端,不要复制到 HTTP 路由或浏览器应用里。领域逻辑应该只有一个事实来源,HTTP 路由与前端只做编排与呈现;
- 把每个 HTTP 边界与部署边界都当作兼容性边界,并为每类行为在"离它最近的层"补测试。测试放得越贴近行为发生地,跨层重构时的回归信号就越精准。
这两条原则与上文所述的"共享进程、独立发布"的运行时现实互为因果:正因为 backend/frontend 共享进程却不共享发布周期,领域逻辑才必须内聚在服务端,而所有 HTTP/部署边界都需要被当作稳定的契约来维护。
小结
理解 Ghost 运行时架构,核心是把握三个层次的事实:单进程内的 Ghost Core 由"共享父应用 + backend + frontend"组成,它们共享配置、数据库与 Boot 生命周期,只是代码边界;进程外的 Admin 与各类公开应用通过 HTTP 与 Core 的 Content/Admin API 集成;发布维度上 Core、Admin 与公开应用各有节奏,因此把领域行为收拢到服务端、把每个边界当作兼容性契约,是扩展 Ghost 时最值得坚持的两条准则。要按目录逐层浏览代码,可继续阅读 monorepo 结构指南;对文中涉及的各服务(jobs、caching、authentication 等)的深入说明,可分别查阅 docs/codebase 下的对应文档。
【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考