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 将这些错误转换为 Strings

#[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>。使用借用的类型时,必须进行其他更改:

  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<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', () => {});

 

posted @   marilol  阅读(313)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示