混合架构总览:C++23 后端与 WebView2/Vue3 前端
SpinningMomo(旋转吧大喵)采用"原生 Win32 C++23 后端 + 嵌入式 WebView2 承载 Vue3 前端"的混合架构,两进程之间通过 JSON-RPC 2.0 通信。本页是整个架构层级的总览入口,说明分层、通信契约与初始化时序。
目的与范围(Purpose and Scope)
本页覆盖以下内容:
- 两进程模型:C++ 后端与 Web 前端的边界、运行形态(WebView2 内嵌 vs 浏览器开发模式)
- 双传输层:WebView bridge(生产)与 HTTP + SSE(开发,uWebSockets
51206端口) - 后端分层:
core::*、features::*、ui::*、utils::*、extensions::*、vendor/**的职责划分 - 设计哲学:POD 结构体 + 自由函数、集中式
AppState、std::expected错误处理、Asio 协程 - 前端结构:
web/下 Vue 3 + TypeScript + Pinia 的目录组织 - 初始化顺序与新增功能的接入路径
以下内容由兄弟页面承接,本页只做指路:
- 各具体子系统(gallery、recording、screenshot、overlay 等
features::*模块)的设计细节 → 参见各自专题页 - Android 捕获守护进程(
android/capture,momo-capture)→ 参见其专属页面 - 安装器构建(
installer/、WiX/MSI)与发布流程 → 参见构建与发布相关页面 - 数据库迁移系统与
DataMapper细节 → 参见数据库专题页
概述(Overview)
SpinningMomo 是一个 Windows-only 桌面工具,围绕《无限暖暖》游戏窗口提供拍照、截图、录制与相关工作流能力。仓库形态是"原生 Win32 C++ 应用 + 嵌入式 Web 前端",代码库为双语(注释与 UI 字符串以中文为主)。
这套混合架构的核心动机是:
- 原生能力:屏幕捕获、窗口控制、托盘、悬浮窗、全局热键等必须依赖 Win32 / D3D / 音视频编码等原生 API,必须由 C++ 承担;
- UI 生产力:相册、设置、地图等复杂界面用 Vue 3 + TypeScript + Tailwind 开发效率远高于原生 Win32 控件;
- 一套代码、两种运行形态:同一份 Vue 前端既能嵌入 WebView2 随应用分发(生产),也能在开发期直接跑在普通浏览器里通过 HTTP + SSE 连接后端,便于热重载与 DevTools 调试。
前后端之间的唯一契约是 JSON-RPC 2.0:请求/响应字段在 C++ 侧为 snake_case,在 JSON 侧自动转换为 camelCase(由 reflect-cpp 完成反/序列化),保证两侧命名习惯各自自然。
架构(Architecture)
图解说明:
- 前端层(
web/):Vue 3 应用按 feature 目录组织(gallery、settings、home、about、map、onboarding、common、playground),所有后端访问收敛到web/src/core/rpc/客户端。web/src/core/env/在启动时探测window.chrome.webview是否存在,据此选择传输层——这使同一构建产物既能跑在 WebView2 内,也能跑在普通浏览器里。 - 传输层:两条等价通道都讲 JSON-RPC 2.0。生产走 WebView2 宿主桥;开发走 uWebSockets 监听的
localhost:51206,其中 SSE(Server-Sent Events)承担服务器到客户端的推送(弥补 HTTP 请求/响应模型无法主动通知的缺口)。 - 后端层(
src/):端点按领域分布在src/core/rpc/endpoints/<domain>/,每个领域暴露register_all(state),由registry.cpp统一装配。所有可变状态集中于core::AppState,各层以自由函数 +AppState&的方式读写。 - 辅助表面:
android/是独立的 Java 捕获守护进程(不参与本页的 RPC 架构);playground/是 Node/TS 编写的后端 HTTP/RPC 调试脚本;docs/是独立 VitePress 站点,不进入运行时产物。
后端分层详解
后端目录按命名空间划分职责,边界清晰(详见 AGENTS.md):
| 命名空间 | 目录 | 职责 |
|---|---|---|
core::* | src/core/ | 框架基础设施:async 协程运行时、database、events 事件总线、http client/server、rpc、webview、i18n、commands、migration、worker pool、tasks、runtime info、shutdown、state |
features::* | src/features/ | 业务逻辑:adb_mode、gallery、letterbox、notifications、overlay、preview、recording、screenshot、settings、update、window_control 等 |
ui::* | src/ui/ | 原生 Win32 UI:floating_window、tray_icon、context_menu、webview_window |
utils::* | src/utils/ | 共享工具:logger、file、graphics、image、media、path、string、system、throttle、timer、dialog、crash_dump、crypto |
extensions::* | src/extensions/ | 游戏专属集成(如 infinity_nikki) |
vendor/** | src/vendor/ | 项目自有的 include 门面,对应标准库、Win32 SDK 与第三方头文件 |
vendor 门面约束:只有 src/vendor/ 内部允许出现外部尖括号 include。src/vendor/windows/ 下的 SDK 门面与物理 SDK 头文件一一对应,不允许创建"领域聚合门面"。稳定且高频的精确门面才进入 src/pch.hpp(预编译头,仅加速编译,不改变语义),低频新依赖保留在使用点本地。每个项目头文件必须在不带 PCH 的前提下自包含。
RPC 端点组织
端点位于 src/core/rpc/endpoints/<domain>/,每个领域实现 register_all(state),由 registry.cpp 统一调用装配。游戏专属适配器放在 src/extensions/,经由 rpc/endpoints/extensions/ 暴露。注册使用模板形式 core::rpc::register_method<Req, Res>(),依赖 reflect-cpp 完成结构体与 JSON 的双向映射。
设计哲学:为什么不是 OOP 类层次
后端刻意不使用 OOP 继承体系,而是(见 AGENTS.md):
- POD 结构体 + 自由函数:普通数据结构与操作它们的自由函数;
- 集中式状态:所有状态放在
AppState,以引用传递; - Usecase 编排:
usecase.hpp/.cpp是特性级的顶层编排层,可跨core::*、UI、extensions 协调调用; - 工作流归属:模块间可复用彼此的公共能力,但跨模块工作流编排必须放在 usecase 层,防止 feature 之间形成网状耦合。
这一决策的动机:桌面工具的生命周期是单进程、长驻、以状态为中心的。集中状态 + 自由函数让调用链显式、可 grep、无虚函数分发开销,也让 std::expected 风格的错误传播更自然。
核心流程(Core Flow)
双传输层选择与一次 RPC 调用
步骤解读:
- 前端启动即做环境探测:
window.chrome.webview存在则说明运行在 WebView2 宿主内,走原生桥;否则是浏览器开发模式,走 HTTP。Vite 开发服务器会把/rpc与/static代理到后端localhost:51206。 - RPC 处理器返回
asio::awaitable<RpcResult<T>>——异步以协程表达而非回调,配合core::async运行时。 - 事件总线
core::events提供同步send()与异步post();异步路径通过PostMessageW唤醒 Win32 消息循环,保证 UI 线程亲和性。 - SSE 通道只在开发形态下承担服务端→客户端推送;生产形态下推送经由 WebView 桥反向到达前端。
应用初始化顺序
初始化遵循 main.cpp → Application::Initialize() → core::initializer::initialize_application(),大致顺序为:核心基础设施 → 原生 UI → 特性服务 → extensions 与启动任务。这一顺序保证 AppState 中被依赖的子系统先于依赖者就绪(如事件总线、数据库先于任何 feature 初始化)。
前端结构(web/)
技术栈:Vue 3 + TypeScript + Pinia + Tailwind CSS v4 + shadcn-vue/reka-ui,Vite 工具链构建(见 AGENTS.md):
| 目录 | 职责 |
|---|---|
web/src/core/rpc/ | JSON-RPC 客户端,含 WebView 与 HTTP 两种传输实现 |
web/src/core/i18n/ | 客户端国际化 |
web/src/core/env/ | 运行环境检测 |
web/src/core/tasks/ | 前端任务编排 |
web/src/features/ | 业务模块:gallery、settings、home、about、map、onboarding、common、playground |
web/src/composables/ | 共享组合式函数:useRpc、useI18n、useToast |
web/src/extensions/ | 游戏专属集成(infinity_nikki) |
web/src/router/ | 路由 |
web/src/types/ | 共享 TS 类型 |
web/src/lib/ | 共享 UI/辅助函数 |
前后端在两侧保持镜像的分层心智模型:src/features/<name> ↔ web/src/features/<name>,src/extensions/<game> ↔ web/src/extensions/<game>,使得新增能力时两侧改动路径可预测。
关键模式与横切关注点
- 错误处理:全程使用
std::expected<T, std::string>,不使用异常做控制流。错误以值的形式沿调用链显式传播,与 RPC 层的RpcResult语义对齐。 - 异步:基于 Asio 的协程运行时(
core::async);RPC 处理器统一返回asio::awaitable<RpcResult<T>>,ui_awaitable等设施桥接协程与 UI 线程(src/core/async/ui_awaitable.hpp)。 - 命名转换:C++ 字段
snake_case↔ JSONcamelCase,由 reflect-ccpp 在(反)序列化时自动完成,前端 TS 类型与后端结构体无需手工对齐命名风格。 - 命令注册表:
core::commands将动作、切换状态、i18n key、可选热键绑定在一起;右键菜单与托盘图标均由该注册表驱动,避免菜单逻辑散落。 - 数据库:SQLite(SQLiteCpp 封装),线程局部连接避免跨线程共享;
DataMapper提供 ORM 风格行映射;迁移系统由scripts/generate-migrations.js自动生成。 - 字符串编码:内部统一 UTF-8(
std::string),Win32 API 边界用 UTF-16(std::wstring),经由utils::string转换。这一约定消除了 Win32 开发中最常见的编码事故面。
配置与构建产物
构建命令
| 命令 | 用途 |
|---|---|
pnpm run build:web | 构建 Web 前端(web/,Vite 工具链) |
node scripts/build-installer.js(或 pnpm run build:installer) | 构建 MSI 与 WiX bundle 安装包到 dist/;--msi-only 跳过 bundle,--version X.Y.Z 覆盖 version.json |
node scripts/generate-migrations.js | 修改 src/migrations/*.sql 后必须重跑 |
node scripts/generate-embedded-locales.js | 修改 src/locales/*.json(zh-CN / en-US)后必须重跑 |
node scripts/generate-map-injection-cpp.js | 修改 web/src/features/map/injection/source/*.js 后重跑(生成压缩 JS 及其 C++ 头) |
(来源:AGENTS.md)
产物路径
| 产物 | 路径 |
|---|---|
| Release 构建 | build\windows\x64\release\ |
| Debug 构建 | build\windows\x64\debug\ |
| Android 守护进程 | build\android\momo-capture.jar |
| 分发包 | dist/(exe + web 资源) |
开发期端口约定
- 后端 HTTP/RPC:
localhost:51206(uWebSockets) - Vite 开发服务器将
/rpc与/static代理到上述端口
新增功能的接入路径(扩展点)
混合架构下新增一个功能需在两侧同时落点,路径是固定的(见 AGENTS.md):
- 在
src/features/<name>/下创建目录,.hpp为接口、.cpp为实现; - 在
<name>/state.hpp中定义状态结构体(命名空间features::<name>),并注册进core::AppState; - 在
src/core/rpc/endpoints/<name>/下新增端点文件,实现register_all(state),并在registry.cpp中接线; - 若需要热键/菜单项,在
src/core/commands/builtin.cpp注册命令; - 若需要初始化逻辑,加入
core::initializer::initialize_application; - Web 侧在
web/src/features/<name>/下创建api.ts、store/index.ts、types.ts、组件与页面。
这个清单本身就是架构的"接线图":状态必须进 AppState、RPC 必须经 registry.cpp、命令必须经 commands 注册表——三条汇聚点保证了集中式状态与统一装配的不变量。
测试策略
- 场景测试(TypeScript):位于
tests/scenarios/(pnpm run test:scenarios),对编译后的SpinningMomo.exe在隔离的便携沙箱中通过 JSON-RPC 做端到端验证。测试前需关闭运行中的应用实例;默认门槛是 Release 构建(内容哈希语义只在 Release 下成立),因此先执行xmake release;可用--exe=<path>或SPINNING_MOMO_EXE指向其他构建。 - 单元测试(C++):遗留 doctest 单元测试位于
tests/(xmake test)。 - 交互式调试:交互式 RPC 测试工具位于
web/src/features/playground/与根目录playground/。
场景测试直接打到 JSON-RPC 契约这一层,恰好验证了混合架构的边界稳定性——测试无需启动 WebView 前端即可覆盖后端全部行为。
失败模式与边界情况
- 传输层降级:前端依据
window.chrome.webview探测选择传输,同一构建在两种环境间无需配置切换。开发模式下若后端未起在51206,前端/rpc代理会失败——表现为 RPC 超时而非崩溃。 - 错误传播:后端不抛异常,错误以
std::expected<T, std::string>值语义传播并映射到 RPC 结果;前端按 TS 类型解析失败分支。 - 线程边界:SQLite 使用线程局部连接规避跨线程共享;事件总线异步
post()通过PostMessageW回到 Win32 消息循环,避免 UI 线程外直接触碰 UI。 - 编码边界:UTF-8(内部)与 UTF-16(Win32 API)之间的转换集中在
utils::string,边界清晰可审计。 - 构建耦合:迁移、内嵌语言包、地图注入三处代码生成物必须随源文件重跑生成脚本,否则出现产物与源不一致的隐性漂移。
相关链接
- 源码指引文档:AGENTS.md
- 子系统 README(按需阅读):
- src/features/gallery/README.md — 资产身份、元数据继承、30 天缺失生命周期
- src/core/state/README.md —
AppState布局与 API 依赖约定 - src/extensions/infinity_nikki/README.md — 媒体硬链接镜像与任务编排
- android/capture/README.md — momo-capture 守护进程架构