首页 / Tauri 2 入门教程 / 多窗口管理与通信

Tauri 2 入门教程

多窗口管理与通信

本教程共 48 篇 · 第 28 篇 · 更新于 2026-08-09 · 约 8 分钟阅读

多窗口WebviewWindowBuilder事件通信emitlisten

本节目标:学会在运行期用 WebviewWindowBuilder 创建新窗口、用 label 定位已有窗口,并通过事件(emit/listen)让窗口之间互相通信。

运行时创建窗口

前面章节的窗口都在 tauri.conf.json 里预先写好。但很多场景需要「点一下按钮,弹出一个新窗口」——这要在运行期用代码创建。Tauri 2 提供 WebviewWindowBuilder,用建造者模式拼出一个窗口:

use tauri::{AppHandle, WebviewUrl, WebviewWindowBuilder};

#[tauri::command]
fn open_settings(app: AppHandle) -> tauri::Result<()> {
    WebviewWindowBuilder::new(
        &app,
        "settings",
        WebviewUrl::App("settings.html".into()),
    )
    .title("设置")
    .inner_size(400.0, 500.0)
    .build()?;
    Ok(())
}

WebviewWindowBuilder::new 收三个参数:AppHandle 引用、窗口 label(必须唯一)、要加载的页面地址。WebviewUrl::App("settings.html") 表示加载前端构建目录里的 settings.html。之后用 .title().inner_size() 等方法继续设置属性,最后 .build() 真正创建窗口,返回 Result,我们用 ? 把错误抛给调用方。

Warning

label 重复时 build() 会返回错误而非覆盖旧窗口。想做「再来一个相同窗口」,得给每个窗口不同的 label,比如用计数器拼:format!("editor-{i}"),否则第二次创建会失败。

用 label 定位已有窗口

创建了窗口,之后的操作大多靠 label 找到它。Tauri 2 有两种取法,效果一样:

// 方式一:从 AppHandle 取
let w = app.get_webview_window("settings");

// 方式二:用 WebviewWindow 的关联函数
let w = tauri::WebviewWindow::get_by_label("settings", &app);

两者都返回 Option<WebviewWindow>:有这个窗口就是 Some,没有就是 None。拿到句柄后就能调用方法,比如聚焦:

if let Some(w) = app.get_webview_window("settings") {
    w.set_focus()?;
}
Tip

app.webview_windows() 能一次性拿到所有窗口的 HashMap<label, WebviewWindow>,想遍历或统计数量时用它最方便,比如 app.webview_windows().len() 就是当前窗口数。

单例窗口:存在就聚焦,不存在就创建

弹设置窗口时,通常希望「只开一个」:已经开了就把它提到前面,而不是叠一堆。结合上面的取法,很容易实现:

#[tauri::command]
fn open_settings(app: AppHandle) -> tauri::Result<()> {
    match app.get_webview_window("settings") {
        // 已存在:直接聚焦
        Some(w) => {
            w.set_focus()?;
        }
        // 不存在:新建
        None => {
            WebviewWindowBuilder::new(
                &app,
                "settings",
                WebviewUrl::App("settings.html".into()),
            )
            .title("设置")
            .build()?;
        }
    }
    Ok(())
}

get_webview_window 试取,有就 set_focus,没有才 build。这样无论点多少次,设置窗口始终只有一个,不会越开越多。

窗口间通信:事件机制

多窗口最大的难点是「它们怎么说话」。Tauri 的事件(event)机制就是为此而生。核心两个动作:emit(发出事件)和 listen(监听事件)。

从 Rust 向某个具体窗口发事件:

use tauri::Emitter;

if let Some(w) = app.get_webview_window("main") {
    w.emit("refresh-data", "数据已更新")?;
}

emit 的第一个参数是事件名,第二个是负载(任意可序列化的数据)。它只发往 main 这一个窗口。前端在对应的窗口里监听:

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

await listen('refresh-data', (event) => {
  console.log('收到消息:', event.payload);
});
Note

事件名两端要一致,才能对上。emit("refresh-data", ...) 必须配 listen('refresh-data', ...)。想发往所有窗口,用 app.emit(...)(在 AppHandle 上调用)即可,不必逐个窗口发。

前端也能主动发事件

通信是双向的。前端可以用 emit 把事件发回 Rust,后端用 listen 接收:

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

await emit('save-request', { name: '小明' });

Rust 在 setup 里监听:

use tauri::{Listener, Emitter};

app.listen("save-request", |event| {
    let payload = event.payload();
    println!("收到前端请求:{payload}");
});

这样,窗口 A 的前端发事件,窗口 B 的后端或前端都能监听——只要在事件名上约定好,就能实现任意窗口间的消息转发。常见套路是:窗口 A 发一个「请通知主窗口」的事件,Rust 收到后用 get_by_label 找到主窗口再 emit 过去,完成定向投递。

Tip

跨窗口通信时,优先用「Rust 当中转站」:前端只管发,Rust 负责按 label 精准投递。这样前端不用知道目标窗口叫什么,逻辑更清晰,也更容易在后端统一做权限与格式校验。

前端也能创建窗口

不写 Rust 也能开窗口。@tauri-apps/api/window 提供了 WebviewWindow 类:

import { WebviewWindow } from '@tauri-apps/api/window';

const win = new WebviewWindow('editor-1', {
  url: 'editor.html',
  title: '编辑器',
  width: 800,
  height: 600,
});

win.once('tauri-created', () => console.log('窗口建好了'));
win.once('tauri-error', (e) => console.error('创建失败', e));

构造函数的第一个参数是 label,第二个是配置对象(字段名和 tauri.conf.jsonapp.windows 类似)。tauri-created / tauri-error 是两个一次性事件,用来知道创建成功还是失败。注意 label 同样不能重复,重复了会触发 tauri-error

关闭窗口与一次性监听

关闭当前窗口用 win.close()(JS 端)或 Rust 端的 window.close()。监听事件时,如果只想响应一次,用 once 代替 listen

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

await once('refresh-data', (event) => {
  console.log('只处理这一次', event.payload);
});

once 触发一次后自动取消,适合「等某个结果返回」的场景,比如等设置窗口保存完毕后刷新主界面。它和 listen 的区别仅在于是否重复触发,事件名、负载格式完全一样。

label 的复用与清理

窗口关闭后,它的 label 就空出来了,可以再次用同一个 label 创建新窗口。但如果在窗口还开着时第二次用同 labelbuild(),会拿到错误而不是新窗口。所以「先 get_by_label 判断是否存在、存在就聚焦、不存在再建」这个单例套路(见前文)不只是体验问题,也是避免创建报错的稳妥写法。

小结

多窗口的本质就是「用 label 区分、用 WebviewWindowBuilder 创建、用事件传递消息」。创建时记得查重,通信时记住事件名要对齐,并以 Rust 做中转能省掉前端对目标窗口名的依赖。掌握这三板斧,复杂界面也能拆成多个清爽的窗口来组织。