Tauri 通信协议有哪些?IPC 自定义扩展详解
Tauri 的前端和 Rust 后端跑在不同进程里,两者之间的通信全靠 IPC(Inter-Process Communication)。理解 IPC 的机制和边界,是写好 Tauri 应用的前提——选错了通信方式,要么性能拉胯,要么安全踩坑。
Tauri IPC 的两种原语:Commands 和 Events
Tauri 的 IPC 不是什么"消息总线",它就两种东西:Commands 和 Events。
Commands:请求-响应模式
Command 本质上是前端调用后端的一个 Rust 函数,传参数进去,拿返回值出来。类似浏览器的 fetch,但走的是 IPC 通道而非网络。
后端定义 Command:
rust#[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) }
注册到应用里:
rustfn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
前端调用:
javascriptimport { invoke } from '@tauri-apps/api/core'; const result = await invoke('greet', { name: 'Tauri' }); console.log(result); // "Hello, Tauri!"
几个关键点:
- 参数和返回值都通过
serde序列化,Rust 侧必须实现Serialize和Deserialize - Command 支持异步(
async fn),Tauri 会自动在 tokio 运行时上调度 - 如果返回大数据,别用 JSON 序列化——用
tauri::ipc::Response直接返回原始字节,性能好得多
Events:发布-订阅模式
Event 是单向的"即发即忘"消息,适合通知、状态变更、进度更新这类不需要返回值的场景。
后端向前端发事件:
rustuse 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)); } }); }
前端监听:
javascriptimport { listen } from '@tauri-apps/api/event'; const unlisten = await listen('progress', (event) => { console.log(`进度: ${event.payload}%`); }); // 不再需要时移除监听 unlisten();
前端也能往后端发事件:
javascriptimport { emit } from '@tauri-apps/api/event'; await emit('user-action', { type: 'click', target: 'button-1' });
后端接收:
rustuse 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)), } }
前端统一调用:
javascriptconst result = await invoke('custom_handler', { req: { action: 'query', data: { key: 'value' } } });
这本质上是在 IPC 之上封装了一套应用层协议,消息格式、路由逻辑完全由你控制。
注册自定义 URI Scheme——可以,但有边界
Tauri 提供了 register_uri_scheme_protocol,让你注册自己的 URI 协议(比如 myapp://),前端可以通过这个协议和后端通信:
rusttauri::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,不会阻塞主线程:
rusttauri::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:用
axum或actix-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 安全模型的根基。