Tauri 里 JS ↔ Rust 的通信:一套受控的“双向总线”

在 Tauri 里,你可以把通信分成两条主线:

一条叫 Command :JS 主动调用 Rust 的函数,像"发起请求拿结果"。
一条叫 Event:Rust 主动推消息给 JS,像"通知、进度、订阅"。

这两条线合起来,就是一个完整的应用内 RPC + PubSub 体系。


1)JS → Rust:Command(invoke)

你在前端写的调用长这样

在 Tauri v1 常见写法:

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

const result = await invoke<string>("greet", { name: "Aran" });
console.log(result);

你会发现它很像调用远端 API,只是目标不在网络上,而在本机的 Rust 里。

Rust 端怎么接

Rust 端注册一个 command:

复制代码
#[tauri::command]
fn greet(name: String) -> String {
  format!("Hello, {}!", name)
}

fn main() {
  tauri::Builder::default()
    .invoke_handler(tauri::generate_handler![greet])
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

这就是一套最小闭环:JS 发 invoke,Rust 收到并返回结果。

背后的协议本质是什么

前端把参数序列化成 JSON(或结构化 payload),通过 WebView 的桥接机制发送给宿主(Rust)。Rust 反序列化成函数入参,执行后把返回值再序列化回去。

你可以把它理解成:

  • JS 发起 commandName + payload

  • Rust 执行 command(payload) -> result

  • JS 等待 Promise resolve

这套机制天然带来了一个工程优势:前端代码不需要关心系统细节,它只关心"我能调用哪些能力"。


2)Rust → JS:Event(emit)

Command 解决了"请求-响应",但桌面应用还有大量场景需要"推送":

  • 批处理进度条

  • 后台任务状态

  • 文件变更通知

  • SQLite 同步完成提示

  • 模型推理流式输出

这类需求用 Event 更合适。

Rust 端发事件

复制代码
use tauri::Manager;

fn main() {
  tauri::Builder::default()
    .setup(|app| {
      let window = app.get_window("main").unwrap();
      window.emit("progress", 10).unwrap();
      Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error");
}

JS 端监听事件

复制代码
import { listen } from "@tauri-apps/api/event";

const unlisten = await listen<number>("progress", (event) => {
  console.log("progress:", event.payload);
});

这是一种典型的发布订阅模型:

  • Rust emit(eventName, payload)

  • JS listen(eventName, handler)

Event 的关键价值是:Rust 可以在任何时刻主动通知 UI,适合长任务、实时变化、后台线程。


3)把两条线拼起来:桌面应用的"状态机循环"

一个成品软件的真实运行方式通常是这样的:

  1. 用户点按钮

  2. JS invoke Rust 开始任务

  3. Rust 在后台线程跑任务

  4. Rust emit 进度事件给 JS

  5. JS 更新 UI

  6. Rust 完成后返回结果或再 emit 完成事件

这里你会看到:Command 更像启动器,Event 更像过程反馈。

如果你的软件要做"短信智标官"这种批处理,几乎一定是这个模型。


4)安全模型:为什么 Tauri 的通信比你想象中严格

桌面应用的最大风险常见在这里:

JS 是 UI 层,容易被注入脚本;Rust 是系统层,能读写文件、执行命令。

如果通信是全开放的,等于给潜在攻击者提供了一条直达系统能力的通路。

所以 Tauri 的设计很明确:通信能力必须被约束。

常见约束点包括:

  • 只有标记了 #[tauri::command] 的函数才允许被调用

  • command 名称是显式注册的,不存在"随便调"

  • 权限、文件访问、shell 等能力建议走 allowlist(尤其是涉及系统级调用的插件)

  • 生产环境建议启用 CSP 和严格的资源加载策略

你可以把它理解成:Rust 是高权限域,JS 只能走白名单桥接。


5)工程实践:什么时候用 Command,什么时候用 Event

这块直接决定你项目会不会越写越乱。

适合 Command 的典型场景

  • 读取配置、查询 SQLite、加载文件

  • 触发一次性动作(导入、导出、开始推理)

  • 返回一个明确结果

特征:一次调用对应一个结果,前端可以 await。

适合 Event 的典型场景

  • 进度更新、日志流、长任务状态

  • 监听系统事件(文件变化、窗口状态)

  • 推送消息、提醒

特征:持续产生信息,前端要订阅。


6)更进一步:你最终会需要的三个"通信层设计"

当项目变大,你会发现"随手写 invoke / listen"会变得难维护。成品感强的软件一般会有三层设计:

第一层:API 层(前端封装)

前端不直接 scattered invoke,而是封装成:

  • api.importSms(filePath)

  • api.batchTag(options)

  • api.exportResult(format)

这样 UI 组件永远不接触底层通信细节。

第二层:Rust Command 层(接口白名单)

Rust 里 command 只做三件事:

  • 校验参数

  • 调用 service

  • 返回结构化结果

它不直接堆业务逻辑。

第三层:Service 层(业务核心)

真正的:

  • SQLite 操作

  • 推理引擎调用

  • 批处理 pipeline

  • 日志与进度

都在 service 层跑,event 也从这里 emit。

这套结构会让你的项目可测试、可扩展、可维护。


7)结合你的场景:短信批处理/模型推理怎么通信最顺

你这种"离线小模型 + 批处理 + 复核 + 导出"的桌面软件,通信最好走这个模式:

  • 点击"开始打标" → invoke("start_tagging", params)

  • Rust 启动后台线程

  • 每处理 N 条短信 emit 一次 tagging_progress

  • 每条结果 emit tagging_item_result 或写入 SQLite 后只 emit id

  • 完成后 emit tagging_done 或让前端再 invoke 查询汇总

这样 UI 不会卡,体验更像成品软件。

相关推荐
Lee川1 小时前
解锁 JavaScript 的灵魂:深入浅出原型与原型链
javascript·面试
swipe2 小时前
从原理到手写:彻底吃透 call / apply / bind 与 arguments 的底层逻辑
前端·javascript·面试
Lee川4 小时前
探索JavaScript的秘密令牌:独一无二的`Symbol`数据类型
javascript·面试
Lee川4 小时前
深入浅出JavaScript事件机制:从捕获冒泡到事件委托
前端·javascript
光影少年4 小时前
async/await和Promise的区别?
前端·javascript·掘金·金石计划
codingWhat4 小时前
如何实现一个「万能」的通用打印组件?
前端·javascript·vue.js
前端Hardy7 小时前
别再无脑用 `JSON.parse()` 了!这个安全漏洞你可能每天都在触发
前端·javascript·vue.js
前端Hardy7 小时前
别再让 `console.log` 上线了!它正在悄悄拖垮你的生产系统
前端·javascript·vue.js
csdn飘逸飘逸8 小时前
Autojs基础-用户界面(ui)
javascript
炫饭第一名8 小时前
速通Canvas指北🦮——图形、文本与样式篇
前端·javascript·程序员