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

一篇寫給「想做跨端產品,但不想把業務邏輯在每個平台重寫一遍」的工程筆記。主角是 SwarmNote:桌面端用 Tauri + React,行動端用 Expo + React Native,底層共享同一份 Rust 核心。

SwarmNote logo轉存失敗,建議直接上傳圖片檔SwarmNote:你的筆記,在你自己的裝置之間群聚流動。

先說結論

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 跑所有端」

一開始我也很想要一個特別漂亮的答案: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

桌面端:Tauri 負責「系統殼」

Tauri 在 SwarmNote 裡不是業務核心,而是桌面 host。它做這些事:

  • 建立視窗、系統匣、自動更新和系統通知
  • 把前端 invoke() 呼叫轉成 Rust 命令
  • 把 Rust 事件透過 emit() 推給前端
  • 提供桌面端實作:Keychain、檔案監看、視窗到工作區的映射

SwarmNote 的 src-tauri/src/lib.rs 裡可以看到典型入口:註冊外掛、註冊 commands,在 setup 階段建立 AppCore,再把桌面專屬能力注入進去。

rust 体验AI代码助手 代码解读复制代码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 体验AI代码助手 代码解读复制代码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。

在 Rust 端,行動端 wrapper 很薄。它定義 #[derive(uniffi::Object)] 的物件,把 WorkspaceCore 包起來,然後導出 async 方法:

rust 体验AI代码助手 代码解读复制代码#[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 adapterJSON 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

這看起來多了一層,但換來了非常現實的收益:

  • 桌面和行動共享同一套 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 複製後按自己的產品體驗改。

如果只想在自己的 Tauri / Electron / Web 專案裡嵌一個 Markdown 編輯器,可以從最小安裝開始:

bash 体验AI代码助手 代码解读复制代码pnpm add @swarmnote/editor-core @swarmnote/editor-react

如果是 React Native / Expo,則是:

bash 体验AI代码助手 代码解读复制代码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 保持平台無關:

桌面端實作這些 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 的價值在這裡變得很明確:

Rust 這層同時持有:

  • libp2p 網路執行時
  • 裝置身分和配對狀態
  • SQLite 中繼資料
  • Y.Doc 狀態讀寫
  • 文件增量同步協議

桌面和行動共享它,意味著同一個 bug 只修一次,同一套同步協議不會因為平台不同而悄悄分叉。

這套架構的檔案視角

桌面倉庫:

text 体验AI代码助手 代码解读复制代码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 体验AI代码助手 代码解读复制代码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 体验AI代码助手 代码解读复制代码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

一次「開啟文件」的完整鏈路

把上面的圖合起來,看一條使用者操作鏈路會更直觀。

桌面端鏈路幾乎一樣,只是 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 ModuleRust 改動後需要重新產生綁定pnpm --filter react-native-swarmnote-core ubrn:androidubrn:iosWebView 編輯器可能載入舊 bundle改 editor-coreeditor-react-native/webview 後重建對應 npm 套件 / WebView bundle生成程式碼很大明確約定 src/generated / cpp/generated 不手改平台能力邊界容易滑坡新功能先判斷:業務規則進 core,平台能力進 host adapter事件鏈路更長用統一 AppEvent + Tauri emit / UniFFI callback 做映射一個實用判斷標準:

如果你也想用這套方案

可以按這個順序思考,而不是一上來就選框架:

  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 核心

如果你對「沒有雲端帳號、沒有中心伺服器、自己的裝置直接同步筆記」感興趣,可以關注:

參考資料


原文出處:https://juejin.cn/post/7639930076302770216


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝17   💬11   ❤️1
559
🥈
alicec
📝1   ❤️2
79
🥉
我愛JS
💬2  
7
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登