跳到内容
Tauri

从前端调用 Rust

本文档包含有关如何从应用程序前端与 Rust 代码进行通信的指南。要了解如何从 Rust 代码与前端进行通信,请参阅 从 Rust 调用前端

Tauri 提供了一个用于以类型安全方式访问 Rust 函数的 命令 (command) 原语,以及一个更具动态性的 事件系统

Tauri 提供了一个简单而强大的 command 系统,用于从你的 Web 应用调用 Rust 函数。命令可以接受参数并返回值。它们还可以返回错误并使用 async 修饰。

可以在 src-tauri/src/lib.rs 文件中定义命令。要创建一个命令,只需添加一个函数并使用 #[tauri::command] 进行标注。

src-tauri/src/lib.rs
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JavaScript!");
}

你需要将你的命令列表提供给构建器函数,如下所示:

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");
}

现在,你可以从 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');

如果你的应用程序定义了许多组件,或者它们可以进行分组,你可以将命令定义在单独的模块中,而不是让 lib.rs 文件变得臃肿。

例如,让我们在 src-tauri/src/commands.rs 文件中定义一个命令:

src-tauri/src/commands.rs
#[tauri::command]
pub fn my_custom_command() {
println!("I was invoked from JavaScript!");
}

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");
}

注意命令列表中的 commands:: 前缀,它表示命令函数的完整路径。

本例中的命令名称是 my_custom_command,因此你仍然可以通过在前端执行 invoke("my_custom_command") 来调用它,commands:: 前缀会被忽略。

当使用 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!
}

你的命令处理器可以接收参数。

#[tauri::command]
fn my_custom_command(invoke_message: String) {
println!("I was invoked from JavaScript, with this message: {}", invoke_message);
}

参数应作为带有 camelCase(驼峰命名)键的 JSON 对象传递。

invoke('my_custom_command', { invokeMessage: 'Hello!' });

参数可以是任何类型,只要它们实现了 serde::Deserialize

命令处理器也可以返回数据。

#[tauri::command]
fn my_custom_command() -> String {
"Hello from Rust!".into()
}

invoke 函数返回一个 Promise,该 Promise 会解析为返回的值。

invoke('my_custom_command').then((message) => console.log(message));

返回的数据可以是任何类型,只要它实现了 serde::Serialize

当响应发送到前端时,实现了 serde::Serialize 的返回值会被序列化为 JSON。如果你尝试返回大量数据(如文件或 HTTP 下载响应),这可能会减慢你的应用程序速度。要以优化的方式返回 Array Buffers,请使用 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)
}

如果你的处理器可能会失败并需要返回一个错误,请让该函数返回一个 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 将会被拒绝 (reject);否则,它会解析 (resolve)。

invoke('login', { user: 'tauri', password: '0j4rijw8=' })
.then((message) => console.log(message))
.catch((error) => console.error(error));

如上所述,从命令返回的所有内容都必须实现 serde::Serialize,包括错误。如果你正在处理来自 Rust 标准库或外部 crate 的错误类型,这可能会有问题,因为大多数错误类型并未实现该 trait。在简单的场景下,你可以使用 map_err 将这些错误转换为 String

#[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(())
}

由于这不够惯用,你可能希望创建自己的错误类型,并实现 serde::Serialize。在下面的示例中,我们使用 thiserror crate 来帮助创建错误类型。它允许你通过派生 thiserror::Error trait 将枚举转换为错误类型。你可以查阅其文档以获取更多详细信息。

// 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 } 的错误对象。

type ErrorKind = {
kind: 'io' | 'utf8';
message: string;
};
invoke('read').catch((e: ErrorKind) => {});

在 Tauri 中,建议使用异步命令来执行繁重的任务,以避免 UI 卡顿或变慢。

如果你的命令需要异步运行,只需将其声明为 async 即可。

处理借用类型时,你必须进行额外的更改。这是你的两个主要选项:

选项 1:将类型(如 &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
}

选项 2:将返回类型包装在 Result 中。这实现起来稍微困难一点,但适用于所有类型。

使用返回类型 Result<a, b>,将 a 替换为你想要返回的类型,或者如果你想返回 null 则使用 ();将 b 替换为如果出错时要返回的错误类型,或者如果你不想返回可选错误则使用 ()。例如:

  • 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 调用命令已经返回一个 Promise,它的工作方式与其他任何命令一样。

invoke('my_custom_command', { value: 'Hello, Async!' }).then(() =>
console.log('Completed!')
);

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();
}
}

有关更多信息,请参阅 通道文档

命令可以访问调用消息的 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().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(())
}

在前端,你可以调用 invoke() 并通过在负载参数中提供 ArrayBuffer 或 Uint8Array 来发送原始请求主体,并在第三个参数中包含请求头。

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));

事件系统是一种在你的前端和 Rust 之间进行通信的更简单的机制。与命令不同,事件不是类型安全的,总是异步的,不能返回值,并且仅支持 JSON 负载。

要触发全局事件,你可以使用 event.emitWebviewWindow#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 });

要向由特定 webview 注册的监听器触发事件,你可以使用 event.emitToWebviewWindow#emitTo 函数。

import { emitTo } 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',
});

@tauri-apps/api NPM 包提供了监听全局事件和 webview 特定事件的 API。

  • 监听全局事件:

    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();

此外,Tauri 提供了一个用于仅监听一次事件的实用函数:

import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
once('ready', (event) => {});
const appWebview = getCurrentWebviewWindow();
appWebview.once('ready', () => {});

全局事件和 webview 特定事件也会发送到在 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);
}
});

此外,Tauri 提供了一个用于仅监听一次事件的实用函数:

app.once("ready", |event| {
println!("app is ready");
});

在这种情况下,事件监听器在第一次触发后会立即取消注册。

要了解如何监听事件以及从 Rust 代码中触发事件,请参阅 Rust 事件系统文档


© 2026 Tauri 贡献者。CC-BY / MIT