Repository Wiki
ChanIok/SpinningMomo

混合架构总览: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 字符串以中文为主)。

这套混合架构的核心动机是:

  1. 原生能力:屏幕捕获、窗口控制、托盘、悬浮窗、全局热键等必须依赖 Win32 / D3D / 音视频编码等原生 API,必须由 C++ 承担;
  2. UI 生产力:相册、设置、地图等复杂界面用 Vue 3 + TypeScript + Tailwind 开发效率远高于原生 Win32 控件;
  3. 一套代码、两种运行形态:同一份 Vue 前端既能嵌入 WebView2 随应用分发(生产),也能在开发期直接跑在普通浏览器里通过 HTTP + SSE 连接后端,便于热重载与 DevTools 调试。

前后端之间的唯一契约是 JSON-RPC 2.0:请求/响应字段在 C++ 侧为 snake_case,在 JSON 侧自动转换为 camelCase(由 reflect-cpp 完成反/序列化),保证两侧命名习惯各自自然。

架构(Architecture)

Loading diagram...

图解说明:

  • 前端层(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 调用

Loading diagram...

步骤解读:

  1. 前端启动即做环境探测:window.chrome.webview 存在则说明运行在 WebView2 宿主内,走原生桥;否则是浏览器开发模式,走 HTTP。Vite 开发服务器会把 /rpc 与 /static 代理到后端 localhost:51206。
  2. RPC 处理器返回 asio::awaitable<RpcResult<T>>——异步以协程表达而非回调,配合 core::async 运行时。
  3. 事件总线 core::events 提供同步 send() 与异步 post();异步路径通过 PostMessageW 唤醒 Win32 消息循环,保证 UI 线程亲和性。
  4. 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 ↔ JSON camelCase,由 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):

  1. 在 src/features/<name>/ 下创建目录,.hpp 为接口、.cpp 为实现;
  2. 在 <name>/state.hpp 中定义状态结构体(命名空间 features::<name>),并注册进 core::AppState;
  3. 在 src/core/rpc/endpoints/<name>/ 下新增端点文件,实现 register_all(state),并在 registry.cpp 中接线;
  4. 若需要热键/菜单项,在 src/core/commands/builtin.cpp 注册命令;
  5. 若需要初始化逻辑,加入 core::initializer::initialize_application;
  6. 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,边界清晰可审计。
  • 构建耦合:迁移、内嵌语言包、地图注入三处代码生成物必须随源文件重跑生成脚本,否则出现产物与源不一致的隐性漂移。

相关链接

Sources

(1 files)