tauri中的通信
rust端调用前端
- 事件系统
-
通道
-
Evaluating JavaScript
事件系统
应用:流式传输少量数据或推送通知系统
限制:事件有效负载始终是 JSON 字符串(不适合较大的消息),不支持功能系统来精细控制事件数据和通道
使用范围:全局或特定webview
全局事件
使用 Emitter#emit 函数
// src-tauri/src/lib.rs
use tauri::{AppHandle, Emitter};
#[tauri::command]
fn download(app: AppHandle, url: String) {
app.emit("download-started", &url).unwrap();
for progress in [1, 15, 50, 80, 100] {
app.emit("download-progress", 10).unwrap();
}
app.emit("download-finished", &url).unwrap();
}
特定webview事件
使用 Emitter#emit_to 函数
// src-tauri/src/lib.rs
use tauri::{AppHandle, Emitter};
#[tauri::command]
fn login(app: AppHandle, user: String, password: String) {
let authenticated = user == "tauri-apps" && password == "tauri";
let result = if authenticated { "loggedIn" } else { "invalidCredentials" };
app.emit_to("login", "login-result", result).unwrap();
}
使用 Emitter#emit_filter 函数
// src-tauri/src/lib.rs
use tauri::{AppHandle, Emitter, EventTarget};
#[tauri::command]
fn open_file(app: AppHandle, path: std::path::PathBuf) {
app.emit_filter("open-file", path, |target| match target {
EventTarget::WebviewWindow { label } => label == "main" || label == "file-viewer",
_ => false,
}).unwrap();
}
事件负载
// src-tauri/src/lib.rs
use tauri::{AppHandle, Emitter};
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadStarted<'a> {
url: &'a str,
download_id: usize,
content_length: usize,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadProgress {
download_id: usize,
chunk_length: usize,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadFinished {
download_id: usize,
}
#[tauri::command]
fn download(app: AppHandle, url: String) {
let content_length = 1000;
let download_id = 1;
app.emit("download-started", DownloadStarted {
url: &url,
download_id,
content_length
}).unwrap();
for chunk_length in [15, 150, 35, 500, 300] {
app.emit("download-progress", DownloadProgress {
download_id,
chunk_length,
}).unwrap();
}
app.emit("download-finished", DownloadFinished { download_id }).unwrap();
}
监听事件
在前端监听事件
监听全局事件
import { listen } from '@tauri-apps/api/event';
type DownloadStarted = {
url: string;
downloadId: number;
contentLength: number;
};
listen<DownloadStarted>('download-started', (event) => {
console.log(
`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`
);
})
监听特定webview 的事件
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
const appWebview = getCurrentWebviewWindow();
appWebview.listen<string>('logged-in', (event) => {
localStorage.setItem('session-token', event.payload);
});
listen
函数在应用程序的整个生命周期内保持事件侦听器的注册状态。要停止监听事件,你可以使用 listen
函数返回的 unlisten
函数:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});
unlisten();
只监听一次事件
import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
once('ready', (event) => {});
const appWebview = getCurrentWebviewWindow();
appWebview.once('ready', () => {});
在rust上监听事件
监听全局事件
// src-tauri/src/lib.rs
use tauri::Listener;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
app.listen("download-started", |event| {
if let Ok(payload) = serde_json::from_str::<DownloadStarted>(&event.payload()) {
println!("downloading {}", payload.url);
}
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
监听特定webview 的事件
// src-tauri/src/lib.rs
use tauri::{Listener, Manager};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let webview = app.get_webview_window("main").unwrap();
webview.listen("logged-in", |event| {
let session_token = event.data;
// save token..
});
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
listen
函数在应用程序的整个生命周期内保持事件侦听器的注册状态。要停止监听事件,你可以使用 unlisten
函数:
// unlisten outside of the event handler scope:
let event_id = app.listen("download-started", |event| {});
app.unlisten(event_id);
// unlisten when some event criteria is matched
let handle = app.handle().clone();
app.listen("status-changed", |event| {
if event.data == "ready" {
handle.unlisten(event.id);
}
});
只监听一次事件
app.once("ready", |event| {
println!("app is ready");
});
通道
应用:流式处理操作。下载进程,子进程输出,WebSocket 消息
// src-tauri/src/lib.rs
use tauri::{AppHandle, ipc::Channel};
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
enum DownloadEvent<'a> {
#[serde(rename_all = "camelCase")]
Started {
url: &'a str,
download_id: usize,
content_length: usize,
},
#[serde(rename_all = "camelCase")]
Progress {
download_id: usize,
chunk_length: usize,
},
#[serde(rename_all = "camelCase")]
Finished {
download_id: usize,
},
}
#[tauri::command]
fn download(app: AppHandle, url: String, on_event: Channel<DownloadEvent>) {
let content_length = 1000;
let download_id = 1;
on_event.send(DownloadEvent::Started {
url: &url,
download_id,
content_length,
}).unwrap();
for chunk_length in [15, 150, 35, 500, 300] {
on_event.send(DownloadEvent::Progress {
download_id,
chunk_length,
}).unwrap();
}
on_event.send(DownloadEvent::Finished { download_id }).unwrap();
}
调用 download 命令时,必须创建频道并将其作为参数提供:
import { invoke, Channel } from '@tauri-apps/api/core';
type DownloadEvent =
| {
event: 'started';
data: {
url: string;
downloadId: number;
contentLength: number;
};
}
| {
event: 'progress';
data: {
downloadId: number;
chunkLength: number;
};
}
| {
event: 'finished';
data: {
downloadId: number;
};
};
const onEvent = new Channel<DownloadEvent>();
onEvent.onmessage = (message) => {
console.log(`got download event ${message.event}`);
};
await invoke('download', {
url: 'https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-schema-generator/schemas/config.schema.json',
onEvent,
});
Evaluating JavaScript
在 webview 上下文中直接执行任何 JavaScript 代码,可以使用 WebviewWindow#eval
函数(必须使用来自 Rust 对象的输入)
// src-tauri/src/lib.rs
use tauri::Manager;
tauri::Builder::default()
.setup(|app| {
let webview = app.get_webview_window("main").unwrap();
webview.eval("console.log('hello from Rust')")?;
Ok(())
})
前端调用rust
- 命令
- 事件系统
命令
应用:从Web 应用程序调用 Rust 函数,JSON序列,文件,下载HTTP响应等。接受参数并返回值,可以返回错误并且是异步
的
在 src-tauri/src/lib.rs
文件中定义
1、创建命令,只需添加一个函数并使用 #[tauri::command]
对其进行注释
// src-tauri/src/lib.rs
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JavaScript!");
}
由于 glue 代码生成的限制,
lib.rs
文件中定义的命令无法标记为 pub
。
2、向 builder 函数提供命令列表
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
3、从 JavaScript 代码中调用该命令
// When using the Tauri API npm package:
import { invoke } from '@tauri-apps/api/core';
// When using the Tauri global script (if not using the npm package)
// Be sure to set `app.withGlobalTauri` in `tauri.conf.json` to true
const invoke = window.__TAURI__.core.invoke;
// Invoke the command
invoke('my_custom_command');
在单独模块中定义命令
1、在 src-tauri/src/commands.rs
文件中定义一个命令
// src-tauri/src/commands.rs
#[tauri::command]
pub fn my_custom_command() {
println!("I was invoked from JavaScript!");
}
在单独的模块中定义命令时,它们应该被标记为
pub
命令名称的范围不限于模块,因此即使在模块之间,它们也必须是唯一的
2、在 lib.rs
文件中,定义模块并相应地提供命令列表
// src-tauri/src/lib.rs
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![commands::my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
入参
参数应作为带有 camelCase 键的 JSON 对象传递,参数可以是任何类型,只要它们实现 serde::D eserialize
#[tauri::command]
fn my_custom_command(invoke_message: String) {
println!("I was invoked from JavaScript, with this message: {}", invoke_message);
}
invoke('my_custom_command', { invokeMessage: 'Hello!' });
当使用 Rust 前端调用不带参数的 invoke()
时,你需要调整你的前端代码,如下所示。原因是 Rust 不支持可选参数。
#[wasm_bindgen]
extern "C" {
// invoke without arguments
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
async fn invoke_without_args(cmd: &str) -> JsValue;
// invoke with arguments (default)
#[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])]
async fn invoke(cmd: &str, args: JsValue) -> JsValue;
// They need to have different names!
}
出参
invoke
函数返回一个 Promise,该 Promise 使用返回的值进行 resolve。返回的数据可以是任何类型,只要它实现 serde::Serialize
即可。
#[tauri::command]
fn my_custom_command() -> String {
"Hello from Rust!".into()
}
invoke('my_custom_command').then((message) => console.log(message));
当响应发送到前端时,实现 serde::Serialize
的返回值将序列化为 JSON。如果您尝试返回大型数据(如文件或下载 HTTP 响应),这可能会降低您的应用程序速度。要以优化的方式返回数组缓冲区,请使用 tauri::ipc::Response
use tauri::ipc::Response;
#[tauri::command]
fn read_file() -> Response {
let data = std::fs::read("/path/to/file").unwrap();
tauri::ipc::Response::new(data)
}
Tauri 通道是将数据(例如流式 HTTP 响应)流式传输到前端的推荐机制。
// 读取一个文件,并以 4096 字节的块形式通知前端进度
use tokio::io::AsyncReadExt;
#[tauri::command]
async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) {
// for simplicity this example does not include error handling
let mut file = tokio::fs::File::open(path).await.unwrap();
let mut chunk = vec![0; 4096];
loop {
let len = file.read(&mut chunk).await.unwrap();
if len == 0 {
// Length of zero means end of file.
break;
}
reader.send(&chunk).unwrap();
}
}
错误处理
如果您的处理程序可能失败并且需要能够返回错误,请让函数返回 Result
。
#[tauri::command]
fn login(user: String, password: String) -> Result<String, String> {
if user == "tauri" && password == "tauri" {
// resolve
Ok("logged_in".to_string())
} else {
// reject
Err("invalid credentials".to_string())
}
}
如果命令返回错误,则 promise 将拒绝,否则,它将解析
invoke('login', { user: 'tauri', password: '0j4rijw8=' })
.then((message) => console.log(message))
.catch((error) => console.error(error));
从命令返回的所有内容都必须实现 serde::Serialize
,包括错误。如果你正在使用 Rust 的 std 库或外部 crate 中的错误类型,这可能会有问题,因为大多数错误类型都没有实现它。在简单方案中,您可以使用 map_err
将这些错误转换为 String
s
#[tauri::command]
fn my_custom_command() -> Result<(), String> {
std::fs::File::open("path/to/file").map_err(|err| err.to_string())?;
// Return `null` on success
Ok(())
}
自定义错误类型,使用 thiserror
crate 来帮助创建错误类型
// create the error type that represents all errors possible in our program
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error)
}
// we must manually implement serde::Serialize
impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
fn my_custom_command() -> Result<(), Error> {
// This will return an error
std::fs::File::open("path/that/does/not/exist")?;
// Return `null` on success
Ok(())
}
控制错误类型的序列化方式。为每个错误分配一个代码,更轻松地将其映射到外观相似的 TypeScript 错误枚举
#[derive(Debug, thiserror::Error)]
enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("failed to parse as string: {0}")]
Utf8(#[from] std::str::Utf8Error),
}
#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
enum ErrorKind {
Io(String),
Utf8(String),
}
impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let error_message = self.to_string();
let error_kind = match self {
Self::Io(_) => ErrorKind::Io(error_message),
Self::Utf8(_) => ErrorKind::Utf8(error_message),
};
error_kind.serialize(serializer)
}
}
#[tauri::command]
fn read() -> Result<Vec<u8>, Error> {
let data = std::fs::read("/path/to/file")?;
Ok(data)
}
在你的前端,你现在得到一个 { kind: 'io' | 'utf8', message: string }
error 对象:
type ErrorKind = {
kind: 'io' | 'utf8';
message: string;
};
invoke('read').catch((e: ErrorKind) => {});
异步命令
异步命令使用 async_runtime::spawn
在单独的异步任务上执行。
没有 async 关键字的命令将在主线程上执行,除非使用 #[tauri::command(async)] 定义。
使用 Tauri 创建异步函数时,不能简单地在异步函数的签名中包含借用的参数。此类类型的一些常见示例包括 &str
和 State<'_、Data>
。使用借用的类型时,必须进行其他更改:
- 将类型(例如
&str
)转换为未借用的类似类型(例如String
)。这可能不适用于所有类型的类型,例如State<'_、Data>
。
// Declare the async function using String instead of &str, as &str is borrowed and thus unsupported #[tauri::command] async fn my_custom_command(value: String) -> String { // Call another async function and wait for it to finish some_async_function().await; value }
- 将返回类型包装在
Result
中。适用于所有类型。Result<String, ()>
返回 String,并且没有错误。Result<(), ()>
返回null
。Result<bool, Error>
返回布尔值或错误,如上面的错误处理部分所示。
// Return a Result<String, ()> to bypass the borrowing issue #[tauri::command] async fn my_custom_command(value: &str) -> Result<String, ()> { // Call another async function and wait for it to finish some_async_function().await; // Note that the return value must be wrapped in `Ok()` now. Ok(format!(value)) }
从 JavaScript 调用命令与任何其他命令一样
invoke('my_custom_command', { value: 'Hello, Async!' }).then(() =>
console.log('Completed!')
);
命令的应用
命令可以访问调用该消息的 WebviewWindow
实例
// src-tauri/src/lib.rs
#[tauri::command]
async fn my_custom_command(webview_window: tauri::WebviewWindow) {
println!("WebviewWindow: {}", webview_window.label());
}
命令可以访问 AppHandle
实例
// src-tauri/src/lib.rs
#[tauri::command]
async fn my_custom_command(app_handle: tauri::AppHandle) {
let app_dir = app_handle.path_resolver().app_dir();
use tauri::GlobalShortcutManager;
app_handle.global_shortcut_manager().register("CTRL + U", move || {});
}
Tauri 可以使用 tauri::Builder
上的 manage
函数管理状态。可以使用 tauri::State
在命令上访问状态
// src-tauri/src/lib.rs
struct MyState(String);
#[tauri::command]
fn my_custom_command(state: tauri::State<MyState>) {
assert_eq!(state.0 == "some state value", true);
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(MyState("some state value".into()))
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Tauri 命令还可以访问完整的 tauri::ipc::Request
对象,其中包括原始正文有效负载和请求标头
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("unexpected request body")]
RequestBodyMustBeRaw,
#[error("missing `{0}` header")]
MissingHeader(&'static str),
}
impl serde::Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[tauri::command]
fn upload(request: tauri::ipc::Request) -> Result<(), Error> {
let tauri::ipc::InvokeBody::Raw(upload_data) = request.body() else {
return Err(Error::RequestBodyMustBeRaw);
};
let Some(authorization_header) = request.headers().get("Authorization") else {
return Err(Error::MissingHeader("Authorization"));
};
// upload...
Ok(())
}
在前端,您可以通过在 payload 参数上提供 ArrayBuffer 或 Uint8Array 来调用 invoke() 来发送原始请求正文,并在第三个参数中包含请求标头
const data = new Uint8Array([1, 2, 3]);
await __TAURI__.core.invoke('upload', data, {
headers: {
Authorization: 'apikey',
},
});
创建多个命令
tauri::generate_handler!
宏接受一个命令数组。要注册多个命令,不能多次调用 invoke_handler。将仅使用最后一个调用。您必须将每个命令传递给 tauri::generate_handler!
的单个调用。
// src-tauri/src/lib.rs
#[tauri::command]
fn cmd_a() -> String {
"Command a"
}
#[tauri::command]
fn cmd_b() -> String {
"Command b"
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![cmd_a, cmd_b])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
完整实例
// src-tauri/src/lib.rs
struct Database;
#[derive(serde::Serialize)]
struct CustomResponse {
message: String,
other_val: usize,
}
async fn some_other_function() -> Option<String> {
Some("response".into())
}
#[tauri::command]
async fn my_custom_command(
window: tauri::Window,
number: usize,
database: tauri::State<'_, Database>,
) -> Result<CustomResponse, String> {
println!("Called from {}", window.label());
let result: Option<String> = some_other_function().await;
if let Some(message) = result {
Ok(CustomResponse {
message,
other_val: 42 + number,
})
} else {
Err("No result".into())
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(Database {})
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
// Invocation from JavaScript
invoke('my_custom_command', {
number: 42,
})
.then((res) =>
console.log(`Message: ${res.message}, Other Val: ${res.other_val}`)
)
.catch((e) => console.error(e));
事件系统
应用:更简单的通信机制
限制:不是类型安全的,始终是异步的,无法返回值,并且仅支持 JSON 有效负载。
触发全局事件
使用 event.emit 或 WebviewWindow#emit 函数
import { emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emit(eventName, payload)
emit('file-selected', '/path/to/file');
const appWebview = getCurrentWebviewWindow();
appWebview.emit('route-changed', { url: window.location.href });
全局事件被传送到所有侦听器
触发事件到由特定 Web 视图注册的侦听器
使用 event.emitTo 或 WebviewWindow#emitTo 函数
import { emit } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// emitTo(webviewLabel, eventName, payload)
emitTo('settings', 'settings-update-requested', {
key: 'notification',
value: 'all',
});
const appWebview = getCurrentWebviewWindow();
appWebview.emitTo('editor', 'file-changed', {
path: '/path/to/file',
contents: 'file contents',
});
特定于 Webview 的事件不会触发到常规的全局事件侦听器。要监听任何事件,您必须为 event.listen 函数提供 { target: { kind: 'Any' } }
选项,该函数将监听器定义为作为已发出事件的 catch-all
import { listen } from '@tauri-apps/api/event';
listen(
'state-changed',
(event) => {
console.log('got state changed event', event);
},
{
target: { kind: 'Any' },
}
);
监听全局事件
import { listen } from '@tauri-apps/api/event';
type DownloadStarted = {
url: string;
downloadId: number;
contentLength: number;
};
listen<DownloadStarted>('download-started', (event) => {
console.log(
`downloading ${event.payload.contentLength} bytes from ${event.payload.url}`
);
});
侦听特定于 webview 的事件
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
const appWebview = getCurrentWebviewWindow();
appWebview.listen<string>('logged-in', (event) => {
localStorage.setItem('session-token', event.payload);
});
停止监听事件
listen
函数在应用程序的整个生命周期内保持事件侦听器的注册状态。要停止监听事件,你可以使用 listen
函数返回的 unlisten
函数
import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
once('ready', (event) => {});
const appWebview = getCurrentWebviewWindow();
appWebview.once('ready', () => {});
监听一次事件
import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
once('ready', (event) => {});
const appWebview = getCurrentWebviewWindow();
appWebview.once('ready', () => {});
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)