Desarrollo de Plugins
Los plugins pueden vincularse al ciclo de vida de Tauri, exponer código Rust que se apoya en las APIs de la web view, manejar comandos con código Rust, Kotlin o Swift, y mucho más.
Tauri ofrece un sistema de ventanas con funcionalidad de web view, una forma de enviar mensajes entre el proceso de Rust y la web view, y un sistema de eventos junto con varias herramientas para mejorar la experiencia de desarrollo. Por diseño, el núcleo de Tauri no contiene funciones que no sean necesarias para todos. En su lugar, ofrece un mecanismo para agregar funcionalidades externas a una aplicación Tauri llamado plugins.
Un plugin de Tauri está compuesto por un crate de Cargo y un paquete NPM opcional que proporciona enlaces de API (bindings) para sus comandos y eventos. Además, un proyecto de plugin puede incluir un proyecto de biblioteca de Android y un paquete Swift para iOS. Puedes obtener más información sobre el desarrollo de plugins para Android e iOS en la Guía de desarrollo de plugins móviles.
Convención de Nombres
Sección titulada «Convención de Nombres»Los plugins de Tauri tienen un prefijo seguido por el nombre del plugin. El nombre del plugin se especifica en la configuración del plugin bajo tauri.conf.json > plugins.
Por defecto, Tauri añade el prefijo tauri-plugin- a tu crate de plugin. Esto ayuda a que tu plugin sea descubierto por la comunidad de Tauri y a que se use con la CLI de Tauri. Al inicializar un nuevo proyecto de plugin, debes proporcionar su nombre. El nombre del crate generado será tauri-plugin-{plugin-name} y el nombre del paquete NPM JavaScript será tauri-plugin-{plugin-name}-api (aunque recomendamos usar un scope de NPM si es posible). La convención de nombres de Tauri para paquetes NPM es @scope-name/plugin-{plugin-name}.
Inicializar Proyecto de Plugin
Sección titulada «Inicializar Proyecto de Plugin»Para inicializar un nuevo proyecto de plugin, ejecuta plugin new. Si no necesitas el paquete NPM, usa el flag de la CLI --no-api. Si quieres inicializar el plugin con soporte para Android y/o iOS, usa los flags --android y/o --ios.
Después de instalar, puedes ejecutar lo siguiente para crear un proyecto de plugin:
npx @tauri-apps/cli plugin new [name]Esto inicializará el plugin en el directorio tauri-plugin-[name] y, dependiendo de los flags de la CLI utilizados, el proyecto resultante se verá así:
. tauri-plugin-[name]/├── src/ - Rust code│ ├── commands.rs - Defines the commands the webview can use| ├── desktop.rs - Desktop implementation| ├── error.rs - Default error type to use in returned results│ ├── lib.rs - Re-exports appropriate implementation, setup state...│ ├── mobile.rs - Mobile implementation│ └── models.rs - Shared structs├── permissions/ - This will host (generated) permission files for commands├── android - Android library├── ios - Swift package├── guest-js - Source code of the JavaScript API bindings├── dist-js - Transpiled assets from guest-js├── Cargo.toml - Cargo crate metadata└── package.json - NPM package metadataSi tienes un plugin existente y deseas agregarle capacidades para Android o iOS, puedes usar plugin android add y plugin ios add para inicializar los proyectos de biblioteca móvil y guiarte a través de los cambios necesarios.
Desarrollo de Plugins Móviles
Sección titulada «Desarrollo de Plugins Móviles»Los plugins pueden ejecutar código móvil nativo escrito en Kotlin (o Java) y Swift. La plantilla de plugin por defecto incluye un proyecto de biblioteca Android que utiliza Kotlin y un paquete Swift. Incluye un comando móvil de ejemplo que muestra cómo activar su ejecución desde código Rust.
Lee más sobre el desarrollo de plugins para móviles en la Guía de desarrollo de plugins móviles.
Configuración del Plugin
Sección titulada «Configuración del Plugin»En la aplicación Tauri donde se utiliza el plugin, la configuración del plugin se especifica en tauri.conf.json donde plugin-name es el nombre del plugin:
{ "build": { ... }, "tauri": { ... }, "plugins": { "plugin-name": { "timeout": 30 } }}La configuración del plugin se establece en el Builder y se analiza en tiempo de ejecución. Aquí hay un ejemplo de la estructura Config utilizándose para especificar la configuración del plugin:
use serde::Deserialize;use tauri::{ plugin::{Builder, TauriPlugin}, Runtime,};
// Define the plugin config#[derive(Deserialize)]pub struct Config { timeout: usize,}
pub fn init<R: Runtime>() -> TauriPlugin<R, Config> { // Make the plugin config optional // by using `Builder::<R, Option<Config>>` instead Builder::<R, Config>::new("<plugin-name>") .setup(|app, api| { let timeout = api.config().timeout; Ok(()) }) .build()}Eventos del Ciclo de Vida
Sección titulada «Eventos del Ciclo de Vida»Los plugins pueden vincularse a varios eventos del ciclo de vida:
- setup: El plugin se está inicializando
- on_navigation: La web view está intentando realizar una navegación
- on_webview_ready: Se está creando una nueva ventana
- on_event: Eventos del bucle de eventos (event loop)
- on_drop: El plugin se está desconstruyendo
Existen eventos del ciclo de vida adicionales para plugins móviles.
- Cuándo: El plugin se está inicializando
- Por qué: Registrar plugins móviles, gestionar estado, ejecutar tareas en segundo plano
use tauri::{Manager, plugin::Builder};use std::{collections::HashMap, sync::Mutex, time::Duration};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>") .setup(|app, api| { app.manage(DummyStore(Default::default()));
let app_ = app.clone(); std::thread::spawn(move || { loop { app_.emit("tick", ()); std::thread::sleep(Duration::from_secs(1)); } });
Ok(()) })on_navigation
Sección titulada «on_navigation»- Cuándo: La web view está intentando realizar una navegación
- Por qué: Validar la navegación o rastrear cambios de URL
Retornar false cancela la navegación.
use tauri::plugin::Builder;
Builder::new("<plugin-name>") .on_navigation(|window, url| { println!("window {} is navigating to {}", window.label(), url); // Cancels the navigation if forbidden url.scheme() != "forbidden" })on_webview_ready
Sección titulada «on_webview_ready»- Cuándo: Se ha creado una nueva ventana
- Por qué: Ejecutar un script de inicialización para cada ventana
use tauri::plugin::Builder;
Builder::new("<plugin-name>") .on_webview_ready(|window| { window.listen("content-loaded", |event| { println!("webview content has been loaded"); }); })on_event
Sección titulada «on_event»- Cuándo: Eventos del bucle de eventos (event loop)
- Por qué: Manejar eventos principales como eventos de ventana, eventos de menú y solicitud de salida de la aplicación
Con este hook de ciclo de vida puedes ser notificado de cualquier evento del bucle de eventos.
use std::{collections::HashMap, fs::write, sync::Mutex};use tauri::{plugin::Builder, Manager, RunEvent};
struct DummyStore(Mutex<HashMap<String, String>>);
Builder::new("<plugin-name>") .setup(|app, _api| { app.manage(DummyStore(Default::default())); Ok(()) }) .on_event(|app, event| { match event { RunEvent::ExitRequested { api, .. } => { // user requested a window to be closed and there's no windows left
// we can prevent the app from exiting: api.prevent_exit(); } RunEvent::Exit => { // app is going to exit, you can cleanup here
let store = app.state::<DummyStore>(); write( app.path().app_local_data_dir().unwrap().join("store.json"), serde_json::to_string(&*store.0.lock().unwrap()).unwrap(), ) .unwrap(); } _ => {} } })on_drop
Sección titulada «on_drop»- Cuándo: El plugin se está desconstruyendo
- Por qué: Ejecutar código cuando el plugin ha sido destruido
Consulta Drop para obtener más información.
use tauri::plugin::Builder;
Builder::new("<plugin-name>") .on_drop(|app| { // plugin has been destroyed... })Exponer APIs de Rust
Sección titulada «Exponer APIs de Rust»Las APIs del plugin definidas en desktop.rs y mobile.rs del proyecto se exportan al usuario como una estructura (struct) con el mismo nombre del plugin (en PascalCase). Cuando se configura el plugin, se crea una instancia de esta estructura y se gestiona como un estado para que los usuarios puedan recuperarla en cualquier momento con una instancia de Manager (como AppHandle, App o Window) a través del trait de extensión definido en el plugin.
Por ejemplo, el plugin global-shortcut define una estructura GlobalShortcut que se puede leer utilizando el método global_shortcut del trait GlobalShortcutExt:
use tauri_plugin_global_shortcut::GlobalShortcutExt;
tauri::Builder::default() .plugin(tauri_plugin_global_shortcut::init()) .setup(|app| { app.global_shortcut().register(...); Ok(()) })Agregar Comandos
Sección titulada «Agregar Comandos»Los comandos se definen en el archivo commands.rs. Son comandos regulares de aplicaciones Tauri. Pueden acceder a las instancias de AppHandle y Window directamente, acceder al estado y tomar entradas de la misma manera que los comandos de la aplicación. Lee la Guía de Comandos para obtener más detalles sobre los comandos de Tauri.
Este comando muestra cómo obtener acceso a la instancia de AppHandle y Window a través de inyección de dependencias, y toma dos parámetros de entrada (on_progress y url):
use tauri::{command, ipc::Channel, AppHandle, Runtime, Window};
#[command]async fn upload<R: Runtime>(app: AppHandle<R>, window: Window<R>, on_progress: Channel, url: String) { // implement command logic here on_progress.send(100).unwrap();}Para exponer el comando a la webview, debes vincularlo a la llamada invoke_handler() en lib.rs:
Builder::new("<plugin-name>") .invoke_handler(tauri::generate_handler![commands::upload])Define una función de binding en webview-src/index.ts para que los usuarios del plugin puedan llamar fácilmente al comando en JavaScript:
import { invoke, Channel } from '@tauri-apps/api/core'
export async function upload(url: string, onProgressHandler: (progress: number) => void): Promise<void> { const onProgress = new Channel<number>() onProgress.onmessage = onProgressHandler await invoke('plugin:<plugin-name>|upload', { url, onProgress })}Asegúrate de compilar el código TypeScript antes de probarlo.
Permisos de Comandos
Sección titulada «Permisos de Comandos»Por defecto, tus comandos no son accesibles desde el frontend. Si intentas ejecutar uno de ellos, recibirás un rechazo por error de denegación. Para exponer realmente los comandos, también necesitas definir permisos que permitan cada comando.
Archivos de Permisos
Sección titulada «Archivos de Permisos»Los permisos se definen como archivos JSON o TOML dentro del directorio permissions. Cada archivo puede definir una lista de permisos, una lista de conjuntos de permisos y el permiso por defecto de tu plugin.
Permisos
Sección titulada “Permisos”Un permiso describe los privilegios de los comandos de tu plugin. Puede permitir o denegar una lista de comandos y asociar scopes específicos de comandos y globales.
"$schema" = "schemas/schema.json"
[[permission]]identifier = "allow-start-server"description = "Enables the start_server command."commands.allow = ["start_server"]
[[permission]]identifier = "deny-start-server"description = "Denies the start_server command."commands.deny = ["start_server"]Los scopes permiten que tu plugin defina restricciones más profundas a comandos individuales. Cada permiso puede definir una lista de objetos de scope que definen algo que se permitirá o denegará, ya sea específico para un comando o de forma global para el plugin.
Definamos una estructura de ejemplo que mantendrá los datos de scope para una lista de binarios que un plugin de shell tiene permitido ejecutar:
#[derive(Debug, schemars::JsonSchema)]pub struct Entry { pub binary: String,}Scope de Comando
Sección titulada «Scope de Comando»El consumidor de tu plugin puede definir un scope para un comando específico en su archivo de capacidades (consulta la documentación).
Puedes leer el scope específico del comando con la estructura tauri::ipc::CommandScope:
use tauri::ipc::CommandScope;use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, command_scope: CommandScope<'_, Entry>) -> Result<()> { let allowed = command_scope.allows(); let denied = command_scope.denies(); todo!()}Scope Global
Sección titulada «Scope Global»Cuando un permiso no define ningún comando para permitir o denegar, se considera un permiso de scope y solo debe definir un scope global para tu plugin:
[[permission]]identifier = "allow-spawn-node"description = "This scope permits spawning the `node` binary."
[[permission.scope.allow]]binary = "node"Puedes leer el scope global con la estructura tauri::ipc::GlobalScope:
use tauri::ipc::GlobalScope;use crate::scope::Entry;
async fn spawn<R: tauri::Runtime>(app: tauri::AppHandle<R>, scope: GlobalScope<'_, Entry>) -> Result<()> { let allowed = scope.allows(); let denied = scope.denies(); todo!()}Esquema
Sección titulada «Esquema»La entrada de scope requiere la dependencia schemars para generar un esquema JSON de modo que los consumidores del plugin conozcan el formato del scope y tengan autocompletado en sus IDEs.
Para definir el esquema, primero agrega la dependencia a tu archivo Cargo.toml:
# we need to add schemars to both dependencies and build-dependencies because the scope.rs module is shared between the app code and build script[dependencies]schemars = "0.8"
[build-dependencies]schemars = "0.8"En tu script de compilación, agrega el siguiente código:
#[path = "src/scope.rs"]mod scope;
const COMMANDS: &[&str] = &[];
fn main() { tauri_plugin::Builder::new(COMMANDS) .global_scope_schema(schemars::schema_for!(scope::Entry)) .build();}Conjuntos de Permisos
Sección titulada «Conjuntos de Permisos»Los conjuntos de permisos son grupos de permisos individuales que ayudan a los usuarios a gestionar tu plugin con un mayor nivel de abstracción. Por ejemplo, si una sola API utiliza múltiples comandos o si hay una conexión lógica entre una colección de comandos, deberías definir un conjunto que los contenga:
"$schema" = "schemas/schema.json"[[set]]identifier = "allow-websocket"description = "Allows connecting and sending messages through a WebSocket"permissions = ["allow-connect", "allow-send"]Permiso por Defecto
Sección titulada «Permiso por Defecto»El permiso por defecto es un conjunto de permisos especial con el identificador default. Se recomienda habilitar los comandos requeridos por defecto.
Por ejemplo, el plugin http no sirve de nada sin el comando request permitido:
"$schema" = "schemas/schema.json"[default]description = "Allows making HTTP requests"permissions = ["allow-request"]Permisos Autogenerados
Sección titulada «Permisos Autogenerados»La forma más fácil de definir permisos para cada uno de tus comandos es utilizar la opción de autogeneración definida en el script de compilación de tu plugin, ubicado en el archivo build.rs.
Dentro de la constante COMMANDS, define la lista de comandos en snake_case (debe coincidir con el nombre de la función del comando) y Tauri generará automáticamente los permisos allow-$commandname y deny-$commandname.
El siguiente ejemplo genera los permisos allow-upload y deny-upload:
const COMMANDS: &[&str] = &["upload"];
fn main() { tauri_plugin::Builder::new(COMMANDS).build();}Consulta la documentación de Visión General de Permisos para más información.
Gestión de Estado
Sección titulada «Gestión de Estado»Un plugin puede gestionar el estado de la misma manera que lo hace una aplicación Tauri. Lee la Guía de gestión de estado para obtener más información.
© 2026 Colaboradores de Tauri. CC-BY / MIT