一套 Rust 核心,跑通 Tauri + React Native

一篇写给"想做跨端产品,但不想把业务逻辑在每个平台重写一遍"的工程笔记。主角是 SwarmNote:桌面端用 Tauri + React,移动端用 Expo + React Native,底层共享同一份 Rust 核心。

SwarmNote: Your notes, swarming across your own devices.

先说结论

SwarmNote 的跨端方案不是"桌面一套、手机一套、业务逻辑靠复制粘贴同步",而是把系统拆成三层:

  1. 产品界面层 :桌面端是 React + Tauri WebView,移动端是 Expo + React Native
  2. 平台适配层 :桌面端用 #[tauri::command] 暴露能力,移动端用 uniffi-bindgen-react-native 生成 JSI/Turbo Module。
  3. 共享核心层swarmnote-core 是平台无关的 Rust crate,负责工作区、文档、Yjs/yrs 状态、P2P 配对和同步。

换句话说,桌面和移动不是两个产品,而是同一个 Rust 核心外面套了两个不同的壳。

目录

  • [为什么不是"一套 Web 跑所有端"](#为什么不是“一套 Web 跑所有端” "#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%98%AF%E4%B8%80%E5%A5%97-web-%E8%B7%91%E6%89%80%E6%9C%89%E7%AB%AF")
  • [故事其实从 SwarmDrop 开始](#故事其实从 SwarmDrop 开始 "#%E6%95%85%E4%BA%8B%E5%85%B6%E5%AE%9E%E4%BB%8E-swarmdrop-%E5%BC%80%E5%A7%8B")
  • [Tauri v2 mobile 卡在哪里](#Tauri v2 mobile 卡在哪里 "#tauri-v2-mobile-%E5%8D%A1%E5%9C%A8%E5%93%AA%E9%87%8C")
  • [桌面端:Tauri 负责"系统壳"](#桌面端:Tauri 负责“系统壳” "#%E6%A1%8C%E9%9D%A2%E7%AB%AFtauri-%E8%B4%9F%E8%B4%A3%E7%B3%BB%E7%BB%9F%E5%A3%B3")
  • [移动端:RN 负责"手机体验"](#移动端:RN 负责“手机体验” "#%E7%A7%BB%E5%8A%A8%E7%AB%AFrn-%E8%B4%9F%E8%B4%A3%E6%89%8B%E6%9C%BA%E4%BD%93%E9%AA%8C")
  • [编辑器:为什么移动端还需要 WebView](#编辑器:为什么移动端还需要 WebView "#%E7%BC%96%E8%BE%91%E5%99%A8%E4%B8%BA%E4%BB%80%E4%B9%88%E7%A7%BB%E5%8A%A8%E7%AB%AF%E8%BF%98%E9%9C%80%E8%A6%81-webview")
  • 顺手把编辑器也产品化
  • [真正的核心:把平台差异变成 trait](#真正的核心:把平台差异变成 trait "#%E7%9C%9F%E6%AD%A3%E7%9A%84%E6%A0%B8%E5%BF%83%E6%8A%8A%E5%B9%B3%E5%8F%B0%E5%B7%AE%E5%BC%82%E5%8F%98%E6%88%90-trait")
  • [P2P:SwarmNote 为什么需要 Rust core](#P2P:SwarmNote 为什么需要 Rust core "#p2pswarmnote-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81-rust-core")
  • 一次"打开文档"的完整链路
  • 如果你也想用这套方案
flowchart TB subgraph Desktop["桌面端:SwarmNote"] React["React 19 + TypeScript
CodeMirror 6 · Zustand · TanStack Router"] Tauri["Tauri 2 Host
Commands · Events · Tray · Updater"] end subgraph Mobile["移动端:SwarmNote Mobile"] RN["Expo + React Native
NativeWind · Expo Router · Zustand"] UniFFI["UniFFI / JSI Turbo Module
生成 TS + C++ 绑定"] WV["WebView Editor
CodeMirror 6 + Comlink"] end Core["swarmnote-core
Workspace · Document CRUD · YDocManager · Pairing"] P2P["swarm-p2p-core
libp2p · mDNS · DHT · DCUtR · Relay · GossipSub"] DB[("SQLite
workspace.db · devices.db")] React --> Tauri --> Core RN --> UniFFI --> Core RN --> WV WV -. "Yjs update bytes" .-> RN Core --> P2P Core --> DB style Core fill:#fff4cc,stroke:#d97706,stroke-width:2px style P2P fill:#e8f4ff,stroke:#208aef

为什么不是"一套 Web 跑所有端"

一开始我也很想要一个特别漂亮的答案:Tauri v2 既能做桌面,也能做移动,那是不是直接一套 Web + Rust 走到底就好了?

这个想法很诱人。Web 技术写 UI,Rust 写核心,桌面包体小,系统能力强;到了移动端,理论上也只是把 Tauri 初始化到 Android / iOS 项目里。听起来像是"跨端开发终于不用再做选择题了"。

但真正写到文件系统、系统分享、移动端权限、手势、键盘、安全区这些地方,事情就开始变得没有那么童话了。

维度 Tauri mobile React Native
移动端 UI 手感 WebView 为主,需要自己处理大量移动交互细节 原生视图,手势、导航、键盘、安全区更自然
生态 Tauri 插件 + Web 生态 Expo / RN 生态,移动能力覆盖更完整
Rust 调用 WebView IPC,JSON 序列化 JSI 直调 C++/Rust 绑定,类型生成
编辑器复用 很适合 Web 编辑器 需要 WebView 承载 CodeMirror
产品定位 "把 Web app 带到移动端"很快 "做一个真正手机 app"更顺

所以后来的方向变成:桌面继续用 Tauri,移动端改用 React Native,但 Rust 核心继续复用。

故事其实从 SwarmDrop 开始

SwarmNote 不是凭空长出来的架构。前面还有一个探路项目:SwarmDrop。

SwarmDrop 做的是 P2P 文件传输,可以理解成"跨网络版 LocalSend"。它很适合拿来验证几件硬骨头:

  • Rust + libp2p 在真实设备上能不能稳定跑
  • mDNS / DHT / Relay / DCUtR 这些发现和连通性方案怎么组合
  • 大文件传输、分片、进度、取消、恢复怎么做
  • Android 上文件选择、公共目录写入、SAF / MediaStore 怎么接

早期做移动端验证时,SwarmDrop 用过 Tauri mobile。这个阶段很重要,因为它证明了"Rust 核心上移动端"这条路是通的。Tauri 的 #[tauri::command]、event、channel 这套模型,对已经熟悉桌面端的人来说非常自然:前端 invoke(),后端 Rust async 处理,进度再推回前端。

但它也暴露了一个现实:能跑起来适合长期做移动产品 是两件事。

SwarmDrop 里最典型的痛点是文件系统。桌面端拿到路径后,很多事情就是 std::fs / tokio::fs。Android 上则不一样:用户选中的可能是 content:// URI,公共下载目录涉及 Scoped Storage,写入要绕 SAF 或 MediaStore,目录遍历、权限持久化、临时缓存、大文件流式读取都要单独处理。

这些不是 Tauri 的错,而是移动端系统本来就复杂。只是当时 Tauri mobile 生态还比较薄,遇到这种偏底层的移动文件系统需求,很难找到一个像 Expo/RN 生态里那样顺手、成体系、案例足够多的解决方案。最后就会变成:你名义上在写跨端 app,实际上在一边写 WebView UI,一边写 Android 原生插件,一边维护 Rust 传输层,一边补权限和生命周期胶水。

这个阶段给了 SwarmNote 一个很重要的教训:Rust 核心值得保留,但移动端的壳不一定非要继续用 Tauri。

Tauri v2 mobile 卡在哪里

这段需要说得公允一点:Tauri v2 mobile 是有价值的。官方已经把 Android / iOS 支持纳入 v2,也提供移动插件能力;插件可以用 Kotlin / Swift 写原生实现,再暴露给 WebView 前端。对很多"Web 产品加一点移动壳"的场景,它是很有吸引力的。

但对 SwarmNote / SwarmDrop 这种项目,几个限制会变得很明显:

问题 在项目里表现出来的影响
移动插件生态还不够厚 官方也明确说并非所有插件都支持移动端;遇到细分能力时,常常要自己写插件
文件系统不是"一个 API 解决所有平台" App 私有目录还好,公共目录、文件选择、SAF URI、MediaStore、大文件流式读写会迅速复杂
WebView UI 要自己补移动细节 键盘避让、安全区、手势导航、bottom sheet、触控反馈都要额外经营
移动端社区案例少 复杂问题搜索到的经验少,很多坑只能自己踩
调用链仍是 WebView IPC 对高频、类型复杂的核心调用来说,JSON IPC 不如 JSI 直调舒服

所以后面写 SwarmNote 移动端时,我换了一个问题问自己:

如果 Rust 核心已经证明可行,移动端为什么不直接用一个真正成熟的移动 UI 生态?

答案就是 React Native + Expo + UniFFI

一开始这只是一次尝试:RN 负责移动端体验,Rust 继续负责核心逻辑,中间用 uniffi-bindgen-react-native 接起来。结果有点出乎意料:它不是"退而求其次",反而把两边的长处都放大了。

  • RN/Expo 负责手机 app 该有的生态:导航、手势、文件选择、安全存储、图片、权限、系统集成
  • Rust 负责不该用 JS 重写的核心:P2P、CRDT、SQLite、同步协议、设备身份
  • UniFFI 把 Rust async API 映射成 TypeScript Promise,把事件映射成 callback interface
  • Hermes JSI 直调让这条桥比 WebView IPC 更类型化、更低摩擦

最后这条路线也反过来影响了 SwarmDrop。SwarmDrop 早期负责验证 libp2p、NAT 穿透、配对、传输等底层能力;SwarmNote 在此基础上把"共享 Rust 核心 + Host 适配层"整理成更清晰的模式;现在 SwarmDrop 也迁移到同一套架构:桌面端薄 Tauri host,移动端 Expo/RN host,中间共享 swarmdrop-core / swarm-p2p-core

timeline title Swarm 系列跨端架构时间线 section 技术验证 SwarmDrop 早期 : 用 Tauri mobile 验证 Rust + libp2p 在移动端可行 文件系统撞墙 : SAF / MediaStore / content URI 需要大量移动端胶水 P2P 核心沉淀 : 抽出 swarm-p2p-core,复用 mDNS / DHT / Relay / GossipSub section SwarmNote 桌面 MVP : Tauri 2 + React + Rust core 核心抽离 : swarmnote-core 去 Tauri 化,通过 trait 注入平台能力 移动端落地 : Expo + RN + UniFFI,复用同一份 Rust 业务核心 section 回流 SwarmDrop 迁移 : 采用同样的 Core / Desktop / Mobile 分层

桌面端:Tauri 负责"系统壳"

Tauri 在 SwarmNote 里不是业务核心,而是桌面 host。它做这些事:

  • 创建窗口、托盘、自动更新和系统通知
  • 把前端 invoke() 调用转成 Rust 命令
  • 把 Rust 事件通过 emit() 推给前端
  • 提供桌面端实现:Keychain、文件监听、窗口到工作区的映射

SwarmNote 的 src-tauri/src/lib.rs 里可以看到典型入口:注册插件、注册 commands,在 setup 阶段创建 AppCore,再把桌面专属能力注入进去。

rust 复制代码
let keychain = Arc::new(platform::DesktopKeychain::new());
let event_bus = Arc::new(platform::TauriEventBus::new(app.handle().clone()));

let app_core = AppCoreBuilder::new(keychain, event_bus, app_data_dir)
    .with_watcher_factory(|p| Arc::new(platform::NotifyFileWatcher::new(p)))
    .build()
    .await?;

前端看到的仍然是熟悉的 Tauri 调用:

ts 复制代码
import { invoke } from "@tauri-apps/api/core";

await invoke("apply_ydoc_update", {
  docUuid,
  update,
});

这里的关键不是 invoke 本身,而是边界:Tauri command 只做参数接收、错误转换和事件转发,真正的业务规则尽量下沉到 swarmnote-core

移动端:RN 负责"手机体验"

移动端仓库 swarmnote-mobile 是 Expo + React Native。它负责手机上该有的东西:

  • Expo Router 文件路由
  • NativeWind / RN primitives UI
  • 安全区、键盘、手势、系统能力
  • expo-secure-storeexpo-file-system 等移动端 host 能力

但移动端没有重写工作区、文档、配对、Yjs 状态机。它通过 react-native-swarmnote-core 这个 workspace 包,把 Rust 暴露成 RN 可以直接调用的 Turbo Module。

sequenceDiagram participant UI as React Native UI participant TS as Generated TypeScript API participant JSI as Hermes JSI / C++ participant Wrap as mobile-core wrapper participant Core as swarmnote-core UI->>TS: workspace.openDoc("daily.md") TS->>JSI: direct native call JSI->>Wrap: UniffiWorkspaceCore::open_doc Wrap->>Core: WorkspaceCore.ydoc().open_doc() Core-->>Wrap: doc_uuid + full Y.Doc state Wrap-->>UI: typed result

在 Rust 侧,移动端 wrapper 很薄。它定义 #[derive(uniffi::Object)] 的对象,把 WorkspaceCore 包起来,然后导出 async 方法:

rust 复制代码
#[derive(uniffi::Object)]
pub struct UniffiWorkspaceCore {
    inner: Arc<WorkspaceCore>,
}

#[uniffi::export(async_runtime = "tokio")]
impl UniffiWorkspaceCore {
    pub async fn open_doc(&self, rel_path: String) -> Result<UniffiOpenDocResult, FfiError> {
        let result = self.inner.ydoc().open_doc(&rel_path).await?;
        Ok(result.into())
    }
}

它和 Tauri command 的关系很像:

桌面端 Tauri 移动端 UniFFI
#[tauri::command] #[uniffi::export]
invoke("cmd", args) 直接调用生成的 TS 函数/对象方法
app.emit("event") callback interface / event adapter
JSON IPC JSI / C++ 绑定
运行时参数匹配 生成 TypeScript 类型

这就是这套架构最舒服的地方:开发体验像 Tauri,但移动端运行时更贴近原生。

编辑器:为什么移动端还需要 WebView

这里有一个容易误解的点:移动端用了 React Native,不代表所有东西都必须变成 RN 原生组件。

SwarmNote 的编辑器是 CodeMirror 6。它依赖 DOM、Selection、MutationObserver、CSS 布局等 Web 能力,很适合桌面 Tauri WebView,但不能直接塞进 RN 原生渲染树。为了解决这个问题,编辑器后来被独立成了 swarmnote-editor monorepo,并发布成 npm 包,让桌面端、移动端和未来其他 host 都能复用同一个 Markdown live-preview 内核。

所以移动端采用"两条桥":

  1. 业务桥:RN -> UniFFI -> Rust core
  2. 编辑器桥:RN -> WebView -> Comlink -> CodeMirror
flowchart LR subgraph Phone["React Native App"] Screen["Editor Screen"] Store["Zustand Stores"] Bridge["Comlink Host Adapter"] end subgraph WebView["WebView"] Endpoint["Comlink WebView Endpoint"] EditorWeb["WebView bundle
@swarmnote/editor-react-native/webview"] EditorCore["@swarmnote/editor-core
CodeMirror 6 · Yjs · Markdown"] end subgraph Rust["Rust Core"] YDoc["YDocManager / yrs"] Sync["WorkspaceSync"] end Screen --> Bridge Bridge <--> Endpoint Endpoint --> EditorWeb --> EditorCore Screen --> Store Store --> YDoc EditorCore -- "local Y.Update bytes" --> Bridge Bridge -- "apply_update()" --> YDoc YDoc -- "remote update bytes" --> Bridge Bridge -- "applyRemoteUpdate()" --> EditorCore YDoc --> Sync

这看起来多了一层,但换来了非常现实的收益:

  • 桌面和移动共享同一套 Markdown 编辑器核心
  • CodeMirror 插件、Yjs 绑定、数学公式、图片渲染逻辑可以复用
  • RN 只负责移动端外壳和交互,不用重写一个编辑器
  • WebView 内部仍是完整 Web 环境,调试和打包路径清晰

移动端这条链路的本质,是加载一个自包含的 editor WebView bundle,再由 RN WebView 承载。早期在 swarmnote-mobile/packages/editor-web 里维护这层入口;现在它已经在 swarmnote-editor 里沉淀为 @swarmnote/editor-react-native/webview 这样的 npm subpath。RN 和 WebView 之间用 Comlink 把 postMessage 包装成"像本地函数一样调用"的 RPC。

顺手把编辑器也产品化

swarmnote-editor 不是 SwarmNote 仓库里的一个私有目录,而是独立发布的编辑器工程。它目前拆成三个公开 npm 包:

用途
@swarmnote/editor-core CodeMirror 6 内核、Markdown live-preview、Plugin SDK,以及 math / table / mermaid / slash / wikilink 等插件
@swarmnote/editor-react React host 的薄适配,提供 EditorViewI18nProvider
@swarmnote/editor-react-native React Native host 的桥接层,提供 useEditorBridge、Comlink adapter 和 WebView HTML bundle

这里的拆法也延续了 SwarmNote 的跨端思路:运行时内核走 npm,容易被复用;UI primitives 走 shadcn 风格 registry,方便 host 复制后按自己的产品体验改。

flowchart TB subgraph npm["npm runtime packages"] Core["@swarmnote/editor-core
CM6 内核 + Plugin SDK"] ReactPkg["@swarmnote/editor-react
React plumbing"] RNPkg["@swarmnote/editor-react-native
RN bridge + WebView bundle"] end subgraph Registry["shadcn-style registry"] WebUI["Web UI primitives
slash-popover · wikilink-popover · toolbar"] RNUI["RN UI primitives
slash-sheet · heading-sheet · markdown-editor"] end ReactPkg --> Core RNPkg --> Core WebUI -. copy to host .-> ReactPkg RNUI -. copy to host .-> RNPkg

如果只想在自己的 Tauri / Electron / Web 项目里嵌一个 Markdown 编辑器,可以从最小安装开始:

bash 复制代码
pnpm add @swarmnote/editor-core @swarmnote/editor-react

如果是 React Native / Expo,则是:

bash 复制代码
pnpm add @swarmnote/editor-core @swarmnote/editor-react-native react-native-webview comlink

这也是我觉得 SwarmNote 架构比较值得写出来的原因:不是只把产品做成跨端,而是把过程中沉淀出来的"可复用零件"也顺手开源、发布、文档化。swarmnote-core 解决本地优先和 P2P 同步,swarmnote-editor 则解决 Markdown 编辑体验复用。

真正的核心:把平台差异变成 trait

跨端最容易失败的地方,是一开始把 Tauri、RN、文件系统、通知、密钥存储混在业务逻辑里。SwarmNote 的做法是让 swarmnote-core 保持平台无关:

classDiagram class AppCore { identity pairing network recent_workspaces } class WorkspaceCore { documents filesystem ydoc sync } class KeychainProvider { <> load_keypair() save_keypair() } class EventBus { <> emit(AppEvent) } class FileSystem { <> read_text() write_text() scan_tree() save_media() } class FileWatcher { <> watch() } AppCore --> KeychainProvider AppCore --> EventBus WorkspaceCore --> FileSystem WorkspaceCore --> FileWatcher WorkspaceCore --> EventBus

桌面端实现这些 trait:

能力 桌面实现
密钥 keyring,对接 macOS Keychain / Windows Credential Manager / Linux Secret Service
事件 TauriEventBus,内部调用 AppHandle::emit
文件监听 notify + debouncer
本地文件 桌面文件系统

移动端则换成另一套实现:

能力 移动实现
密钥 RN 侧 expo-secure-store,Rust 侧通过 callback/adapter 使用
事件 UniFFI callback interface,推到 RN store
文件监听 移动沙盒内通常不需要桌面式 watcher
本地文件 App sandbox / Expo FileSystem 路径

业务核心不问"我现在是不是 Tauri",只问"谁实现了这个 trait"。这就是跨端复用真正成立的原因。

P2P:SwarmNote 为什么需要 Rust core

SwarmNote 不是普通 Markdown 编辑器。它的产品目标是:

  • 笔记保存在本地 Markdown 文件夹
  • 多台自己的设备组成一个 swarm
  • 不依赖云账号或中心服务器
  • 通过 libp2p 做设备发现、连接、配对和消息广播
  • 用 Yjs/yrs 处理离线编辑后的合并

这类能力如果分别用 JS、Kotlin、Swift、Rust 写四遍,很快会进入维护地狱。Rust core 的价值在这里变得很明确:

flowchart LR EditA["设备 A 编辑 Markdown"] --> YA["Y.Doc 产生 update"] YA --> Pub["GossipSub 发布增量"] Pub --> Net["libp2p 网络
mDNS / DHT / Relay"] Net --> Recv["设备 B 收到 update"] Recv --> YB["yrs apply_update"] YB --> Flush["写回 SQLite + .md 文件"] style Net fill:#e8f4ff,stroke:#208aef

Rust 这层同时持有:

  • libp2p 网络运行时
  • 设备身份和配对状态
  • SQLite 元数据
  • Y.Doc 状态读写
  • 文档增量同步协议

桌面和移动共享它,意味着同一个 bug 只修一次,同一套同步协议不会因为平台不同而悄悄分叉。

这套架构的文件视角

桌面仓库:

text 复制代码
swarmnote/
├── src/                       # React 桌面前端
├── src-tauri/                 # Tauri host:commands / plugins / desktop adapters
├── crates/
│   ├── core/                  # swarmnote-core:平台无关业务核心
│   ├── entity/                # SeaORM entities
│   └── migration/             # SQLite migrations
├── libs/core/                 # swarm-p2p-core:libp2p 封装
└── dev-notes/blog/            # 技术文章和架构笔记

移动仓库:

text 复制代码
swarmnote-mobile/
├── src/                       # Expo Router / RN screens / stores
├── packages/
│   ├── editor-web/            # 早期 WebView 编辑器入口;可迁移到 @swarmnote/editor-react-native
│   └── swarmnote-core/        # react-native-swarmnote-core
│       ├── rust/mobile-core/  # UniFFI wrapper crate
│       ├── src/generated/     # 生成的 TS 绑定
│       └── cpp/generated/     # 生成的 C++ JSI 绑定
└── plugins/                   # Expo config plugins

共享编辑器仓库:

text 复制代码
swarmnote-editor/
├── packages/editor-core/              # @swarmnote/editor-core
├── packages/editor-react/             # @swarmnote/editor-react
├── packages/editor-react-native/      # @swarmnote/editor-react-native
└── registry/                          # shadcn 风格 UI primitives

一次"打开文档"的完整链路

把上面的图合起来,看一条用户操作链路会更直观。

sequenceDiagram autonumber participant User as 用户 participant RN as RN Editor Screen participant Core as UniFFI WorkspaceCore participant Rust as swarmnote-core participant WV as WebView CodeMirror participant P2P as libp2p swarm User->>RN: 打开 daily.md RN->>Core: openDoc("daily.md") Core->>Rust: ydoc.open_doc() Rust-->>Core: doc_uuid + full_state Core-->>RN: typed result RN->>WV: seedDocument(full_state) User->>WV: 输入文字 WV-->>RN: local Y.Update bytes RN->>Core: applyUpdate(doc_uuid, bytes) Core->>Rust: apply_update + debounce writeback Rust->>P2P: publish doc update

桌面端链路几乎一样,只是 RN -> UniFFI 换成了 React -> Tauri invokeWebView CodeMirror 就是 Tauri 窗口里的前端编辑器。

它带来的工程收益

第一,业务一致性更强。

配对、同步、文档状态、冲突处理都在 Rust core,同一套测试和同一套状态机覆盖桌面与移动。

第二,平台体验不妥协。

桌面端继续享受 Tauri 的系统集成、托盘、自动更新、小包体;移动端使用 RN/Expo 做导航、手势、键盘、安全区和移动原生能力。

第三,迁移路径自然。

SwarmDrop 先验证 libp2p,再抽出 swarm-p2p-core;SwarmNote 进一步抽出 swarmnote-core;现在 SwarmDrop 也可以沿同样边界迁移。这不是一次性重写,而是把已经跑通的能力逐步"下沉成核心"。

第四,编辑器复用现实可行。

CodeMirror 不硬改成 RN 原生组件,而是让 WebView 做它擅长的事。RN 通过 Comlink 拿到类型化 API,编辑器体验保持一致。

代价和坑

这套架构也不是免费午餐。

解决方式
移动端不能用 Expo Go 必须用 development build,因为有原生 Rust Turbo Module
Rust 改动后需要重生成绑定 pnpm --filter react-native-swarmnote-core ubrn:androidubrn:ios
WebView 编辑器可能加载旧 bundle editor-coreeditor-react-native/webview 后重建对应 npm 包 / WebView bundle
生成代码很大 明确约定 src/generated / cpp/generated 不手改
平台能力边界容易滑坡 新功能先判断:业务规则进 core,平台能力进 host adapter
事件链路更长 用统一 AppEvent + Tauri emit / UniFFI callback 做映射

一个实用判断标准:

flowchart TD A["要加一个新功能"] --> B{"是不是业务规则?"} B -->|"是:文档、同步、配对、状态机"| C["放进 swarmnote-core"] B -->|"否"| D{"是不是平台能力?"} D -->|"是:文件选择、密钥、通知、路径"| E["定义/复用 trait
桌面和移动各自实现"] D -->|"否:纯界面交互"| F["桌面放 src/
移动放 swarmnote-mobile/src/"]

如果你也想用这套方案

可以按这个顺序思考,而不是一上来就选框架:

  1. 先找出真正要共享的核心。

    如果只是 UI 相似,不一定需要 Rust core;如果有协议、同步、加密、数据库、复杂状态机,就很适合。

  2. 把 core 做到"不知道宿主是谁"。

    不要在 core 里 use tauri::*,也不要让它依赖 RN 包。平台差异通过 trait 或 wrapper 注入。

  3. 桌面 host 保持薄。

    Tauri command 不要变成业务泥潭,尽量只做参数转换和事件桥接。

  4. 移动 host 保持移动优先。

    RN 负责手机体验,不要为了"和桌面完全一样"牺牲原生交互。

  5. 编辑器/复杂 Web 组件可以单独走 WebView。

    WebView 不一定是失败,它可以是非常明确的边界:只承载最适合 Web 的模块。

SwarmNote 现在是什么状态

SwarmNote 正在做的是一个本地优先、P2P 同步的 Markdown 笔记工具:

  • 笔记就是本地 .md 文件
  • 设备通过 6 位配对码加入自己的 swarm
  • libp2p 负责设备间连接
  • Yjs/yrs 负责离线编辑后的增量合并
  • 桌面端是 Tauri + React
  • 移动端是 Expo + React Native
  • 两端共享 Rust 核心

如果你对"没有云账号、没有中心服务器、自己的设备直接同步笔记"感兴趣,可以关注:

参考资料

相关推荐
feasibility.1 小时前
反爬十层妖塔:现代爬虫攻防的立体战争
爬虫·python·科技·scrapy·rust·go·硬件
王木风3 小时前
终端里的编程副驾:DeepSeek-TUI-项目深度拆解,实测与原理分析
linux·运维·人工智能·rust·node.js
迷渡5 小时前
聊一聊 Bun 用 Rust 重写这件事
开发语言·后端·rust
贫民窟的勇敢爷们5 小时前
React跨平台能力,打破前端开发的平台边界
前端·react.js·前端框架
RustCoder6 小时前
MangoFetch:一个用 Rust 写的 CLI/TUI 高性能的下载工具
后端·rust·开源
朝阳397 小时前
React【面试】
前端·react.js·面试
漓漾li7 小时前
每日面试题(2026-05-15)- 前端
前端·vue.js·react.js
用户600071819107 小时前
【翻译】在 React Router 中理清对话框
react.js
fox_lht10 小时前
第十二章 泛型、接口和生命周期
开发语言·后端·rust