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 不会卡,体验更像成品软件。

相关推荐
寻寻觅觅☆6 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
萧曵 丶6 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
l1t6 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
Amumu121387 小时前
Vue3扩展(二)
前端·javascript·vue.js
NEXT067 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
ceclar1237 小时前
C++使用format
开发语言·c++·算法
码说AI7 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS7 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子8 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言