拆解ZenNotes的Monorepo架构一套React核心如何同时跑在桌面、Web与Go服务器三种运行时【免费下载链接】zennotesKeyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration.项目地址: https://gitcode.com/gh_mirrors/zenn/zennotesZenNotes 是一个键盘优先的本地 Markdown 笔记应用支持 Vim 键位、图表绘制与 MCP 集成。它的 Monorepo 架构最值得学习的一点是桌面、Web、自托管服务器三种运行时共享同一套 React 产品核心而运行时差异全部被封装在一个类型化的“桥接契约”背后。下面用仓库内的真实目录结构拆解这套架构的设计思路。为什么需要 Monorepo 架构先说结论ZenNotes 的核心难题不是“写三套前端”而是“同一套 UI 如何同时信任三种宿主”。桌面版跑在 Electron 里靠 IPC 调用 Node APIWeb 版跑在浏览器里靠 HTTP/WebSocket 调用 Go 服务器而未来的托管模式还要在同一套栈上叠加认证与存储层。如果按传统方式开发编辑器的行为会在三个运行时里逐渐漂移。ZenNotes 的解法是把产品行为收敛到一个包把运行时差异收敛到一层接口。仓库布局apps 是壳packages 是核心整个仓库只有两大顶层分区规则非常克制详见 docs/monorepo-architecture.md目录职责类比apps/desktop/Electron 壳窗口、菜单、preload、自动更新、打包桌面宿主的“躯壳”apps/web/Vite/PWA 壳 HTTP 桥接浏览器宿主的“躯壳”apps/share-viewer/打包给 Laravel 的只读公开渲染器分享场景的独立工件packages/app-core/共享 React 应用与渲染逻辑唯一事实来源产品的“大脑”packages/bridge-contract/UI 与宿主之间的类型化运行时契约大脑与躯壳之间的“神经接口”packages/shared-domain/可移植的领域函数与兼容类型导出共享业务规则packages/shared-ui/预留工作区当前为空导出留白一条铁律贯穿始终packages/app-core是面向用户功能的唯一事实来源。任何“桌面和浏览器应该表现一致”的功能都该写在这里平台相关的代码窗口、更新器、打包留在apps/壳里绝不渗入核心。依赖方向也是单向的app-core→shared-domain和bridge-contract→ 只依赖标准浏览器类型永不依赖任何具体实现或 Node 全局对象。桥接契约三种运行时的灵魂所在这套架构最精妙的部分是packages/bridge-contract定义的ZenBridge接口。共享 UI 从不直接调用 Electron API也不直接发 fetch 请求——它只依赖桥接接口。每个运行时在启动时“安装”自己的实现Electron preload安装桌面桥接IPC 实现暴露约 60 个方法的window.zen对象Web 客户端安装 HTTP 桥接由 Go 服务器支撑形状与桌面版完全一致移动端仓库安装各自的 Native 适配器。在 http-bridge.ts 的注释里作者把这件事说得直白Web 客户端需要一个与 preload 对象“形状完全相同”的对象只是底层从 Electron IPC 换成对 Go 服务器的 HTTP 调用——“替换这个对象是让app-core里所有 UI 组件零改动继续工作的唯一改动”。契约覆盖的能力面相当完整笔记与文件夹 CRUD、搜索/任务/归档/回收站/标签、资产操作、watcher 订阅事件、更新状态以及一份ZenCapabilities能力标志表见 bridge.ts——supportsUpdater、supportsNativeMenus、supportsFloatingWindows等布尔值告诉 UI 哪些平台能力不存在。缺失的能力不会让界面崩溃只是安静地隐藏对应入口。Go 服务器嵌入式工件而非第三套前端第三个运行时的设计值得单独说。Go 自托管服务器当前正在迁移到独立的znserver仓库不重写任何前端它通过go:embed嵌入本仓库发布的浏览器工件。具体流程是npm run artifact:web产出一个校验过的不可变 Web 归档由 pack-web-artifact.mjs 驱动Go 侧的cmd/prepare-web在embed_web构建前校验 manifest 与资产开发期则由 server-release.json 钉住的发布版本、或ZENNOTES_SERVER_DIR指向的本地 checkout 来启动npm run dev:server。这带来一个优雅的结果服务器二进制本身就是完整应用——没有外部public/目录没有独立 CDN 步骤客户端版本与服务器版本天然锁死不存在漂移。仓库内的性能脚本如 perf-web-runtime.mjs直接在这个钉住的服务器上跑真实负载。从契约到发布工具链闭环tooling/scripts/目录把三种运行时串成可验证的闭环几个值得看的脚本脚本作用pack-app-core.mjs把 app-core 打成精确版本归档供移动端仓库按版本引用sync-contract-fixtures.mjs同步 bridge-contract/fixtures/ 里的契约夹具CI 用check:contract-fixtures防漂移dev-web-stack.mjs一条命令拉起“钉住的 Go 服务器 Web 壳”的联调栈verify-release-channels.mjs校验发布通道配置根 package.json 里的npm run build:prodtypecheck → test → build 全量流水线由 turbo 编排则保证了任何一次合入都必须同时通过桌面与 Web 侧的类型检查与测试——这正是 Monorepo 相对多仓库最大的一致性红利。给新手的三个可迁移经验契约先行实现可换。UI 只依赖ZenBridge接口而非任何宿主 API加一个新运行时比如未来的浏览器托管模式是“写一个新适配器”而不是“再写一套前端”。依赖方向用编译器强制。bridge-contract 用noResolve 空types列表封闭了自己的源码集契约层想“偷看”实现层都会被类型检查拦下——边界靠工具保证不靠自觉。行为一致性靠共享夹具不靠人肉同步。Go 服务器与 TypeScript 的 vault 逻辑被同一批测试夹具验证契约夹具由脚本在 CI 中自动同步漂移在合并前就暴露。延伸阅读官方架构说明docs/monorepo-architecture.md运行时与包映射参考docs/reference/runtime-and-package-map.mdWeb 与自托管设计文档docs/web-architecture.md生态边界与仓库拆分计划docs/specs/ecosystem-boundaries-and-repository-plan.md核心应用包packages/app-core/桥接契约源码packages/bridge-contract/src/这套架构的本质可以浓缩成一句话React 核心是产品壳是运行时契约是它们之间唯一的方言。想动手研究的话从 http-bridge.ts 的注释读起十分钟内就能建立完整心智模型。【免费下载链接】zennotesKeyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration.项目地址: https://gitcode.com/gh_mirrors/zenn/zennotes创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考