5月28日 02:46

Tauri 通信协议有哪些?IPC 自定义扩展详解

Tauri 的前端和 Rust 后端跑在不同进程里,两者之间的通信全靠 IPC(Inter-Process Communication)。理解 IPC 的机制和边界,是写好 Tauri 应用的前提——选错了通信方式,要么性能拉胯,要么安全踩坑。

Tauri IPC 的两种原语:Commands 和 Events

Tauri 的 IPC 不是什么"消息总线",它就两种东西:CommandsEvents

Commands:请求-响应模式

Command 本质上是前端调用后端的一个 Rust 函数,传参数进去,拿返回值出来。类似浏览器的 fetch,但走的是 IPC 通道而非网络。

后端定义 Command:

rust
#[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) }

注册到应用里:

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

前端调用:

javascript
import { invoke } from '@tauri-apps/api/core'; const result = await invoke('greet', { name: 'Tauri' }); console.log(result); // "Hello, Tauri!"

几个关键点:

  • 参数和返回值都通过 serde 序列化,Rust 侧必须实现 SerializeDeserialize
  • Command 支持异步(async fn),Tauri 会自动在 tokio 运行时上调度
  • 如果返回大数据,别用 JSON 序列化——用 tauri::ipc::Response 直接返回原始字节,性能好得多

Events:发布-订阅模式

Event 是单向的"即发即忘"消息,适合通知、状态变更、进度更新这类不需要返回值的场景。

后端向前端发事件:

rust
use tauri::Manager; #[tauri::command] fn start_task(window: tauri::Window) { std::thread::spawn(move || { for i in 0..=100 { window.emit("progress", i).unwrap(); std::thread::sleep(std::time::Duration::from_millis(50)); } }); }

前端监听:

javascript
import { listen } from '@tauri-apps/api/event'; const unlisten = await listen('progress', (event) => { console.log(`进度: ${event.payload}%`); }); // 不再需要时移除监听 unlisten();

前端也能往后端发事件:

javascript
import { emit } from '@tauri-apps/api/event'; await emit('user-action', { type: 'click', target: 'button-1' });

后端接收:

rust
use tauri::Manager; fn main() { tauri::Builder::default() .setup(|app| { app.listen_global("user-action", |event| { println!("收到前端事件: {:?}", event.payload()); }); Ok(()) }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }

怎么选?简单原则

  • 需要返回值 → 用 Command
  • 只是通知一下 → 用 Event
  • 需要持续推送数据(如进度条、日志流)→ 用 Event
  • 前端调后端做一件事然后等结果 → 用 Command

IPC 底层传输:v1 vs v2 的关键差异

Tauri v1 和 v2 的 IPC 传输机制差异很大,直接影响通信性能。

v1:postMessage + JSON 序列化

v1 的 IPC 完全基于 WebView 的 postMessage 接口。所有数据必须序列化成字符串再传,二进制数据也得先 base64 编码。这导致:

  • 大数据传输慢,序列化/反序列化开销大
  • 无法直接传二进制数据(图片、文件等)
  • 每次通信都有额外的字符串转换成本

v2:自定义协议(ipc:// URI Scheme)

v2 用了自定义 URI 协议(ipc://localhost),前端通过类似 HTTP POST 的方式发送 IPC 请求,后端直接处理。好处是:

  • 支持直接传 ArrayBuffer / Uint8Array,不需要 base64
  • 响应也能直接返回原始字节(通过 tauri::ipc::Response
  • 性能接近原生 HTTP 通信,比 v1 快很多

当自定义协议不可用时(比如 Linux 上 webkit2gtk 版本太低),v2 会自动降级到 postMessage 模式。

能不能自定义通信协议?

这要看你说的"自定义"是哪种。

在 IPC 框架内封装——完全可以

IPC 的 Command 本身就是你可以随意定义的函数。你可以设计自己的消息结构:

rust
#[derive(serde::Deserialize, serde::Serialize)] struct CustomRequest { action: String, data: serde_json::Value, } #[tauri::command] async fn custom_handler(req: CustomRequest) -> Result<serde_json::Value, String> { match req.action.as_str() { "query" => Ok(serde_json::json!({"status": "ok", "result": "data"})), "mutate" => { // 执行修改操作 Ok(serde_json::json!({"status": "ok"})) } _ => Err(format!("未知操作: {}", req.action)), } }

前端统一调用:

javascript
const result = await invoke('custom_handler', { req: { action: 'query', data: { key: 'value' } } });

这本质上是在 IPC 之上封装了一套应用层协议,消息格式、路由逻辑完全由你控制。

注册自定义 URI Scheme——可以,但有边界

Tauri 提供了 register_uri_scheme_protocol,让你注册自己的 URI 协议(比如 myapp://),前端可以通过这个协议和后端通信:

rust
tauri::Builder::default() .register_uri_scheme_protocol("myapp", |_ctx, request| { let path = request.uri().path(); // 根据路径处理请求 http::Response::builder() .header("Content-Type", "application/json") .body(r#"{"message": "hello"}"#.as_bytes().to_vec()) .unwrap() }) .run(tauri::generate_context!()) .expect("error while running tauri application");

v2 还支持异步版本 register_asynchronous_uri_scheme_protocol,不会阻塞主线程:

rust
tauri::Builder::default() .register_asynchronous_uri_scheme_protocol("myapp", |_ctx, request, responder| { std::thread::spawn(move || { let data = std::fs::read(request.uri().path()[1..].to_string()); match data { Ok(bytes) => responder.respond( http::Response::builder().body(bytes).unwrap() ), Err(_) => responder.respond( http::Response::builder() .status(http::StatusCode::NOT_FOUND) .body("file not found".as_bytes().to_vec()) .unwrap() ), } }); }) .run(tauri::generate_context!()) .expect("error while running tauri application");

注意事项:

  • 注册的协议只在应用内的 WebView 中可访问,不会注册为系统级协议
  • 不同平台行为有差异,Windows 上尤其需要注意
  • 这个机制更像是"在应用内跑一个本地 API 服务",而不是真正的自定义传输协议

真正自创协议栈——做不到

Tauri 的 IPC 底层传输是固定的(自定义 URI scheme 或 postMessage),你没法绕过它去实现一套完全独立的二进制协议。所有通信最终都得走 Tauri 的消息通道。

第三方协议支持呢?

Tauri 本身只提供 IPC(Commands + Events),不内置 HTTP 服务器、WebSocket 服务端或 MQTT 客户端。但这些不是"通信协议缺失"——它们属于应用层需求,用 Rust 生态的 crate 就能解决:

  • WebSocket 服务:用 tokio-tungstenite 在 Rust 侧起一个 WS 服务
  • HTTP API:用 axumactix-web 起本地 HTTP 服务
  • MQTT:用 rumqtt 接入 MQTT broker
  • DBus(Linux):用 zbus 进行系统级通信

这些方案和 Tauri IPC 是互补关系,不是替代关系。IPC 负责前端-Rust 通信,第三方 crate 负责和外部系统通信,各管各的。

安全注意点

IPC 通信有几个安全相关的配置必须了解:

  • Invoke Key:v2 的每个 IPC 请求都带一个运行时生成的随机 key,确保请求来自已初始化的 WebView,防止恶意页面伪造调用
  • Isolation Pattern:Tauri v2 提供隔离模式,用沙箱化的 <iframe> 拦截和验证 IPC 消息,消息还会用 SubtleCrypto 加密
  • 权限控制:通过 capabilities 配置限制哪些 Command 可以被调用,默认最小权限原则
  • dangerousRemoteDomainIpcAccess:除非你明确知道自己在做什么,否则不要开启这个配置

说到底,Tauri 的通信设计就是:IPC 搞定前后端通信,够用;想扩展,在 Rust 侧加 crate;想定制,在 IPC 之上封装消息格式。 不要试图绕过 IPC,那是 Tauri 安全模型的根基。

标签:Tauri