tauri-event-system

安装量: 120
排名: #7168

安装

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill tauri-event-system

Tauri Advanced Event System Event Fundamentals Backend → Frontend Events

Basic event emission:

use tauri::Window;

[tauri::command]

async fn start_download( url: String, window: Window, ) -> Result<(), String> { window.emit("download-started", url) .map_err(|e| e.to_string())?;

// Perform download...

window.emit("download-complete", "Success")
    .map_err(|e| e.to_string())

}

Frontend listener:

import { listen, UnlistenFn } from '@tauri-apps/api/event';

const unlisten = await listen('download-started', (event) => { console.log('Download started:', event.payload); });

// Clean up when done unlisten();

Structured Event Payloads Typed Events with Serde

Backend:

use serde::Serialize;

[derive(Serialize, Clone)]

struct ProgressEvent { current: usize, total: usize, percentage: f64, message: String, speed_mbps: Option, }

[tauri::command]

async fn download_file( url: String, window: Window, ) -> Result<(), String> { let total_size = get_file_size(&url).await?;

for chunk in 0..total_size {
    // Download chunk...

    let progress = ProgressEvent {
        current: chunk,
        total: total_size,
        percentage: (chunk as f64 / total_size as f64) * 100.0,
        message: format!("Downloading... {}/{}", chunk, total_size),
        speed_mbps: Some(calculate_speed()),
    };

    window.emit("download-progress", progress)
        .map_err(|e| e.to_string())?;
}

Ok(())

}

Frontend:

interface ProgressEvent { current: number; total: number; percentage: number; message: string; speed_mbps?: number; }

const unlisten = await listen('download-progress', (event) => { const { current, total, percentage, message, speed_mbps } = event.payload;

updateProgressBar(percentage);
updateStatus(message);

if (speed_mbps) {
    updateSpeed(speed_mbps);
}

});

Complex Event Payloads

[derive(Serialize, Clone)]

[serde(tag = "type", content = "data")]

enum AppEvent { UserLoggedIn { user_id: String, username: String }, UserLoggedOut { user_id: String }, DataSynced { items_count: usize, timestamp: String }, ErrorOccurred { code: String, message: String, recoverable: bool }, }

[tauri::command]

async fn perform_login( username: String, password: String, window: Window, ) -> Result { let user = authenticate(&username, &password).await?;

// Emit structured event
window.emit("app-event", AppEvent::UserLoggedIn {
    user_id: user.id.clone(),
    username: user.username.clone(),
}).map_err(|e| e.to_string())?;

Ok(user.id)

}

Frontend:

type AppEvent = | { type: 'UserLoggedIn'; data: { user_id: string; username: string } } | { type: 'UserLoggedOut'; data: { user_id: string } } | { type: 'DataSynced'; data: { items_count: number; timestamp: string } } | { type: 'ErrorOccurred'; data: { code: string; message: string; recoverable: boolean } };

listen('app-event', (event) => { const appEvent = event.payload;

switch (appEvent.type) {
    case 'UserLoggedIn':
        handleLogin(appEvent.data.user_id, appEvent.data.username);
        break;
    case 'UserLoggedOut':
        handleLogout(appEvent.data.user_id);
        break;
    case 'DataSynced':
        showSyncSuccess(appEvent.data.items_count);
        break;
    case 'ErrorOccurred':
        handleError(appEvent.data);
        break;
}

});

Streaming Data Patterns Real-Time Data Stream

[tauri::command]

async fn stream_sensor_data( sensor_id: String, window: Window, ) -> Result<(), String> { let mut interval = tokio::time::interval(Duration::from_millis(100));

for _ in 0..100 {
    interval.tick().await;

    let reading = read_sensor(&sensor_id).await?;

    window.emit("sensor-reading", reading)
        .map_err(|e| e.to_string())?;
}

window.emit("sensor-stream-ended", sensor_id)
    .map_err(|e| e.to_string())

}

Frontend with React:

import { useEffect, useState } from 'react'; import { listen } from '@tauri-apps/api/event';

interface SensorReading { value: number; timestamp: number; unit: string; }

function SensorMonitor() { const [readings, setReadings] = useState([]);

useEffect(() => {
    let unlisten: UnlistenFn | undefined;

    listen<SensorReading>('sensor-reading', (event) => {
        setReadings(prev => [...prev.slice(-99), event.payload]);
    }).then(fn => unlisten = fn);

    return () => unlisten?.();
}, []);

return (
    <div>
        {readings.map((r, i) => (
            <div key={i}>{r.value} {r.unit}</div>
        ))}
    </div>
);

}

Buffered Streaming

[tauri::command]

async fn stream_logs( log_file: String, window: Window, ) -> Result<(), String> { use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::fs::File;

let file = File::open(log_file).await
    .map_err(|e| e.to_string())?;

let reader = BufReader::new(file);
let mut lines = reader.lines();

let mut buffer = Vec::new();

while let Some(line) = lines.next_line().await
    .map_err(|e| e.to_string())? {

    buffer.push(line);

    // Send in batches of 10 lines
    if buffer.len() >= 10 {
        window.emit("log-batch", buffer.clone())
            .map_err(|e| e.to_string())?;
        buffer.clear();
    }
}

// Send remaining lines
if !buffer.is_empty() {
    window.emit("log-batch", buffer)
        .map_err(|e| e.to_string())?;
}

Ok(())

}

Multi-Window Communication Broadcasting to All Windows use tauri::{AppHandle, Manager};

[tauri::command]

async fn broadcast_message( message: String, app: AppHandle, ) -> Result<(), String> { // Emit to ALL windows app.emit_all("broadcast", message) .map_err(|e| e.to_string()) }

Targeted Window Messaging

[tauri::command]

async fn send_to_window( target_window: String, message: String, app: AppHandle, ) -> Result<(), String> { // Get specific window if let Some(window) = app.get_window(&target_window) { window.emit("private-message", message) .map_err(|e| e.to_string())?; Ok(()) } else { Err(format!("Window '{}' not found", target_window)) } }

Window-to-Window via Backend

Window A (sender):

import { invoke } from '@tauri-apps/api/core';

async function sendToSettings(data: any) { await invoke('relay_to_settings', { data }); }

Backend relay:

[tauri::command]

async fn relay_to_settings( data: serde_json::Value, app: AppHandle, ) -> Result<(), String> { if let Some(settings_window) = app.get_window("settings") { settings_window.emit("data-update", data) .map_err(|e| e.to_string())?; } Ok(()) }

Window B (receiver - settings):

import { listen } from '@tauri-apps/api/event';

useEffect(() => { let unlisten: UnlistenFn | undefined;

listen('data-update', (event) => {
    console.log('Received from main window:', event.payload);
    updateSettings(event.payload);
}).then(fn => unlisten = fn);

return () => unlisten?.();

}, []);

Frontend → Backend Events Custom Frontend Events import { emit } from '@tauri-apps/api/event';

// Frontend emits event await emit('user-action', { action: 'button-click', button_id: 'save-button', timestamp: Date.now() });

Backend listener:

use tauri::{Manager, Listener};

fn main() { tauri::Builder::default() .setup(|app| { let app_handle = app.handle();

        // Listen for frontend events
        app_handle.listen_global("user-action", move |event| {
            if let Some(payload) = event.payload() {
                println!("User action: {}", payload);
                // Process event...
            }
        });

        Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

}

Advanced Listener Management React Hook for Events import { useEffect, useState } from 'react'; import { listen, UnlistenFn } from '@tauri-apps/api/event';

function useEvent(eventName: string): T | null { const [payload, setPayload] = useState(null);

useEffect(() => {
    let unlisten: UnlistenFn | undefined;

    listen<T>(eventName, (event) => {
        setPayload(event.payload);
    }).then(fn => unlisten = fn);

    return () => unlisten?.();
}, [eventName]);

return payload;

}

// Usage function ProgressDisplay() { const progress = useEvent('download-progress');

if (!progress) return null;

return (
    <div>
        Progress: {progress.percentage.toFixed(2)}%
    </div>
);

}

Event Queue Pattern import { listen } from '@tauri-apps/api/event';

class EventQueue { private queue: T[] = []; private unlisten?: UnlistenFn;

async start(eventName: string) {
    this.unlisten = await listen<T>(eventName, (event) => {
        this.queue.push(event.payload);
    });
}

dequeue(): T | undefined {
    return this.queue.shift();
}

clear() {
    this.queue = [];
}

stop() {
    this.unlisten?.();
}

get length() {
    return this.queue.length;
}

}

// Usage const progressQueue = new EventQueue(); await progressQueue.start('download-progress');

// Process queue periodically setInterval(() => { while (progressQueue.length > 0) { const event = progressQueue.dequeue(); processProgress(event); } }, 100);

One-Time Events import { once } from '@tauri-apps/api/event';

// Listen for event only once await once('initialization-complete', (event) => { console.log('App initialized:', event.payload); startApp(); });

Error Handling in Events Safe Event Emission async fn emit_safe(window: &Window, event: &str, payload: impl Serialize) -> Result<(), String> { window.emit(event, payload) .map_err(|e| { eprintln!("Failed to emit event '{}': {}", event, e); e.to_string() }) }

[tauri::command]

async fn process_with_events( window: Window, ) -> Result<(), String> { emit_safe(&window, "processing-started", "Starting...") .await?;

// Process...

emit_safe(&window, "processing-complete", "Done!")
    .await?;

Ok(())

}

Performance Considerations Throttling Events use std::time::{Duration, Instant};

[tauri::command]

async fn high_frequency_updates( window: Window, ) -> Result<(), String> { let mut last_emit = Instant::now(); let throttle_duration = Duration::from_millis(100);

for i in 0..10000 {
    let value = compute_value(i);

    // Only emit every 100ms
    if last_emit.elapsed() >= throttle_duration {
        window.emit("update", value)
            .map_err(|e| e.to_string())?;
        last_emit = Instant::now();
    }
}

Ok(())

}

Batching Events

[tauri::command]

async fn batch_updates( window: Window, ) -> Result<(), String> { let mut batch = Vec::new();

for item in process_items() {
    batch.push(item);

    // Emit in batches of 50
    if batch.len() >= 50 {
        window.emit("batch-update", batch.clone())
            .map_err(|e| e.to_string())?;
        batch.clear();
    }
}

// Emit remaining items
if !batch.is_empty() {
    window.emit("batch-update", batch)
        .map_err(|e| e.to_string())?;
}

Ok(())

}

Best Practices Always clean up listeners - Use unlisten() to prevent memory leaks Type event payloads - Define interfaces for type safety Use structured events - Tagged unions for multiple event types Throttle high-frequency events - Prevent overwhelming frontend Batch when possible - Reduce serialization overhead Handle errors gracefully - Log failed emissions, don't crash Use once() for one-time events - Initialization, completion signals Namespace event names - Use prefixes like "download:", "user:", "system:" Common Pitfalls

❌ Forgetting to unlisten:

// WRONG - memory leak function Component() { listen('my-event', handler); // Never cleaned up! }

// CORRECT function Component() { useEffect(() => { let unlisten: UnlistenFn | undefined; listen('my-event', handler).then(fn => unlisten = fn); return () => unlisten?.(); }, []); }

❌ Not handling serialization errors:

// WRONG - struct can't serialize

[derive(Clone)] // Missing Serialize!

struct Event { }

window.emit("event", Event {}); // Runtime error!

// CORRECT

[derive(Serialize, Clone)]

struct Event { }

❌ Emitting too frequently:

// WRONG - 10000 events in quick succession for i in 0..10000 { window.emit("update", i); // Overwhelming! }

// CORRECT - throttle or batch

Summary Events are async - Backend → Frontend communication Always type payloads - Use serde::Serialize + TypeScript interfaces Clean up listeners - Call unlisten() in cleanup Throttle/batch - High-frequency events need rate limiting Use structured payloads - Tagged unions for multiple event types Window targeting - emit() for specific, emit_all() for broadcast Frontend events - Use emit() from frontend, listen in backend setup

返回排行榜