Skip to content

从 Rust 调用前端

本文档包含了关于如何从你的 Rust 代码与应用前端进行通信的指南。要查看如何从前端与你的 Rust 代码进行通信,请参见 [从前端调用 Rust]。

🌐 This document includes guides on how to communicate with your application frontend from your Rust code. To see how to communicate with your Rust code from your frontend, see Calling Rust from the Frontend.

你 Tauri 应用的 Rust 端可以通过利用 Tauri 事件系统、使用通道或直接执行 JavaScript 代码来调用前端。

🌐 The Rust side of your Tauri application can call the frontend by leveraging the Tauri event system, using channels or directly evaluating JavaScript code.

🌐 Event System

Tauri 提供了一个简单的事件系统,你可以使用它在 Rust 和前端之间进行双向通信。

🌐 Tauri ships a simple event system you can use to have bi-directional communication between Rust and your frontend.

事件系统是为需要流式传输少量数据的情况设计的,或者你需要实现多消费者多生产者模式(例如推送通知系统)。

🌐 The event system was designed for situations where small amounts of data need to be streamed or you need to implement a multi consumer multi producer pattern (e.g. push notification system).

事件系统并非为低延迟或高吞吐量的情况设计。请参阅通道部分以获取针对流数据优化的实现。

🌐 The event system is not designed for low latency or high throughput situations. See the channels section for the implementation optimized for streaming data.

Tauri 命令和 Tauri 事件之间的主要区别在于,事件没有强类型支持,事件负载始终是 JSON 字符串,这使它们不适合较大的消息,并且没有对 capabilities 系统的支持来对事件数据和通道进行精细控制。

🌐 The major differences between a Tauri command and a Tauri event are that events have no strong type support, event payloads are always JSON strings making them not suitable for bigger messages and there is no support of the capabilities system to fine grain control event data and channels.

AppHandleWebviewWindow 类型实现了事件系统特性 ListenerEmitter

🌐 The AppHandle and WebviewWindow types implement the event system traits Listener and Emitter.

事件要么是全局的(传递给所有监听器),要么是特定于 WebView 的(只传递给与给定标签匹配的 WebView)。

🌐 Events are either global (delivered to all listeners) or webview-specific (only delivered to the webview matching a given label).

🌐 Global Events

要触发全局事件,你可以使用 Emitter#emit 函数:

🌐 To trigger a global event you can use the Emitter#emit function:

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

🌐 Webview Event

要向特定 webview 注册的监听器触发事件,可以使用 Emitter#emit_to 函数:

🌐 To trigger an event to a listener registered by a specific webview you can use the Emitter#emit_to function:

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 触发事件。在以下示例中,我们向 main 和 file-viewer WebView 发出 open-file 事件:

🌐 It is also possible to trigger an event to a list of webviews by calling Emitter#emit_filter. In the following example we emit a open-file event to the main and file-viewer webviews:

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

🌐 Event Payload

事件负载可以是任何也实现了 Clone可序列化 类型。 让我们通过使用一个对象在每个事件中发出更多信息来增强下载事件示例:

🌐 The event payload can be any serializable type that also implements Clone. Let’s enhance the download event example by using an object to emit more information in each event:

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

🌐 Listening to Events

Tauri 提供了用于监听 Webview 和 Rust 接口上的事件的 API。

🌐 Tauri provides APIs to listen to events on both the webview and the Rust interfaces.

🌐 Listening to Events on the Frontend

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

🌐 The @tauri-apps/api NPM package offers APIs to listen to both global and webview-specific events.

  • 聆听全球事件

    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 函数:

🌐 The listen function keeps the event listener registered for the entire lifetime of the application. To stop listening on an event you can use the unlisten function which is returned by the listen function:

import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});
unlisten();

🌐 Common Pitfalls

在监听器解析之前不要调用 unlisten()
Section titled “在监听器解析之前不要调用 unlisten()”

🌐 Don’t call unlisten() before the listener resolves

listen 函数返回一个 Promise,该 Promise 会解析为 unlisten 句柄。
如果在 Promise 解析之前同步调用 unlisten,处理程序将会立即被移除,你将不会收到任何事件:

🌐 The listen function returns a Promise that resolves to the unlisten handle. If you call unlisten synchronously before the Promise resolves, the handler will be removed immediately and you won’t receive any events:

// Wrong: unlisten is called before the listener is registered
const unlisten = listen('sync-complete', (event) => {
console.log('sync finished');
});
unlisten(); // unlisten is a Promise here, not a function -- the listener is not cleaned up
// Correct: await the Promise to get the unlisten handle
const unlisten = await listen('sync-complete', (event) => {
console.log('sync finished');
});
// Now you can store and call it later, e.g. in a cleanup function
unlisten();

🌐 Timing in setup hooks

在像 React、Vue 和 Svelte 这样的框架中,setup 或 mount 钩子会在组件完全渲染之前运行。如果你在 setup 期间监听事件,请确保事件处理程序不依赖尚未渲染的 DOM 元素,或者将监听器注册推迟到在挂载后运行的 effect/钩子中。

🌐 In frameworks like React, Vue, and Svelte, the setup or mount hook runs before the component is fully rendered. If you listen for events during setup, make sure the event handler does not depend on DOM elements that haven’t been rendered yet, or defer the listener registration to an effect/hook that runs after mount.

// Wrong: DOM ref may not be available yet
function MyComponent() {
const ref = useRef(null);
listen('scroll-to', (event) => {
ref.current.scrollIntoView(); // ref.current may be null during setup
});
return <div ref={ref} />;
}
// Correct: use useEffect which runs after the component mounts
function MyComponent() {
const ref = useRef(null);
useEffect(() => {
const unlisten = listen('scroll-to', (event) => {
ref.current?.scrollIntoView();
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
return <div ref={ref} />;
}

🌐 Event ordering and async listeners

事件监听器会按照注册的顺序被调用,但如果一个监听器是异步的,并且事件触发器在短时间内发送多个事件,监听器可能会乱序处理事件。对于有序、高吞吐量的数据传输,可以考虑使用 Channels 而不是事件系统。

🌐 Event listeners are called in the order they are registered, but if a listener is async and the event emitter sends multiple events in rapid succession, the listeners may process events out of order. For ordered, high-throughput data delivery, consider using Channels instead of the event system.

🌐 Framework-specific Cleanup Examples

在使用前端框架时,当组件被卸载时应该清理事件监听器,以避免内存泄漏和重复的处理程序。

🌐 When using a frontend framework, you should clean up event listeners when a component is unmounted to avoid memory leaks and duplicate handlers.

import { useEffect, useState } from 'react';
import { listen } from '@tauri-apps/api/event';
function DownloadTracker() {
const [progress, setProgress] = useState(0);
useEffect(() => {
const unlisten = listen<number>('download-progress', (event) => {
setProgress(event.payload);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
return <div>Download progress: {progress}%</div>;
}

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

🌐 Additionally Tauri provides a utility function for listening to an event exactly once:

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

🌐 Listening to Events on Rust

全局和特定于 webview 的事件也会传递给在 Rust 中注册的监听器。

🌐 Global and webview-specific events are also delivered to listeners registered in 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 函数:

🌐 The listen function keeps the event listener registered for the entire lifetime of the application. To stop listening on an event you can use the unlisten function:

// 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 提供了一个用于仅监听一次事件的实用函数:

🌐 Additionally Tauri provides a utility function for listening to an event exactly once:

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

在这种情况下,事件监听器在第一次触发后立即被注销。

🌐 In this case the event listener is immediately unregistered after its first trigger.

🌐 Channels

事件系统旨在成为一个简单的双向通信,在你的应用中全局可用。 在底层,它直接执行 JavaScript 代码,因此可能不适合传输大量数据。

🌐 The event system is designed to be a simple two way communication that is globally available in your application. Under the hood it directly evaluates JavaScript code so it might not be suitable to sending a large amount of data.

通道被设计为快速并传递有序的数据。它们在内部用于流操作,例如下载进度、子进程输出和 WebSocket 消息。

🌐 Channels are designed to be fast and deliver ordered data. They are used internally for streaming operations such as download progress, child process output and WebSocket messages.

让我们重新编写我们的下载命令示例,使用通道而不是事件系统:

🌐 Let’s rewrite our download command example to use channels instead of the event system:

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

调用下载命令时,你必须创建通道并将其作为参数提供:

🌐 When calling the download command you must create the channel and provide it as an argument:

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 函数:

🌐 To directly execute any JavaScript code on the webview context you can use the WebviewWindow#eval function:

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 crate。

🌐 If the script to be evaluated is not so simple and must use input from Rust objects we recommend using the serialize-to-javascript crate.


Tauri 中文网 - 粤ICP备13048890号
Nodejs.cn 旗下网站