mcpField
L
listening-to-tauri-events

listening-to-tauri-events

github.com/dchuk开发36171 次安装40 周前更新
代码仓库

关于 listening-to-tauri-events

面向 Tauri v2 前端开发者的事件订阅指南,讲解如何接收 Rust 发出的事件。内容涵盖全局与 Webview 定向监听、类型化载荷、一次性订阅和清理。

主要功能

  1. 01全局事件订阅
  2. 02Webview 专属监听
  3. 03类型化事件载荷
  4. 04一次性监听器
  5. 05手动清理监听器
  6. 06Rust 事件发送示例

适用场景

  1. 01接收 Rust 后端更新
  2. 02处理特定窗口的事件
  3. 03在 SPA 组件中移除监听器
  4. 04为 TypeScript 事件数据指定类型

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill listening-to-tauri-events

安装后在 Claude Code 中输入/listening-to-tauri-events确认已加载。

详细介绍

在前端监听 Tauri 事件

本技能介绍如何在 Tauri v2 应用中使用 @tauri-apps/api/event 模块订阅由 Rust 发出的事件。

核心概念

Tauri 提供两种事件作用域:

  1. 全局事件 - 广播给所有 webview 中的所有监听器
  2. Webview 专属事件 - 定向发送到特定的 webview 窗口

重要: Webview 专属事件不会被全局监听器接收。请根据 Rust 发出事件的方式选择合适的监听器类型。

安装

事件 API 是 Tauri 核心 API 包的一部分:

npm install @tauri-apps/api
# or
pnpm add @tauri-apps/api
# or
yarn add @tauri-apps/api

监听全局事件

基本监听模式
import { listen } from '@tauri-apps/api/event';

// Listen for a global event
const unlisten = await listen('download-started', (event) => {
  console.log('Event received:', event.payload);
});

// Clean up when done
unlisten();
类型化事件负载

定义与 Rust 负载结构匹配的 TypeScript 接口:

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

// Define the payload type (matches Rust struct with camelCase)
interface DownloadStarted {
  url: string;
  downloadId: number;
  contentLength: number;
}

// Use generic type parameter for type safety
const unlisten = await listen<DownloadStarted>('download-started', (event) => {
  console.log(`Downloading from ${event.payload.url}`);
  console.log(`Content length: ${event.payload.contentLength} bytes`);
  console.log(`Download ID: ${event.payload.downloadId}`);
});
事件对象结构

事件回调会接收一个 Event<T> 对象:

interface Event<T> {
  /** Event name */
  event: string;
  /** Event identifier */
  id: number;
  /** Event payload (your custom type) */
  payload: T;
}

监听 Webview 专属事件

对于使用 emit_to 或 emit_filter 定向发送到特定 webview 的事件:

import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

const appWebview = getCurrentWebviewWindow();

// Listen for events targeted to this specific webview
const unlisten = await appWebview.listen<string>('login-result', (event) => {
  if (event.payload === 'loggedIn') {
    localStorage.setItem('authenticated', 'true');
  } else {
    console.error('Login failed:', event.payload);
  }
});

一次性事件监听器

对于只需处理一次的事件,请使用 once:

import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';

// Global one-time listener
await once('app-ready', (event) => {
  console.log('Application is ready');
});

// Webview-specific one-time listener
const appWebview = getCurrentWebviewWindow();
await appWebview.once('initialized', (event) => {
  console.log('Webview initialized');
});

清理和取消订阅

手动清理

不再需要监听器时,请务必取消订阅:

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

const unlisten = await listen('my-event', (event) => {
  // Handle event
});

// Later, when done listening
unlisten();
React 组件清理
import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';

interface ProgressPayload {
  percent: number;
  message: string;
}

function DownloadProgress() {
  useEffect(() => {
    let unlisten: (() => void) | undefined;

    const setupListener = async () => {
      unlisten = await listen<ProgressPayload>('download-progress', (event) => {
        console.log(`Progress: ${event.payload.percent}%`);
      });
    };

    setupListener();

    // Cleanup when component unmounts
    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, []);

  return <div>Listening for progress...</div>;
}
Vue Composition API 清理
import { onMounted, onUnmounted } from 'vue';
import { listen } from '@tauri-apps/api/event';

interface NotificationPayload {
  title: string;
  body: string;
}

export default {
  setup() {
    let unlisten: (() => void) | undefined;

    onMounted(async () => {
      unlisten = await listen<NotificationPayload>('notification', (event) => {
        console.log(`${event.payload.title}: ${event.payload.body}`);
      });
    });

    onUnmounted(() => {
      if (unlisten) {
        unlisten();
      }
    });
  }
};
Svelte 清理
<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { listen } from '@tauri-apps/api/event';

  interface StatusPayload {
    status: 'idle' | 'loading' | 'complete';
  }

  let unlisten: (() => void) | undefined;

  onMount(async () => {
    unlisten = await listen<StatusPayload>('status-change', (event) => {
      console.log('Status:', event.payload.status);
    });
  });

  onDestroy(() => {
    if (unlisten) {
      unlisten();
    }
  });
</script>

自动清理监听器

Tauri 会在以下情况下自动清除监听器:

  • 页面重新加载 - 清除所有监听器
  • URL 导航 - 清除监听器(SPA 路由器除外)

警告: 不会触发整页重新加载的单页应用路由器不会自动清理监听器。你必须手动取消订阅。

多个事件监听器

监听多个事件
import { listen } from '@tauri-apps/api/event';

interface DownloadEvent {
  url: string;
}

interface ProgressEvent {
  percent: number;
}

interface CompleteEvent {
  path: string;
  size: number;
}

async function setupDownloadListeners() {
  const unlisteners: Array<() => void> = [];

  unlisteners.push(
    await listen<DownloadEvent>('download-started', (event) => {
      console.log(`Started downloading: ${event.payload.url}`);
    })
  );

  unlisteners.push(
    await listen<ProgressEvent>('download-progress', (event) => {
      console.log(`Progress: ${event.payload.percent}%`);
    })
  );

  unlisteners.push(
    await listen<CompleteEvent>('download-complete', (event) => {
      console.log(`Complete: ${event.payload.path} (${event.payload.size} bytes)`);
    })
  );

  // Return cleanup function
  return () => {
    unlisteners.forEach((unlisten) => unlisten());
  };
}

// Usage
const cleanup = await setupDownloadListeners();
// Later...
cleanup();

类型定义参考

创建类型化事件辅助函数
import { listen, once, Event } from '@tauri-apps/api/event';

// Define all your event types in one place
export interface AppEvents {
  'download-started': { url: string; downloadId: number };
  'download-progress': { downloadId: number; percent: number };
  'download-complete': { downloadId: number; path: string };
  'download-error': { downloadId: number; error: string };
  'notification': { title: string; body: string };
}

// Type-safe listen helper
export async function listenTyped<K extends keyof AppEvents>(
  eventName: K,
  handler: (event: Event<AppEvents[K]>) => void
): Promise<() => void> {
  return listen<AppEvents[K]>(eventName, handler);
}

// Usage
const unlisten = await listenTyped('download-started', (event) => {
  // event.payload is typed as { url: string; downloadId: number }
  console.log(event.payload.url);
});

对应的 Rust 发出事件代码

以下是 Rust 发出事件的方式,供参考:

全局发出
use tauri::{AppHandle, Emitter};
use serde::Serialize;

#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadStarted {
    url: String,
    download_id: usize,
    content_length: usize,
}

#[tauri::command]
fn start_download(app: AppHandle, url: String) {
    app.emit("download-started", DownloadStarted {
        url,
        download_id: 1,
        content_length: 1024,
    }).unwrap();
}
Webview 专属发出
use tauri::{AppHandle, Emitter};

#[tauri::command]
fn notify_window(app: AppHandle, window_label: &str, message: String) {
    // Emit to a specific webview by label
    app.emit_to(window_label, "notification", message).unwrap();
}
筛选后发出
use tauri::{AppHandle, Emitter, EventTarget};

#[tauri::command]
fn broadcast_to_editors(app: AppHandle, content: String) {
    app.emit_filter("content-update", content, |target| {
        match target {
            EventTarget::WebviewWindow { label } => {
                label.starts_with("editor-")
            }
            _ => false,
        }
    }).unwrap();
}

最佳实践

  1. 始终为事件负载定义 TypeScript 接口,以便在编译时发现类型不匹配

  2. 在 Rust 结构体中使用 serde(rename_all = "camelCase"),以匹配 JavaScript 命名约定

  3. 保存取消监听函数,并在清理时调用,以防止内存泄漏

  4. 对于只触发一次的初始化事件,使用 once

  5. 使监听器作用域与发出事件的作用域相匹配 - 对 emit_to 事件使用 webview 专属监听器

  6. 在共享模块中集中定义事件类型,以确保一致性

常见模式

事件总线模式
import { listen, Event } from '@tauri-apps/api/event';

type EventHandler<T> = (payload: T) => void;

class TauriEventBus {
  private unlisteners: Map<string, () => void> = new Map();

  async subscribe<T>(event: string, handler: EventHandler<T>): Promise<void> {
    if (this.unlisteners.has(event)) {
      this.unlisteners.get(event)!();
    }

    const unlisten = await listen<T>(event, (e: Event<T>) => {
      handler(e.payload);
    });

    this.unlisteners.set(event, unlisten);
  }

  unsubscribe(event: string): void {
    const unlisten = this.unlisteners.get(event);
    if (unlisten) {
      unlisten();
      this.unlisteners.delete(event);
    }
  }

  unsubscribeAll(): void {
    this.unlisteners.forEach((unlisten) => unlisten());
    this.unlisteners.clear();
  }
}

export const eventBus = new TauriEventBus();
React Hook 模式
import { useEffect, useState } from 'react';
import { listen, Event } from '@tauri-apps/api/event';

export function useTauriEvent<T>(eventName: string, initialValue: T): T {
  const [value, setValue] = useState<T>(initialValue);

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

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

    return () => {
      if (unlisten) {
        unlisten();
      }
    };
  }, [eventName]);

  return value;
}

// Usage
function StatusDisplay() {
  const status = useTauriEvent<string>('app-status', 'initializing');
  return <div>Status: {status}</div>;
}

评论

支持 Markdown