2026-09-23·by Sijie Wang#lucerna#software#architecture

cross-platform-seams

跨平台注册表接缝(多仓库架构因此存在)

上级:key-designs

@lucerna/core 从不导入某个 shell,却同时驱动着一个 Next.js Web 应用和一个 Electron 桌面应用。其机制是三个模块级的注册表接缝(registry seam),各自由所属的 shell 在启动时设置一次,并通过一个惰性代理来读取,因此导入顺序无关紧要。

接缝一——平台适配器core/src/platform/index.ts)。PlatformAdapter = storage(键值存储)+ navigate(path) + origin + setThemebuildDefaultAdapter() 具备浏览器感知能力(若存在 window 则接入 localStorage/location/documentElement,在 SSR/测试环境下则全部空操作,以免模块初始化时的读取动作导致崩溃)。setPlatformAdapter(impl) 用来替换它;platform 是一个 getter 代理,每次访问都会重新读取当前生效的适配器。

接缝二——仓库层的依赖反转(最要紧的一个)。接口定义在 core/src/domain/repositories/ 中;该目录的桶文件(barrel)本身就是 DI 容器:setRepositories(partial) 把各实现合并进一个 registered 映射表,resolve(key) 在未注册时会抛出 "core: ${key}Repository not registered"(失败即报错,没有任何回退),而 lazy<T>(key) 返回一个 Proxy,它解析出当前生效的实现,并在每次调用时把方法绑定到该实现上。Core 端只需要调用 getVocabularyRepository().create(...),完全不知道底层走的是 HTTP 还是 SQLite。(如实记下的一笔债:HttpError 之所以要从 domain 的桶文件里重新导出,是因为用例代码需要根据 err.status === 401 分支处理——干净的修法已经被承认,但还没有落地。)

接缝三——domain 包里的 zustand storecore/src/state/)。与 UI 无关的 store 通过 platform.storage(而不是直接用 localStorage)来做持久化,因此同一个 store 在 Electron、SSR 和测试环境下都能正常工作;React 一侧的 shell 把它们当作普通的 hook 来消费即可。

同一个仓库接口,两套实现。 VocabularyRepository(core 中的接口)→

  • webHttpVocabularyRepository——对 vocabularyApi.*纯委托,不持有任何本地状态;
  • desktopHybridVocabularyRepository——本地 SQLite 加 outbox 包装器(offline-first-outbox)。 每个 shell 的 coreBootstrap.ts 都会组装出自己的实现集合,并调用一次 setRepositories(...)(web → 10 个 HTTP 委托;desktop → 7 个 hybrid 加 3 个 HTTP)。

web 与 desktop 之间真正的差异(存储方式之外):认证传输方式(web 用会话 cookie,desktop 用 Bearer token,因为 webview 会丢弃跨站的 Set-Cookie);导航方式(web 用整页的 location.href 刷新,desktop 用 SPA 式的 pushState,以便在认证事件前后保留 sqlite 句柄与 outbox);语言区域(web 走 URL 前缀 /en/…,desktop 则只存在 store 里);以及 Electron 独有的 main process(better-sqlite3、操作系统密钥库、自动更新、本地 TTS)——浏览器端根本没有与之对应的东西。

这就是整个多仓库架构存在的原因:这些接缝就是那份契约,正是它让同一个 domain core 能够服务于两种截然不同的运行时。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →