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

0 阅读15分钟

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

SwarmNote logo转存失败,建议直接上传图片文件

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 核心外面套了两个不同的壳。

目录

flowchart TB
    subgraph Desktop["桌面端:SwarmNote"]
        React["React 19 + TypeScript<br/>CodeMirror 6 · Zustand · TanStack Router"]
        Tauri["Tauri 2 Host<br/>Commands · Events · Tray · Updater"]
    end

    subgraph Mobile["移动端:SwarmNote Mobile"]
        RN["Expo + React Native<br/>NativeWind · Expo Router · Zustand"]
        UniFFI["UniFFI / JSI Turbo Module<br/>生成 TS + C++ 绑定"]
        WV["WebView Editor<br/>CodeMirror 6 + Comlink"]
    end

    Core["swarmnote-core<br/>Workspace · Document CRUD · YDocManager · Pairing"]
    P2P["swarm-p2p-core<br/>libp2p · mDNS · DHT · DCUtR · Relay · GossipSub"]
    DB[("SQLite<br/>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 mobileReact 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,再把桌面专属能力注入进去。

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 调用:

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 方法:

#[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 IPCJSI / 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<br/>@swarmnote/editor-react-native/webview"]
        EditorCore["@swarmnote/editor-core<br/>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-coreCodeMirror 6 内核、Markdown live-preview、Plugin SDK,以及 math / table / mermaid / slash / wikilink 等插件
@swarmnote/editor-reactReact host 的薄适配,提供 EditorViewI18nProvider
@swarmnote/editor-react-nativeReact 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<br/>CM6 内核 + Plugin SDK"]
        ReactPkg["@swarmnote/editor-react<br/>React plumbing"]
        RNPkg["@swarmnote/editor-react-native<br/>RN bridge + WebView bundle"]
    end

    subgraph Registry["shadcn-style registry"]
        WebUI["Web UI primitives<br/>slash-popover · wikilink-popover · toolbar"]
        RNUI["RN UI primitives<br/>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 编辑器,可以从最小安装开始:

pnpm add @swarmnote/editor-core @swarmnote/editor-react

如果是 React Native / Expo,则是:

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 {
      <<trait>>
      load_keypair()
      save_keypair()
    }

    class EventBus {
      <<trait>>
      emit(AppEvent)
    }

    class FileSystem {
      <<trait>>
      read_text()
      write_text()
      scan_tree()
      save_media()
    }

    class FileWatcher {
      <<trait>>
      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 网络<br/>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 只修一次,同一套同步协议不会因为平台不同而悄悄分叉。

这套架构的文件视角

桌面仓库:

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/            # 技术文章和架构笔记

移动仓库:

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

共享编辑器仓库:

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 编辑器可能加载旧 bundleeditor-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<br/>桌面和移动各自实现"]
    D -->|"否:纯界面交互"| F["桌面放 src/<br/>移动放 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 核心

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

参考资料