From d60cfe9951edbc062de5522be61e93cbf35c811a Mon Sep 17 00:00:00 2001 From: KikkiZ <64997288+KikkiZ@users.noreply.github.com> Date: Fri, 27 Jun 2025 00:35:03 +0800 Subject: [PATCH] i18n(zh-cn): translate /develop/calling-rust.mdx (#3324) Co-authored-by: Paul Valladares <85648028+dreyfus92@users.noreply.github.com> --- src/content/docs/develop/calling-rust.mdx | 7 +- .../develop/_sections/frontend-listen.mdx | 141 ++++ .../docs/zh-cn/develop/calling-frontend.mdx | 270 +++++++ .../docs/zh-cn/develop/calling-rust.mdx | 699 ++++++++++++++++++ 4 files changed, 1114 insertions(+), 3 deletions(-) create mode 100644 src/content/docs/zh-cn/develop/_sections/frontend-listen.mdx create mode 100644 src/content/docs/zh-cn/develop/calling-frontend.mdx create mode 100644 src/content/docs/zh-cn/develop/calling-rust.mdx diff --git a/src/content/docs/develop/calling-rust.mdx b/src/content/docs/develop/calling-rust.mdx index 90203f647..509485da1 100644 --- a/src/content/docs/develop/calling-rust.mdx +++ b/src/content/docs/develop/calling-rust.mdx @@ -36,7 +36,7 @@ Command names must be unique. Commands defined in the `lib.rs` file cannot be marked as `pub` due to a limitation in the glue code generation. You will see an error like this if you mark it as a public function: -```` +``` error[E0255]: the name `__cmd__command_name` is defined multiple times --> src/lib.rs:28:8 | @@ -47,6 +47,7 @@ error[E0255]: the name `__cmd__command_name` is defined multiple times | = note: `__cmd__command_name` must be defined only once in the macro namespace of this module ``` + ::: You will have to provide a list of your commands to the builder function like so: @@ -59,7 +60,7 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } -```` +``` Now, you can invoke the command from your JavaScript code: @@ -236,7 +237,7 @@ invoke('login', { user: 'tauri', password: '0j4rijw8=' }) As mentioned above, everything returned from commands must implement [`serde::Serialize`], including errors. This can be problematic if you're working with error types from Rust's std library or external crates as most error types do not implement it. -In simple scenarios you can use `map_err` to convert these errors to `String`s: +In simple scenarios you can use `map_err` to convert these errors to `String`: ```rust #[tauri::command] diff --git a/src/content/docs/zh-cn/develop/_sections/frontend-listen.mdx b/src/content/docs/zh-cn/develop/_sections/frontend-listen.mdx new file mode 100644 index 000000000..8f1190a1c --- /dev/null +++ b/src/content/docs/zh-cn/develop/_sections/frontend-listen.mdx @@ -0,0 +1,141 @@ +--- +title: 从 Rust 调用前端 +--- + +`@tauri-apps/api` NPM 包提供 API 来监听全局事件和特定于 webview 的事件。 + +- 监听全局事件 + + ```ts + import { listen } from '@tauri-apps/api/event'; + + type DownloadStarted = { + url: string; + downloadId: number; + contentLength: number; + }; + + listen('download-started', (event) => { + console.log( + `downloading ${event.payload.contentLength} bytes from ${event.payload.url}` + ); + }); + ``` + +- 监听特定的 webview 事件 + + ```ts + import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; + + const appWebview = getCurrentWebviewWindow(); + appWebview.listen('logged-in', (event) => { + localStorage.setItem('session-token', event.payload); + }); + ``` + +`listen` 函数会在应用程序的整个生命周期内保持事件监听器的注册。 +要停止监听某个事件,可以使用 `listen` 函数返回的 `unlisten` 函数: + +```js +import { listen } from '@tauri-apps/api/event'; + +const unlisten = await listen('download-started', (event) => {}); +unlisten(); +``` + +:::note +当执行上下文超出范围时(例如卸载组件时),请始终使用 unlisten 函数。 + +当页面重新加载或导航至其他 URL 时,监听器将自动注销。但这不适用于单页应用(SPA)路由器。 +::: + +此外, Tauri 还提供了一个实用函数,用于精确监听一次事件: + +```js +import { once } from '@tauri-apps/api/event'; +import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; + +once('ready', (event) => {}); + +const appWebview = getCurrentWebviewWindow(); +appWebview.once('ready', () => {}); +``` + +:::note +前端发出的事件也会触发这些 API 注册的监听器。更多信息请参阅 [从前端调用 Rust] 文档。 +::: + +#### 监听 Rust 上的事件 + +全局事件和特定于 webview 的事件也会传递给在 Rust 中注册的监听器。 + +- 监听全局事件 + + ```rust title="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::(&event.payload()) { + println!("downloading {}", payload.url); + } + }); + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); + } + ``` + +- 监听特定的 webview 事件 + + ```rust title="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` 函数: + +```rust +// 在事件处理程序作用域外取消监听: +let event_id = app.listen("download-started", |event| {}); +app.unlisten(event_id); + +// 当满足某些事件条件时取消监听: +let handle = app.handle().clone(); +app.listen("status-changed", |event| { + if event.data == "ready" { + handle.unlisten(event.id); + } +}); +``` + +此外, Tauri 还提供了一个实用函数,用于精确监听一次事件: + +```rust +app.once("ready", |event| { + println!("app is ready"); +}); +``` + +在这种情况下,事件监听器在第一次触发后立即取消注册。 + +[从前端调用 Rust]: /develop/calling-rust/ diff --git a/src/content/docs/zh-cn/develop/calling-frontend.mdx b/src/content/docs/zh-cn/develop/calling-frontend.mdx new file mode 100644 index 000000000..b935f0a30 --- /dev/null +++ b/src/content/docs/zh-cn/develop/calling-frontend.mdx @@ -0,0 +1,270 @@ +--- +title: 从 Rust 调用前端 +i18nReady: true +--- + +import { Content as FrontendListen } from './_sections/frontend-listen.mdx'; + +本文档包含如何从 Rust 代码与应用程序前端通信的指南。 +要了解如何从前端与 Rust 代码通信,请参阅 [从前端调用 Rust] 。 + +Tauri 应用程序的 Rust 端可以利用 Tauri 事件系统、 +使用通道或直接评估 JavaScript 代码来调用前端。 + +## 事件系统 + +Tauri 提供了一个简单的事件系统,您可以使用它在 Rust 和前端之间进行双向通信。 + +事件系统是为需要传输少量数据或需要实现多消费者多生产者模式(例如推送通知系统)的情况而设计的。 + +事件系统并非为低延迟或高吞吐量场景而设计。 +请参阅 [通道部分](#通道) ,了解针对流数据优化的实现。 + +Tauri 命令和 Tauri 事件之间的主要区别在于,事件没有强类型支持, +事件有效负载始终是 JSON 字符串,这使得它们不适合更大的消息, +并且不支持 [功能] 系统对事件数据和渠道进行细粒度控制。 + +[AppHandle] 和 [WebviewWindow] 类型实现了事件系统特征 [Listener] 和 [Emitter] 。 + +事件要么是全局的(传递给所有监听器),要么是特定于 webview 的(仅传递给与给定标签匹配的 webview)。 + +### 全局事件 + +要触发全局事件,您可以使用 [Emitter#emit] 函数: + +```rust title="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", progress).unwrap(); + } + app.emit("download-finished", &url).unwrap(); +} +``` + +:::note +全局事件传递给**所有**监听者 +::: + +### Webview 事件 + +要向特定 webview 注册的监听器触发事件​​,您可以使用 [Emitter#emit_to] 函数: + +```rust title="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] 来触发 webview 列表的事件。 +以下示例中,我们向主 webview 和文件查看器 webview 发出一个打开文件事件: + +```rust title="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(); +} +``` + +:::note +Webview 特有的事件**不会**被触发到常规的全局事件监听器中。 +要监听**任何**事件,您必须使用 `listen_any` 函数而不是 `listen` 函数, +后者将监听器定义为所有已发出事件的集合。 +::: + +### 事件负载 + +事件负载可以是任何 [可序列化][Serialize] 的类型,只要它实现了 [Clone] 接口。 +让我们来进一步改进下载事件的示例,使用一个对象在每个事件中发出更多信息: + +```rust title="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(); +} +``` + +### 监听事件 + +Tauri 提供 API 来监听 webview 和 Rust 中的事件。 + +#### 监听前端事件 + + + +## 通道 + +事件系统旨在提供简单的双向通信,并可在应用程序中全局使用。 +其底层直接执行 JavaScript 代码,因此可能不适合发送大量数据。 + +通道旨在快速传递有序数据。它们在内部用于流式传输操作, +例如下载进度、子进程输出和 WebSocket 消息。 + +让我们重写下载命令示例以使用通道而不是事件系统: + +```rust title="src-tauri/src/lib.rs" +use tauri::{AppHandle, ipc::Channel}; +use serde::Serialize; + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase", rename_all_fields = "camelCase", tag = "event", content = "data")] +enum DownloadEvent<'a> { + Started { + url: &'a str, + download_id: usize, + content_length: usize, + }, + Progress { + download_id: usize, + chunk_length: usize, + }, + Finished { + download_id: usize, + }, +} + +#[tauri::command] +fn download(app: AppHandle, url: String, on_event: Channel) { + 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(); +} +``` + +调用下载命令时,您必须创建通道并将其作为参数提供: + +```ts +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(); +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, +}); +``` + +## 执行 JavaScript + +要在 webview 上下文中直接执行任何 JavaScript 代码,您可以使用 [`WebviewWindow#eval`] 函数: + +```rust title="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 对象的输入, +我们建议使用 [serialize-to-javascript] 包。 + +[`WebviewWindow#eval`]: https://docs.rs/tauri/2.0.0/tauri/webview/struct.WebviewWindow.html#method.eval +[serialize-to-javascript]: https://docs.rs/serialize-to-javascript/latest/serialize_to_javascript/ +[AppHandle]: https://docs.rs/tauri/2.0.0/tauri/struct.AppHandle.html +[WebviewWindow]: https://docs.rs/tauri/2.0.0/tauri/webview/struct.WebviewWindow.html +[Listener]: https://docs.rs/tauri/2.0.0/tauri/trait.Listener.html +[Emitter]: https://docs.rs/tauri/2.0.0/tauri/trait.Emitter.html +[Emitter#emit]: https://docs.rs/tauri/2.0.0/tauri/trait.Emitter.html#tymethod.emit +[Emitter#emit_to]: https://docs.rs/tauri/2.0.0/tauri/trait.Emitter.html#tymethod.emit_to +[Emitter#emit_filter]: https://docs.rs/tauri/2.0.0/tauri/trait.Emitter.html#tymethod.emit_filter +[Clone]: https://doc.rust-lang.org/std/clone/trait.Clone.html +[Serialize]: https://serde.rs/impl-serialize.html +[从前端调用 Rust]: /develop/calling-rust/ +[功能]: /security/capabilities/ diff --git a/src/content/docs/zh-cn/develop/calling-rust.mdx b/src/content/docs/zh-cn/develop/calling-rust.mdx new file mode 100644 index 000000000..566f6855b --- /dev/null +++ b/src/content/docs/zh-cn/develop/calling-rust.mdx @@ -0,0 +1,699 @@ +--- +title: 从前端调用 Rust +i18nReady: true +--- + +import { Content as FrontendListen } from './_sections/frontend-listen.mdx'; + +本文档包含有关如何从应用程序前端与 Rust 代码通信的指南。 +要了解如何从 Rust 代码与前端通信,请参阅 [从 Rust 调用前端] 。 + +Tauri 提供了一个 [命令](#命令) 原语,用于以类型安全的方式访问 Rust 函数,以及一个更加动态的 [事件系统](#事件系统) 。 + +## 命令 + +Tauri 提供了一个简单但功能强大的 `command` 系统,用于从 Web 应用调用 Rust 函数。 +命令可以接受参数并返回结果。它们还可以返回错误或是 `async` 执行。 + +### 基础示例 + +您可以在 `src-tauri/src/lib.rs` 文件中定义命令。 +要创建命令,只需添加一个函数并用 `#[tauri::command]` 注释它: + +```rust title="src-tauri/src/lib.rs" +#[tauri::command] +fn my_custom_command() { + println!("I was invoked from JavaScript!"); +} +``` + +:::note +命令名称必须是唯一的。 +::: + +:::note +由于胶水代码生成的限制, `lib.rs` 文件中定义的命令无法标记为 `pub` 。 +如果将其标记为公共函数,您将看到如下错误: + +``` +error[E0255]: the name `__cmd__command_name` is defined multiple times + --> src/lib.rs:28:8 + | +27 | #[tauri::command] + | ----------------- previous definition of the macro `__cmd__command_name` here +28 | pub fn x() {} + | ^ `__cmd__command_name` reimported here + | + = note: `__cmd__command_name` must be defined only once in the macro namespace of this module +``` + +::: + +您必须向构建器函数提供命令列表,如下所示: + +```rust title="src-tauri/src/lib.rs" ins={4} +#[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 代码调用该命令: + +```javascript +// 使用 Tauri API npm 包时: +import { invoke } from '@tauri-apps/api/core'; + +// 使用 Tauri 全局脚本(不使用 npm 包时) +// 确保在 `tauri.conf.json` 中设置 `app.withGlobalTauri` 为 true +const invoke = window.__TAURI__.core.invoke; + +// 调用命令 +invoke('my_custom_command'); +``` + +#### 在独立的模块中定义命令 + +如果您的应用程序定义了很多组件或者对它们进行了分组, +那么您可以在单独的模块中定义命令,而不是使 `lib.rs` 文件变得臃肿。 + +作为示例,我们在 `src-tauri/src/commands.rs` 文件中定义一个命令: + +```rust title="src-tauri/src/commands.rs" +#[tauri::command] +pub fn my_custom_command() { + println!("I was invoked from JavaScript!"); +} +``` + +:::note +在单独的模块中定义命令时,应将其标记为 `pub` 。 +::: + +:::note +命令名称不局限于模块,因此即使在模块之间它们也必须是唯一的。 +::: + +在 `lib.rs` 文件中,定义模块并相应地提供命令列表; + +```rust title="src-tauri/src/lib.rs" ins={6} +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::` 前缀将被忽略。 + +#### WASM + +当前端使用不带参数的 `invoke()` 调用 `Rust` 时,您需要按如下方式调整前端代码。 +原因是 Rust 不支持可选参数。 + +```rust ins={4-5} +#[wasm_bindgen] +extern "C" { + // 没有参数的 invoke + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)] + async fn invoke_without_args(cmd: &str) -> JsValue; + + // (默认)含参的 invoke + #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] + async fn invoke(cmd: &str, args: JsValue) -> JsValue; + + // 他们需要拥有不同的名字! +} +``` + +### 传递参数 + +您的命令处理程序可以接受参数: + +```rust +#[tauri::command] +fn my_custom_command(invoke_message: String) { + println!("I was invoked from JavaScript, with this message: {}", invoke_message); +} +``` + +参数应以小驼峰命名,并作为 JSON 对象的键的传递: + +```javascript +invoke('my_custom_command', { invokeMessage: 'Hello!' }); +``` + +:::note +您可以将 `snake_case` 用于具有 `rename_all` 属性的参数: + +```rust +#[tauri::command(rename_all = "snake_case")] +fn my_custom_command(invoke_message: String) {} +``` + +```javascript +invoke('my_custom_command', { invoke_message: 'Hello!' }); +``` + +::: + +参数可以是任何类型,只要它们实现 [`serde::Deserialize`] 。 + +相应的 JavaScript 代码: + +```javascript +invoke('my_custom_command', { invoke_message: 'Hello!' }); +``` + +### 返回数据 + +命令处理程序也可以返回数据: + +```rust +#[tauri::command] +fn my_custom_command() -> String { + "Hello from Rust!".into() +} +``` + +`invoke` 函数返回一个使用返回值解析的 `promise` : + +```javascript +invoke('my_custom_command').then((message) => console.log(message)); +``` + +返回的数据可以是任何类型,只要它实现 [`serde::Serialize`] 。 + +#### 返回数组缓冲区 + +实现了 [`serde::Serialize`] 的返回值在将结果发送到前端时被序列化为 JSON。 +如果您尝试返回大量数据(例如文件或下载 HTTP response ),这可能会减慢应用程序的速度。 +要以优化的方式返回数组缓冲区,请使用 [`tauri::ipc::Response`] : + +```rust +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` : + +```rust +#[tauri::command] +fn login(user: String, password: String) -> Result { + if user == "tauri" && password == "tauri" { + // resolve + Ok("logged_in".to_string()) + } else { + // reject + Err("invalid credentials".to_string()) + } +} +``` + +如果命令返回错误,则 promise 将抛出错误,否则,它将正常运行: + +```javascript +invoke('login', { user: 'tauri', password: '0j4rijw8=' }) + .then((message) => console.log(message)) + .catch((error) => console.error(error)); +``` + +如上所述,命令返回的所有内容都必须实现 [`serde::Serialize`] ,包括错误。 +如果您使用 Rust 的 std 库或外部包中的错误类型,这可能会有问题,因为大多数错误类型都没有实现它。 +在简单的场景中,您可以使用 `map_err` 将这些错误转换为 `String` : + +```rust +#[tauri::command] +fn my_custom_command() -> Result<(), String> { + std::fs::File::open("path/to/file").map_err(|err| err.to_string())?; + // 成功时返回 `null` + Ok(()) +} +``` + +由于这不太符合惯用习惯,您可能需要创建自己的错误类型,该类型实现了 `serde::Serialize` 。 +在下面的示例中,我们使用 [`thiserror`] crate 来帮助创建错误类型。 +它允许您通过派生 `thiserror::Error` 特质将枚举转换为错误类型。 +您可以查阅其文档以了解更多详细信息。 + +```rust +// 创建在我们程序中可能发生的所有错误 +#[derive(Debug, thiserror::Error)] +enum Error { + #[error(transparent)] + Io(#[from] std::io::Error) +} + +// 我们需要手动实现 serde::Serialize +impl serde::Serialize for Error { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +#[tauri::command] +fn my_custom_command() -> Result<(), Error> { + // 这里将会返回一个错误 + std::fs::File::open("path/that/does/not/exist")?; + // 成功时返回 `null` + Ok(()) +} +``` + +自定义错误类型的优点是可以明确显示所有可能的错误,以便读者能够快速识别可能发生的错误。 +这可以为其他人(以及您自己)在以后审查和重构代码时节省大量时间。 + +它还可以让你完全控制错误类型的序列化方式。在上面的例子中, +我们只是将错误消息作为字符串返回,但你可以为每个错误分配一个代码, +这样就可以更轻松地将其映射到类似的 TypeScript 错误枚举,例如: + +```rust +#[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(&self, serializer: S) -> Result + 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, Error> { + let data = std::fs::read("/path/to/file")?; + Ok(data) +} +``` + +在您的前端您现在会得到一个 `{ kind: 'io' | 'utf8', message: string }` 错误对象: + +```ts +type ErrorKind = { + kind: 'io' | 'utf8'; + message: string; +}; + +invoke('read').catch((e: ErrorKind) => {}); +``` + +### 异步命令 + +Tauri 优先使用异步命令来执行繁重的工作,而不会导致 UI 冻结或减速。 + +:::note + +异步命令使用 [`async_runtime::spawn`] 在单独的异步任务上执行。 +除非使用 _#[tauri::command(async)]_ 定义,否则没有 _async_ 关键字的命令将在主线程上执行。 + +::: + +**如果您的命令需要异步运行,只需将其声明为 `async` 。** + +:::caution + +使用 Tauri 创建异步函数时需要小心谨慎。 +目前,您不能简单地在异步函数的签名中包含借用的参数。 +此类类型的一些常见示例是 `&str` 和 `State<'_, Data>` 。 +此限制在此处跟踪: https://github.com/tauri-apps/tauri/issues/2533 ,解决方法如下所示。 + +::: + +使用借用类型时,您必须进行额外的更改。以下是您的两个主要选项: + +**选项 1**:将类型转换为非借用类型,例如 `&str` 转为 `String` 。这可能不适用于所有类型,例如 `State<'_, Data>` 。 + +_例子:_ + +```rust +// 在声明异步函数时使用 String 而不是 &str,因为 &str 是借用的,因此不支持 +#[tauri::command] +async fn my_custom_command(value: String) -> String { + // 调用另一个异步函数并等待它完成 + some_async_function().await; + value +} +``` + +**选项 2**:将返回类型包装在 [`Result`] 中。这个实现起来有点困难,但适用于所有类型。 + +使用返回类型 `Result` ,将 `a` 替换为您希望返回的类型,或者 `()` 如果您希望返回 `null` , +并将 `b` 替换为错误类型以在出现问题时返回,或者 `()` 如果您希望不返回可选错误。例如: + +- `Result` 返回一个 String ,且不会返回错误。 +- `Result<(), ()>` 返回 `null`。 +- `Result` 返回一个 bool 值或一个错误,如上面的 [错误处理](#错误处理) 部分所示。 + +_例子:_ + +```rust +// 返回一个 Result 以绕过借用问题 +#[tauri::command] +async fn my_custom_command(value: &str) -> Result { + // 调用另一个异步函数并等待它完成 + some_async_function().await; + // 注意:返回值必须用 Ok() 包装。 + Ok(format!(value)) +} +``` + +##### 从 JavaScript 调用 + +由于从 JavaScript 调用命令已经返回一个 promise ,因此它的工作方式与任何其他命令一样: + +```javascript +invoke('my_custom_command', { value: 'Hello, Async!' }).then(() => + console.log('Completed!') +); +``` + +### 通道 + +Tauri 通道是推荐的流式数据传输机制,例如将 HTTP 响应流式传输到前端。 +以下示例读取一个文件,并以 4096 字节为单位的块通知前端传输进度: + +```rust +use tokio::io::AsyncReadExt; + +#[tauri::command] +async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) { + // 为了简单起见,本示例未包含错误处理 + 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 { + // 读到文件末尾时结束循环 + break; + } + reader.send(&chunk).unwrap(); + } +} +``` + +请参阅 [通道文档] 以了解更多信息。 + +### 在命令中访问 WebviewWindow + +命令可以访问调用该消息的 `WebviewWindow` 实例: + +```rust title="src-tauri/src/lib.rs" +#[tauri::command] +async fn my_custom_command(webview_window: tauri::WebviewWindow) { + println!("WebviewWindow: {}", webview_window.label()); +} +``` + +### 在命令中访问 AppHandle + +命令可以访问 `AppHandle` 实例: + +```rust title="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` 在命令中访问状态: + +```rust title="src-tauri/src/lib.rs" +struct MyState(String); + +#[tauri::command] +fn my_custom_command(state: tauri::State) { + 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` 对象,其中包括原始主体有效负载和请求标头。 + +```rust +#[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(&self, serializer: S) -> Result + 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()` 通过在 payload 参数上提供 ArrayBuffer +或 Uint8Array 来发送原始请求主体,并在第三个参数中包含请求标头: + +```js +const data = new Uint8Array([1, 2, 3]); +await __TAURI__.core.invoke('upload', data, { + headers: { + Authorization: 'apikey', + }, +}); +``` + +### 创建多个命令 + +`tauri::generate_handler!` 宏接受一个命令数组。要注册多个命令,你不能多次调用 invoke_handler。 +只有最后一个将使用 call 。您必须将每个命令传递给` tauri::generate_handler!` 。 + +```rust title="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"); +} +``` + +### 完整示例 + +上述特性中的任何或所有功能都可以组合使用: + +```rust title="src-tauri/src/lib.rs" +struct Database; + +#[derive(serde::Serialize)] +struct CustomResponse { + message: String, + other_val: usize, +} + +async fn some_other_function() -> Option { + Some("response".into()) +} + +#[tauri::command] +async fn my_custom_command( + window: tauri::Window, + number: usize, + database: tauri::State<'_, Database>, +) -> Result { + println!("Called from {}", window.label()); + let result: Option = 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"); +} +``` + +```javascript +import { invoke } from '@tauri-apps/api/core'; + +// 从 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.emit] 或 [WebviewWindow#emit] 函数: + +```js +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 }); +``` + +:::note +全局事件将传递给**所有**监听者 +::: + +### Webview 事件 + +要向特定 webview 注册的监听器触发事件​​,您可以使用 [event.emitTo] 或 [WebviewWindow#emitTo] 函数: + +```js +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', +}); +``` + +:::note +Webview 特有的事件**不会**被触发到常规的全局事件监听器中。 +要监听**所有**事件,您必须为 [event.listen] 函数提供 `{ target: { kind: 'Any' } }` +选项,该选项将监听器定义为所有已发出事件的集合: + +```js +import { listen } from '@tauri-apps/api/event'; +listen( + 'state-changed', + (event) => { + console.log('got state changed event', event); + }, + { + target: { kind: 'Any' }, + } +); +``` + +::: + +### 监听事件 + +. 要了解如何从 Rust 代码中监听事件和发出事件,请参阅 [Rust +事件系统文档] 。 + +[从 Rust 调用前端]: /develop/calling-frontend/ +[`async_runtime::spawn`]: https://docs.rs/tauri/2.0.0/tauri/async_runtime/fn.spawn.html +[`serde::serialize`]: https://docs.serde.rs/serde/trait.Serialize.html +[`serde::deserialize`]: https://docs.serde.rs/serde/trait.Deserialize.html +[`tauri::ipc::Response`]: https://docs.rs/tauri/2.0.0/tauri/ipc/struct.Response.html +[`tauri::ipc::Request`]: https://docs.rs/tauri/2.0.0/tauri/ipc/struct.Request.html +[`thiserror`]: https://github.com/dtolnay/thiserror +[`result`]: https://doc.rust-lang.org/std/result/index.html +[event.emit]: /reference/javascript/api/namespaceevent/#emit +[event.listen]: /reference/javascript/api/namespaceevent/#listen +[WebviewWindow#emit]: /reference/javascript/api/namespacewebviewwindow/#emit +[event.emitTo]: /reference/javascript/api/namespaceevent/#emitto +[WebviewWindow#emitTo]: /reference/javascript/api/namespacewebviewwindow/#emitto +[Rust 事件系统文档]: /develop/calling-frontend/#event-system +[通道文档]: /develop/calling-frontend/#channels +[Calling Rust from the Frontend]: /develop/calling-rust/