Appeler Rust depuis le Frontend
Ce document comprend des guides sur la façon de communiquer avec votre code Rust depuis le frontend de votre application. Pour voir comment communiquer avec votre frontend depuis votre code Rust, consultez Appeler le frontend depuis Rust.
Tauri fournit une primitive de commande pour atteindre les fonctions Rust avec une sécurité de type, ainsi qu’un système d’événements qui est plus dynamique.
Tauri fournit un système de commande simple mais puissant pour appeler des fonctions Rust depuis votre application web.
Les commandes peuvent accepter des arguments et retourner des valeurs. Elles peuvent également retourner des erreurs et être async.
Les commandes peuvent être définies dans votre fichier src-tauri/src/lib.rs.
Pour créer une commande, ajoutez simplement une fonction et annotez-la avec #[tauri::command] :
#[tauri::command]fn my_custom_command() { println!("I was invoked from JavaScript!");}Vous devrez fournir une liste de vos commandes à la fonction constructeur comme ceci :
#[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");}Maintenant, vous pouvez invoquer la commande depuis votre code JavaScript :
// Lors de l'utilisation du paquet npm API Tauri :import { invoke } from '@tauri-apps/api/core';
// Lors de l'utilisation du script global Tauri (si vous n'utilisez pas le paquet npm)// Assurez-vous de définir `app.withGlobalTauri` dans `tauri.conf.json` sur trueconst invoke = window.__TAURI__.core.invoke;
// Invoquer la commandeinvoke('my_custom_command');Définition de commandes dans un module séparé
Section intitulée « Définition de commandes dans un module séparé »Si votre application définit beaucoup de composants ou s’ils peuvent être regroupés,
vous pouvez définir des commandes dans un module séparé au lieu de gonfler le fichier lib.rs.
À titre d’exemple, définissons une commande dans le fichier src-tauri/src/commands.rs :
#[tauri::command]pub fn my_custom_command() { println!("I was invoked from JavaScript!");}Dans le fichier lib.rs, définissez le module et fournissez la liste de vos commandes en conséquence ;
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");}Notez le préfixe commands:: dans la liste des commandes, qui indique le chemin complet vers la fonction de commande.
Le nom de la commande dans cet exemple est my_custom_command, vous pouvez donc toujours l’appeler en exécutant invoke("my_custom_command")
dans votre frontend, le préfixe commands:: est ignoré.
Lorsque vous utilisez un frontend Rust pour appeler invoke() sans arguments, vous devrez adapter votre code frontend comme ci-dessous.
La raison est que Rust ne prend pas en charge les arguments optionnels.
#[wasm_bindgen]extern "C" { // invoke sans arguments #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)] async fn invoke_without_args(cmd: &str) -> JsValue;
// invoke avec arguments (par défaut) #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] async fn invoke(cmd: &str, args: JsValue) -> JsValue;
// Ils doivent avoir des noms différents !}Vos gestionnaires de commandes peuvent prendre des arguments :
#[tauri::command]fn my_custom_command(invoke_message: String) { println!("I was invoked from JavaScript, with this message: {}", invoke_message);}Les arguments doivent être passés sous forme d’objet JSON avec des clés camelCase :
invoke('my_custom_command', { invokeMessage: 'Hello!' });Les arguments peuvent être de n’importe quel type, tant qu’ils implémentent serde::Deserialize.
Les gestionnaires de commandes peuvent également retourner des données :
#[tauri::command]fn my_custom_command() -> String { "Hello from Rust!".into()}La fonction invoke retourne une promesse qui se résout avec la valeur retournée :
invoke('my_custom_command').then((message) => console.log(message));Les données retournées peuvent être de n’importe quel type, tant qu’elles implémentent serde::Serialize.
Les valeurs de retour qui implémentent serde::Serialize sont sérialisées en JSON lorsque la réponse est envoyée au frontend.
Cela peut ralentir votre application si vous essayez de retourner de grandes données telles qu’un fichier ou une réponse HTTP de téléchargement.
Pour retourner des tampons de tableau de manière optimisée, utilisez 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)}Si votre gestionnaire peut échouer et doit pouvoir retourner une erreur, faites en sorte que la fonction retourne un Result :
#[tauri::command]fn login(user: String, password: String) -> Result<String, String> { if user == "tauri" && password == "tauri" { // résoudre Ok("logged_in".to_string()) } else { // rejeter Err("invalid credentials".to_string()) }}Si la commande retourne une erreur, la promesse sera rejetée, sinon, elle se résout :
invoke('login', { user: 'tauri', password: '0j4rijw8=' }) .then((message) => console.log(message)) .catch((error) => console.error(error));Comme mentionné ci-dessus, tout ce qui est retourné par les commandes doit implémenter serde::Serialize, y compris les erreurs.
Cela peut être problématique si vous travaillez avec des types d’erreur de la bibliothèque standard de Rust ou de crates externes, car la plupart des types d’erreur ne l’implémentent pas.
Dans des scénarios simples, vous pouvez utiliser map_err pour convertir ces erreurs en String :
#[tauri::command]fn my_custom_command() -> Result<(), String> { std::fs::File::open("path/to/file").map_err(|err| err.to_string())?; // Retourner `null` en cas de succès Ok(())}Comme ce n’est pas très idiomatique, vous voudrez peut-être créer votre propre type d’erreur qui implémente serde::Serialize.
Dans l’exemple suivant, nous utilisons le crate thiserror pour aider à créer le type d’erreur.
Il vous permet de transformer des énumérations en types d’erreur en dérivant le trait thiserror::Error.
Vous pouvez consulter sa documentation pour plus de détails.
// créer le type d'erreur qui représente toutes les erreurs possibles dans notre programme#[derive(Debug, thiserror::Error)]enum Error { #[error(transparent)] Io(#[from] std::io::Error)}
// nous devons implémenter manuellement serde::Serializeimpl 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> { // Cela retournera une erreur std::fs::File::open("path/that/does/not/exist")?; // Retourner `null` en cas de succès Ok(())}Un type d’erreur personnalisé a l’avantage de rendre toutes les erreurs possibles explicites afin que les lecteurs puissent identifier rapidement quelles erreurs peuvent se produire.
Cela permet à d’autres personnes (et à vous-même) de gagner énormément de temps lors de la révision et du refactoring du code plus tard.
Cela vous donne également un contrôle total sur la façon dont votre type d’erreur est sérialisé.
Dans l’exemple ci-dessus, nous avons simplement retourné le message d’erreur sous forme de chaîne, mais vous pourriez attribuer à chaque erreur un code
afin de pouvoir le mapper plus facilement à une énumération d’erreur TypeScript d’apparence similaire par exemple :
#[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)}Dans votre frontend, vous obtenez maintenant un objet d’erreur { kind: 'io' | 'utf8', message: string } :
type ErrorKind = { kind: 'io' | 'utf8'; message: string;};
invoke('read').catch((e: ErrorKind) => {});Les commandes asynchrones sont préférées dans Tauri pour effectuer des travaux lourds d’une manière qui n’entraîne pas de gel ou de ralentissement de l’interface utilisateur.
Si votre commande doit s’exécuter de manière asynchrone, déclarez-la simplement comme async.
Lorsque vous travaillez avec des types empruntés, vous devez apporter des modifications supplémentaires. Voici vos deux options principales :
Option 1 : Convertir le type, tel que &str en un type similaire qui n’est pas emprunté, tel que String.
Cela peut ne pas fonctionner pour tous les types, par exemple State<'_, Data>.
Exemple :
// Déclarer la fonction asynchrone en utilisant String au lieu de &str, car &str est emprunté et donc non pris en charge#[tauri::command]async fn my_custom_command(value: String) -> String { // Appeler une autre fonction asynchrone et attendre qu'elle se termine some_async_function().await; value}Option 2 : Envelopper le type de retour dans un Result. C’est un peu plus difficile à implémenter, mais cela fonctionne pour tous les types.
Utilisez le type de retour Result<a, b>, en remplaçant a par le type que vous souhaitez retourner, ou () si vous souhaitez retourner null, et en remplaçant b par un type d’erreur à retourner si quelque chose ne va pas, ou () si vous ne souhaitez pas qu’une erreur facultative soit retournée. Par exemple :
Result<String, ()>pour retourner une chaîne, et aucune erreur.Result<(), ()>pour retournernull.Result<bool, Error>pour retourner un booléen ou une erreur comme indiqué dans la section Gestion des erreurs ci-dessus.
Exemple :
// Retourner un Result<String, ()> pour contourner le problème d'emprunt#[tauri::command]async fn my_custom_command(value: &str) -> Result<String, ()> { // Appeler une autre fonction asynchrone et attendre qu'elle se termine some_async_function().await; // Notez que la valeur de retour doit être enveloppée dans `Ok()` maintenant. Ok(format!(value))}Étant donné que l’invocation de la commande depuis JavaScript retourne déjà une promesse, cela fonctionne comme n’importe quelle autre commande :
invoke('my_custom_command', { value: 'Hello, Async!' }).then(() => console.log('Completed!'));Le canal Tauri est le mécanisme recommandé pour diffuser des données telles que des réponses HTTP diffusées vers le frontend. L’exemple suivant lit un fichier et notifie le frontend de la progression par morceaux de 4096 octets :
use tokio::io::AsyncReadExt;
#[tauri::command]async fn load_image(path: std::path::PathBuf, reader: tauri::ipc::Channel<&[u8]>) { // pour simplifier, cet exemple n'inclut pas la gestion des erreurs 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 { // Une longueur de zéro signifie la fin du fichier. break; } reader.send(&chunk).unwrap(); }}Voir la documentation sur les canaux pour plus d’informations.
Accès à WebviewWindow dans les commandes
Section intitulée « Accès à WebviewWindow dans les commandes »Les commandes peuvent accéder à l’instance WebviewWindow qui a invoqué le message :
#[tauri::command]async fn my_custom_command(webview_window: tauri::WebviewWindow) { println!("WebviewWindow: {}", webview_window.label());}Accès à un AppHandle dans les commandes
Section intitulée « Accès à un AppHandle dans les commandes »Les commandes peuvent accéder à une instance AppHandle :
#[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 peut gérer l’état à l’aide de la fonction manage sur tauri::Builder.
L’état peut être accédé sur une commande en utilisant tauri::State :
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");}Les commandes Tauri peuvent également accéder à l’objet complet tauri::ipc::Request qui comprend la charge utile du corps brut et les en-têtes de la requête.
#[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")); };
// télécharger...
Ok(())}Dans le frontend, vous pouvez appeler invoke() en envoyant un corps de requête brut en fournissant un ArrayBuffer ou Uint8Array sur l’argument de charge utile, et inclure les en-têtes de requête dans le troisième argument :
const data = new Uint8Array([1, 2, 3]);await __TAURI__.core.invoke('upload', data, { headers: { Authorization: 'apikey', },});La macro tauri::generate_handler! prend un tableau de commandes. Pour enregistrer
plusieurs commandes, vous ne pouvez pas appeler invoke_handler plusieurs fois. Seul le dernier
appel sera utilisé. Vous devez passer chaque commande à un seul appel de
tauri::generate_handler!.
#[tauri::command]fn cmd_a() -> String { "Commande a"}#[tauri::command]fn cmd_b() -> String { "Commande 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");}Tout ou partie des fonctionnalités ci-dessus peuvent être combinées :
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!("Appelé depuis {}", window.label()); let result: Option<String> = some_other_function().await; if let Some(message) = result { Ok(CustomResponse { message, other_val: 42 + number, }) } else { Err("Aucun résultat".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 depuis JavaScriptinvoke('my_custom_command', { number: 42,}) .then((res) => console.log(`Message: ${res.message}, Autre val: ${res.other_val}`) ) .catch((e) => console.error(e));Le système d’événements est un mécanisme de communication plus simple entre votre frontend et Rust. Contrairement aux commandes, les événements ne sont pas typés, sont toujours asynchrones, ne peuvent pas retourner de valeurs et ne supportent que les charges utiles JSON.
Pour déclencher un événement global, vous pouvez utiliser les fonctions event.emit ou 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 });Pour déclencher un événement à un auditeur enregistré par une webview spécifique, vous pouvez utiliser les fonctions event.emitTo ou WebviewWindow#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',});Le paquet NPM @tauri-apps/api offre des API pour écouter les événements globaux et spécifiques à la webview.
-
Écoute des événements globaux
import { listen } from '@tauri-apps/api/event';type DownloadStarted = {url: string;downloadId: number;contentLength: number;};listen<DownloadStarted>('download-started', (event) => {console.log(`téléchargement de ${event.payload.contentLength} octets depuis ${event.payload.url}`);}); -
Écoute des événements spécifiques à la webview
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';const appWebview = getCurrentWebviewWindow();appWebview.listen<string>('logged-in', (event) => {localStorage.setItem('session-token', event.payload);});
La fonction listen maintient l’écouteur d’événements enregistré pendant toute la durée de vie de l’application.
Pour arrêter d’écouter un événement, vous pouvez utiliser la fonction unlisten qui est retournée par la fonction listen :
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});unlisten();De plus, Tauri fournit une fonction utilitaire pour écouter un événement exactement une fois :
import { once } from '@tauri-apps/api/event';import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
once('ready', (event) => {});
const appWebview = getCurrentWebviewWindow();appWebview.once('ready', () => {});Les événements globaux et spécifiques à la webview sont également livrés aux auditeurs enregistrés dans Rust.
-
Écoute des événements globaux
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!("téléchargement {}", payload.url);}});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");} -
Écoute des événements spécifiques à la 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;// sauvegarder le jeton..});Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");}
La fonction listen maintient l’écouteur d’événements enregistré pendant toute la durée de vie de l’application.
Pour arrêter d’écouter un événement, vous pouvez utiliser la fonction unlisten :
// unlisten en dehors de la portée du gestionnaire d'événements :let event_id = app.listen("download-started", |event| {});app.unlisten(event_id);
// unlisten lorsque certains critères d'événement sont remplislet handle = app.handle().clone();app.listen("status-changed", |event| { if event.data == "ready" { handle.unlisten(event.id); }});De plus, Tauri fournit une fonction utilitaire pour écouter un événement exactement une fois :
app.once("ready", |event| { println!("app est prêt");});Dans ce cas, l’écouteur d’événements est immédiatement désinscrit après son premier déclenchement.
Pour apprendre comment écouter des événements et émettre des événements depuis votre code Rust, consultez la documentation du système d’événements Rust.
© 2025 Tauri Contributors. CC-BY / MIT