使用 TauriSvelteKit 开发,有几种实现多窗口管理的方式
官方文档:Multiwindow | Tauri Apps

创建窗口

1. Tauri 配置文件配置

如果使用的是 Tauri 的官方 Sveltekit 模板,则直接配置 url 就可以实现多窗口

CleanShot 2024-07-25 at 21.07.32@2x.png

    "windows": [
      {
        "title": "采购",
        "width": 1180,
        "height": 900
      },
      {
        "title": "设置",
        "width": 600,
        "height": 800,
        "label": "setting",
        "url": "/settings/general",
        "center": true,
        "resizable": false,
        "visible": true
      }
    ],
  • url:支持两种类型 WindowURL
    • 外部URL
    • 应用程序 URL 的路径部分。例如,要加载tauri://localhost/settings,只需设置 url/settings 即可
  • visible 配置的值如果为 true,则应用启动时会自动打开多窗口,反之设置为false则默认隐藏,关于 windows 下更多的配置属性看这里 Configuration | Tauri Apps

2. 运行时通过Rust创建

这种方式与在 tauri.conf.json 中配置的行为类似,可以设置窗口的名称,默认是否展示等

fn main() {
    tauri::Builder::default()
        .setup(|app| {
	        WindowBuilder::new(
	            app,
	            "settings",
	            WindowUrl::External("http://localhost:1420/settings/general".parse().unwrap()),
	        )
	        .title("设置")
		    .visible(false)
	        .inner_size(600.0, 500.0)
	        .position(550.0, 100.0)
	        .build()?;
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

3. tauri::command

通过 tauri::command 定义一个创建窗口的命令,然后前端通过 invoke 来触发这个操作
command 定义

#[tauir::command]
pub async fn open_window_test(app: AppHandle) {
    WindowBuilder::new(
        &app,
        "browser",
        WindowUrl::External("http://localhost:1420/settings/general".parse().unwrap()),
    )
    .title("百度")
    .inner_size(1400.0, 800.0)
    .build()
    .expect("Failed to create window");
}

前端调用 invoke

import { invoke } from "@tauri-apps/api"
 
async function onOpenSetting() {
  try {
    await invoke("open_window_test")
  } catch (error) {
    console.error("error:", error)
  }
}

4. 纯JS创建

在前端代码中使用 WebviewWindow API 来动态创建窗口

import { WebviewWindow } from "@tauri-apps/api/window"
 
function onCreateWindow() {
  const webview = new WebviewWindow("settings", {
    url: "/settings/general",
    title: "JS设置",
    width: 800,
    height: 600,
    x: 100,
    y: 100,
  })
 
  webview.once("tauri://created", () => {
    console.log("窗口创建成功")
  })
 
  webview.once("tauri://error", (error) => {
    console.log("窗口创建失败", error)
  })
}

注意这种方式需要在 tauri.conf.json 中开启 window > create 权限

"allowlist": {
  "window": {
	"create": true
  },
}

通过 WebviewWindow API 关闭窗口

import { WebviewWindow } from "@tauri-apps/api/window"
 
function onCloseWindow() {
  // 获取窗口的引用
  const window = WebviewWindow.getByLabel("settings")
  window?.close()
}

打开窗口

只针对应用初始化时已经创建好,但是默认隐藏的窗口,第一种使用 invoke 通信,第二种则是监听 event 事件

1. Invoke 打开窗口

首先定义 invoke

#[command]
pub fn open_setting(app_handle: tauri::AppHandle) {
    if let Some(window) = app_handle.get_window("setting") {
        window.show().unwrap();
        window.set_focus().unwrap();
    }
}

前端调用 invoke 打开窗口

import { invoke } from "@tauri-apps/api"
 
async function onOpenSetting() {
  try {
    await invoke("open_setting")
  } catch (error) {
    console.error("error:", error)
  }
}

2. Event 事件触发

rust 中定义事件监听

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
 
mod api;
mod app;
 
use app::invoke;
use app::window;
use tauri::Manager;
use tauri_plugin_store::Builder as windowStorePlugin;
 
fn main() {
    let tauri_app = tauri::Builder::default().setup(|app| {
        // 设置事件监听
        let app_handle = app.handle();
        let app_handle_clone = app_handle.clone();
        tauri::async_runtime::spawn(async move {
	        // 这里通过监听前端的 emit 事件来触发窗口的打开
            app_handle_clone.listen_global("open_setting", move |_event| {
                let window = app_handle.get_window("setting").unwrap();
                window.show().unwrap();
                window.set_focus().unwrap();
            });
        });
 
        Ok(())
    });
 
    tauri_app
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
 

前端调用 emit 触发事件

import { emit } from "@tauri-apps/api/event"
 
async function showSettingsWindow() {
  await emit("open_setting")
}

使用 invoke 还是 emit

invoke 方法用于前端直接调用 Rust 后端的命令,并等待结果。它适用于需要获得立即响应或需要进行数据交互的场景。
emit 方法是一次性的单向 IPC 消息,用于前端向后端发送事件,不要求立即响应。它适用于需要异步处理或广播消息的场景。更多请查看 - Inter-Process Communication | Tauri Apps

设置窗口关闭行为为隐藏

默认情况下,当你手动关闭一个窗口时,窗口会被销毁。这就导致了我们通过前端代码打开的设置窗口,如果手动关闭后,窗口被销毁,再次打开时就无法打开了。

为了能够在关闭设置窗口后可以继续打开,有两种方式可以实现,Tauri 提供了一种机制,可以在窗口关闭事件触发时将其隐藏,而不是销毁窗口。

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let app_handle = app.handle();
            let window = app_handle.get_window("setting").unwrap();
            window.on_window_event(move |event| match event {
                WindowEvent::CloseRequested { api, .. } => {
                    // 取消默认的关闭行为
                    api.prevent_close();
                    // 隐藏窗口而不是关闭
                    let window = app_handle.get_window("setting").unwrap();
                    window.hide().unwrap();
                }
                _ => {}
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

再次通过前端代码打开设置窗口,然后点击设置窗口的关闭按钮,设置窗口消失,再次打开正常。

如果你在 windows 配置了 decorations 值为 false(隐藏顶部栏和边框),并且使用了自定义的顶部栏,则可以通过自定义关闭按钮来设置关闭行为为隐藏

import { appWindow } from "@tauri-apps/api/window"
 
function onCloseWindow() {
  appWindow.hide().catch((e) => console.error("Failed to hide window:", e))
}

appWindow.hide() 是隐藏当前窗口(即调用该方法所在的窗口)。如果你有多个窗口,并且想要在特定窗口上执行操作(例如隐藏某个特定窗口)。

获取窗口

获取窗口的示例同样有几种方式

Rust 获取

#[tauri::command]
fn get_window(app_handle: tauri::AppHandle, label: String) -> Result<(), String> {
    if let Some(window) = app_handle.get_window(&label) {
        println!("window: {:?}", window);
        window.close();
    }
    Ok(())
}

JS 获取

import { WebviewWindow } from "@tauri-apps/api/window"
 
function onCloseWindow() {
  // 获取窗口的引用
  const window = WebviewWindow.getByLabel("settings")
  window?.close()
}

窗口间通信

在 Tauri 中窗口之间通信有几种场景和方式,一种是主进程与窗口之间的相互通信,另一种是窗口与窗口之间的相互通信.

不推荐直接进行窗口与窗口之间的通信,而是通过主进程设置事件监听的方式进行窗口之间的事件转发,通过主进程全局管理所有窗口和事件,更符合 Tauri 的设计理念和安全模型,确保各窗口之间的通信是有序和受控的。

CleanShot 2024-08-13 at 09.43.54@2x.png

上图中描述了Event 的处理方式,Tauri 应用中有三个独立的窗口 主窗口 Main、设置窗口 Settings 以及 About 窗口,需求是要在这三个窗口之间进行 Event 通信,例如从 Main 发送事件到 Settings或者从 Settings 发送事件到 About

Rust 主进程设置 Event 事件转发:

// main.rs
 
/**
* 发送到Main窗口
*/
pub fn event_listener_to_main(app_handle: AppHandle) {
    let app = app_handle.clone();
    app.listen_global("EVENT_SEND_TO_MAIN", move |event| {
        if let Some(payload) = event.payload() {
            match serde_json::from_str::<Value>(payload) {
                Ok(parsed_payload) => {
                    if let Some(window) = app_handle.get_window("main") {
                        window.emit("EVENT_TO_MAIN", parsed_payload).unwrap();
                    } else {
                        println!("Main window not found");
                    }
                }
                Err(e) => println!("Failed to parse payload: {:?}", e),
            }
        } else {
            println!("No payload found in event");
        }
    });
}

event_listener_to_main 方法定义了向 main 窗口发送事件的方式,监听了全局 Event EVENT_SEND_TO_MAIN, 当收到消息时,会获取 mian 窗口,main 窗口存在时,向其广播 EVENT_TO_MAIN 事件,这样避免了向其他两个窗口广播事件。
同理,Settings 窗口与 About 窗口也需要设置事件监听转发

pub fn event_listener_to_settings(app_handle: AppHandle) {
    let app = app_handle.clone();
    app.listen_global("EVENT_SEND_TO_SETTINGS", move |event| {
        if let Some(payload) = event.payload() {
            match serde_json::from_str::<Value>(payload) {
                Ok(parsed_payload) => {
                    if let Some(window) = app_handle.get_window("settings") {
                        window.emit("EVENT_TO_SETTINGS", parsed_payload).unwrap();
                    } else {
                        println!("Main window not found");
                    }
                }
                Err(e) => println!("Failed to parse payload: {:?}", e),
            }
        } else {
            println!("No payload found in event");
        }
    });
}

MainSettings 等窗口需要监听当前窗口的事件, 例如 Main 窗口需要监听 EVENT_SEND_TO_MAINSettings 需要监听 EVENT_SEND_TO_SETTINGS
前端进行事件调用时,可以在 payload 参数里额外自定义一个 Event 类型,用于对事件进行细分处理,Event 参数如下

type EventPayload<T extends any> = {
	type: string;
	payload: T;
}

调用 event 方法

event.emit('EVENT_SEND_TO_MAIN', {
	type: 'GET_USER_INFO',
	payload: { id }
})

参考