Saltar al contenido

Llamar al Frontend desde Rust

Este documento incluye guías sobre cómo comunicarse con el frontend de tu aplicación desde tu código en Rust. Para ver cómo comunicarse con tu código de Rust desde tu frontend, consulta Llamar a Rust desde el Frontend.

El lado de Rust de tu aplicación Tauri puede llamar al frontend aprovechando el sistema de eventos de Tauri, utilizando canales o evaluando código JavaScript directamente.

Tauri incluye un sistema de eventos simple que puedes usar para tener comunicación bidireccional entre Rust y tu frontend.

El sistema de eventos fue diseñado para situaciones en las que se necesita transmitir pequeñas cantidades de datos o cuando necesitas implementar un patrón de múltiples consumidores y múltiples productores (por ejemplo, un sistema de notificaciones push).

El sistema de eventos no está diseñado para situaciones de baja latencia o alto rendimiento. Consulta la sección de canales para la implementación optimizada para transmisión de datos.

Las principales diferencias entre un comando de Tauri y un evento de Tauri son que los eventos no tienen soporte para tipos fuertes, las cargas útiles (payloads) de los eventos siempre son cadenas JSON, lo que no los hace adecuados para mensajes más grandes y no hay soporte para el sistema de capacidades para controlar en detalle los datos de eventos y canales.

Los tipos AppHandle y WebviewWindow implementan los traits del sistema de eventos Listener y Emitter.

Los eventos son globales (entregados a todos los oyentes) o específicos de un webview (solo entregados al webview que coincide con una etiqueta dada).

Para activar un evento global puedes usar la función 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", progress).unwrap();
}
app.emit("download-finished", &url).unwrap();
}

Para activar un evento para un oyente registrado por un webview específico puedes usar la función 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();
}

También es posible activar un evento para una lista de webviews llamando a Emitter#emit_filter. En el siguiente ejemplo emitimos un evento open-file para los webviews main y file-viewer:

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

El payload del evento puede ser cualquier tipo serializable que también implemente Clone. Mejoremos el ejemplo del evento de descarga utilizando un objeto para emitir más información en cada evento:

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 proporciona API para escuchar eventos tanto en el webview como en las interfaces de Rust.

El paquete NPM @tauri-apps/api ofrece API para escuchar eventos globales y específicos de webview.

  • Escuchar eventos globales

    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}`
    );
    });
  • Escuchar eventos específicos de webview

    import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
    const appWebview = getCurrentWebviewWindow();
    appWebview.listen<string>('logged-in', (event) => {
    localStorage.setItem('session-token', event.payload);
    });

La función listen mantiene el oyente de eventos registrado durante todo el ciclo de vida de la aplicación. Para dejar de escuchar un evento puedes usar la función unlisten devuelta por la función listen:

import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('download-started', (event) => {});
unlisten();
No llames a unlisten() antes de que el oyente se resuelva
Sección titulada «No llames a unlisten() antes de que el oyente se resuelva»

La función listen devuelve una promesa que se resuelve con el handle unlisten. Si llamas a unlisten de forma síncrona antes de que la promesa se resuelva, el manejador se eliminará inmediatamente y no recibirás ningún evento:

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

En frameworks como React, Vue y Svelte, el hook de setup o montado se ejecuta antes de que el componente esté completamente renderizado. Si escuchas eventos durante la configuración, asegúrate de que el manejador de eventos no dependa de elementos DOM que aún no se hayan renderizado, o pospone el registro del oyente para un efecto/hook que se ejecute después del montaje.

// 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} />;
}

Los oyentes de eventos se llaman en el orden en que fueron registrados, pero si un oyente es asíncrono y el emisor de eventos envía múltiples eventos en rápida sucesión, los oyentes pueden procesar los eventos fuera de orden. Para una entrega de datos ordenada y de alto rendimiento, considera utilizar Canales en lugar del sistema de eventos.

Ejemplos de limpieza específicos de frameworks

Sección titulada «Ejemplos de limpieza específicos de frameworks»

Al utilizar un framework frontend, debes limpiar los oyentes de eventos cuando un componente se desmonte para evitar fugas de memoria y manejadores duplicados.

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

Además, Tauri proporciona una función de utilidad para escuchar un evento exactamente una vez:

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

Los eventos globales y específicos de webview también se entregan a los oyentes registrados en Rust.

  • Escuchar eventos globales

    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");
    }
  • Escuchar eventos específicos de 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");
    }

La función listen mantiene el oyente de eventos registrado durante todo el ciclo de vida de la aplicación. Para dejar de escuchar un evento puedes usar la función 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);
}
});

Además, Tauri proporciona una función de utilidad para escuchar un evento exactamente una vez:

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

En este caso, el oyente de eventos se desregistra inmediatamente después de su primera activación.

El sistema de eventos está diseñado para ser una comunicación simple de dos vías que esté disponible globalmente en tu aplicación. Internamente evalúa directamente código JavaScript, por lo que podría no ser adecuado para enviar una gran cantidad de datos.

Los canales están diseñados para ser rápidos y entregar datos ordenados. Se utilizan internamente para operaciones de transmisión como el progreso de descarga, la salida de procesos hijo y mensajes de WebSocket.

Reescribamos nuestro ejemplo del comando de descarga para usar canales en lugar del sistema de eventos:

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

Al llamar al comando de descarga, debes crear el canal y proporcionarlo como argumento:

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

Para ejecutar directamente cualquier código JavaScript en el contexto del webview puedes usar la función WebviewWindow#eval:

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

Si el script que se va a evaluar no es tan simple y debe usar entradas de objetos de Rust, recomendamos utilizar la crate serialize-to-javascript.


© 2026 Colaboradores de Tauri. CC-BY / MIT