基于tauri+vue3.x多開窗口|Tauri創(chuàng)建多窗體實(shí)踐
最近一種在搗鼓 Tauri 集成 Vue3 技術(shù)開發(fā)桌面端應(yīng)用實(shí)踐,tauri 實(shí)現(xiàn)創(chuàng)建多窗口,窗口之間通訊功能。

開始正文之前,先來了解下 tauri 結(jié)合 vue3.js 快速創(chuàng)建項(xiàng)目。
tauri 在 github 上star高達(dá)53K+,而且呈快速增長(zhǎng)趨勢(shì)。相比electron構(gòu)建應(yīng)用更具優(yōu)勢(shì)。

分別用 Tauri 和 Electron 打包測(cè)試一個(gè) todo list 程序。
Electron打包體積 69 M,Tauri打包體積才只有 7.5 M。
Tauri 構(gòu)建的桌面應(yīng)用體積遠(yuǎn)遠(yuǎn)比 Electron 構(gòu)建的小得多。因?yàn)樗艞壛梭w積龐大的 Chromium 內(nèi)核和Nodejs,tauri前端集成了 webview,后端使用 Rust。而且 Tauri 構(gòu)建應(yīng)用還提供了諸多初始化程序模板,比如原生 JavaScript、Vue2/3、React、Svelte.js、SvelteKit 等。

準(zhǔn)備工作
首先您需要安裝 Rust 及其他系統(tǒng)依賴。
- "C++ 生成工具" 和 Windows 10 SDK。
- Tauri 需要 WebView2 才能在 Windows 上呈現(xiàn)網(wǎng)頁內(nèi)容,所以您必須先安裝 WebView2。
- Rust
具體操作,請(qǐng)前往 https://tauri.app/zh/v1/guides/getting-started/prerequisites 來按步驟操作。

- 創(chuàng)建 tauri 初始化項(xiàng)目
具體的前端框架模板,大家根據(jù)實(shí)際情況選擇。
npm create tauri-app

- 開發(fā)/構(gòu)建打包
tauri dev tauri build

非常簡(jiǎn)單的幾步就能快速搭建 vue3+tauri 桌面端模板。接下來就能順利的開發(fā)了。
tauri 也提供了如下幾種常用創(chuàng)建多窗口的方法。
- tauri.conf.json
{ "tauri": { "windows": [ { "label": "external", "title": "Tauri App", "url": "https://tauri.app" }, { "label": "local", "title": "Tauri", "url": "home.html" } ] } }
- src-tauri/src/main.rs
tauri::Builder::default() .setup(|app| { let docs_window = tauri::WindowBuilder::new( app, "external", /* the unique window label */ tauri::WindowUrl::External("https://tauri.app/".parse().unwrap()) ).build()?; let local_window = tauri::WindowBuilder::new( app, "local", tauri::WindowUrl::App("index.html".into()) ).build()?; Ok(()) })
- 通過前端 JS 創(chuàng)建窗口。
import { WebviewWindow } from '@tauri-apps/api/window'
const webview = new WebviewWindow('main_win', {
url: '/home',
})
webview.once('tauri://created', function () {
// webview window successfully created
})
webview.once('tauri://error', function (e) {
// an error happened creating the webview window
})
具體詳細(xì)的介紹,大家可以去官網(wǎng)查看,文檔都有非常詳細(xì)的講解。
https://tauri.app/zh/v1/guides/features/multiwindow
上面介紹的方法比較適用于一些簡(jiǎn)單的窗口,對(duì)于一些復(fù)雜多開窗口,還得封裝一個(gè)窗口創(chuàng)建器,直接通過傳入?yún)?shù)快速生成窗體。
createWin({ label: 'Home', title: '主頁', url: '/home', width: 800, height: 600, })
新建一個(gè) windows 文件夾,用來封裝窗口及調(diào)用窗口。

/** * @desc 窗口容器 * @author: YXY Q:282310962 * @time 2022.10 */ import { WebviewWindow, appWindow, getAll, getCurrent } from '@tauri-apps/api/window' import { relaunch, exit } from '@tauri-apps/api/process' import { emit, listen } from '@tauri-apps/api/event' import { setWin } from './actions' // 系統(tǒng)參數(shù)配置 export const windowConfig = { label: null, // 窗口唯一label title: '', // 窗口標(biāo)題 url: '', // 路由地址url width: 900, // 窗口寬度 height: 640, // 窗口高度 minWidth: null, // 窗口最小寬度 minHeight: null, // 窗口最小高度 x: null, // 窗口相對(duì)于屏幕左側(cè)坐標(biāo) y: null, // 窗口相對(duì)于屏幕頂端坐標(biāo) center: true, // 窗口居中顯示 resizable: true, // 是否支持縮放 maximized: false, // 最大化窗口 decorations: false, // 窗口是否無邊框及導(dǎo)航條 alwaysOnTop: false, // 置頂窗口 } class Windows { constructor() { this.mainWin = null } // 獲取窗口 getWin(label) { return WebviewWindow.getByLabel(label) } // 獲取全部窗口 getAllWin() { return getAll() } // 創(chuàng)建新窗口 async createWin(options) { const args = Object.assign({}, windowConfig, options) // 判斷窗口是否存在 const existWin = getAll().find(w => w.label == args.label) if(existWin) { if(existWin.label.indexOf('main') == -1) { await existWin?.unminimize() await existWin?.setFocus() return } await existWin?.close() } // 創(chuàng)建窗口對(duì)象 let win = new WebviewWindow(args.label, args) // 是否最大化 if(args.maximized && args.resizable) { win.maximize() } // 窗口創(chuàng)建完畢/失敗 win.once('tauri://created', async() => { console.log('window create success!') ... }) win.once('tauri://error', async() => { console.log('window create error!') }) } // 開啟主進(jìn)程監(jiān)聽事件 async listen() { // 創(chuàng)建新窗體 await listen('win-create', (event) => { console.log(event) this.createWin(JSON.parse(event.payload)) }) // 顯示窗體 await listen('win-show', async(event) => { if(appWindow.label.indexOf('main') == -1) return await appWindow.show() await appWindow.unminimize() await appWindow.setFocus() }) // 隱藏窗體 await listen('win-hide', async(event) => { if(appWindow.label.indexOf('main') == -1) return await appWindow.hide() }) // 退出應(yīng)用 await listen('win-exit', async(event) => { setWin('logout') await exit() }) // 重啟應(yīng)用 await listen('win-relaunch', async(event) => { await relaunch() }) // 主/渲染進(jìn)程傳參 await listen('win-setdata', async(event) => { await emit('win-postdata', JSON.parse(event.payload)) }) } } export default Windows
actions.js進(jìn)行一些調(diào)用處理。
/** * 處理渲染器進(jìn)程到主進(jìn)程的異步通信 */ import { WebviewWindow } from '@tauri-apps/api/window' import { emit } from '@tauri-apps/api/event' /** * @desc 創(chuàng)建新窗口 */ export async function createWin(args) { await emit('win-create', args) } /** * @desc 獲取窗口 * @param args {string} */ export async function getWin(label) { return await WebviewWindow.getByLabel(label) } /** * @desc 設(shè)置窗口 * @param type {string} 'show'|'hide'|'close'|'min'|'max'|'max2min'|'exit'|'relaunch' */ export async function setWin(type) { await emit('win-' + type) } /** * @desc 登錄窗口 */ export async function loginWin() { await createWin({ label: 'Login', title: '登錄', url: '/login', width: 320, height: 420, resizable: false, alwaysOnTop: true, }) } // ...
在需要調(diào)用創(chuàng)建窗口的.vue頁面,引入actions.js文件。
import { loginWin, createWin } from '@/windows/actions'
const createManageWin = async() => { createWin({ label: 'Manage', title: '管理頁面', url: '/manage', width: 600, height: 450, minWidth: 300, minHeight: 200 }) } const createAboutWin = async() => { createWin({ label: 'About', title: '關(guān)于頁面', url: '/about', width: 500, height: 500, resizable: false, alwaysOnTop: true }) }
一些注意點(diǎn)
- 創(chuàng)建系統(tǒng)托盤圖標(biāo)

use tauri::{ AppHandle, Manager, CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu }; // 托盤菜單 pub fn menu() -> SystemTray { let quit = CustomMenuItem::new("quit".to_string(), "Quit"); let show = CustomMenuItem::new("show".to_string(), "Show"); let hide = CustomMenuItem::new("hide".to_string(), "Hide"); let change_ico = CustomMenuItem::new("change_ico".to_string(), "Change Icon"); let tray_menu = SystemTrayMenu::new() .add_submenu(SystemTraySubmenu::new( "Language", // 語言菜單 SystemTrayMenu::new() .add_item(CustomMenuItem::new("lang_english".to_string(), "English")) .add_item(CustomMenuItem::new("lang_zh_CN".to_string(), "簡(jiǎn)體中文")) .add_item(CustomMenuItem::new("lang_zh_HK".to_string(), "繁體中文")), )) .add_native_item(SystemTrayMenuItem::Separator) // 分割線 .add_item(change_ico) .add_native_item(SystemTrayMenuItem::Separator) .add_item(hide) .add_item(show) .add_native_item(SystemTrayMenuItem::Separator) .add_item(quit); SystemTray::new().with_menu(tray_menu) } // 托盤事件 pub fn handler(app: &AppHandle, event: SystemTrayEvent) { match event { SystemTrayEvent::LeftClick { position: _, size: _, .. } => { println!("點(diǎn)擊左鍵"); } SystemTrayEvent::RightClick { position: _, size: _, .. } => { println!("點(diǎn)擊右鍵"); } SystemTrayEvent::DoubleClick { position: _, size: _, .. } => { println!("雙擊"); } SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { "change_ico" => { // 更新托盤圖標(biāo) app.tray_handle() .set_icon(tauri::Icon::Raw( include_bytes!("../icons/new.png").to_vec() )) .unwrap(); } lang if lang.contains("lang_") => { // 選擇語言,匹配 id 前綴包含 `lang_` 的事件 Lang::new( app, id, // 點(diǎn)擊菜單的 id vec![ Lang { name: "English", id: "lang_english", }, Lang { name: "繁體中文", id: "lang_zh_HK", }, Lang { name: "簡(jiǎn)體中文", id: "lang_zh_CN", }, ], ); } "hide" => { // let window = app.get_window("main").unwrap(); // window.show().unwrap(); println!("點(diǎn)擊隱藏"); } "show" => { println!("點(diǎn)擊顯示"); } "quit" => { println!("點(diǎn)擊退出"); std::process::exit(0); } _ => {} }, _ => {} } } struct Lang<'a> { name: &'a str, id: &'a str, } impl Lang<'static> { fn new(app: &AppHandle, id: String, langs: Vec<Lang>) { // 獲取點(diǎn)擊的菜單項(xiàng) langs.iter().for_each(|lang| { let handle = app.tray_handle().get_item(lang.id); if lang.id.to_string() == id.as_str() { // 設(shè)置菜單名稱 handle.set_title(format!(" {}", lang.name)).unwrap(); // 還可以使用 `set_selected`、`set_enabled` 和 `set_native_image`(僅限 macOS) handle.set_selected(true).unwrap(); } else { handle.set_title(lang.name).unwrap(); handle.set_selected(false).unwrap(); } }); } }
創(chuàng)建托盤圖標(biāo),默認(rèn)圖標(biāo)文件在src-tauri/icons目錄下。如果想使用自定義的.ico圖標(biāo),可通過tauri.cong.json文件配置。
"systemTray": { "iconPath": "icons/tray.ico", "iconAsTemplate": true, "menuOnLeftClick": false }
如果setIcon報(bào)錯(cuò),則需要在 src-tauri/src/Cargo.toml 中配置 icon-ico 或 icon-png

- tauri 配置自定義拖拽區(qū)域。
當(dāng)創(chuàng)建窗口的時(shí)候配置了 decorations: false 則會(huì)不顯示窗口邊框及頂部導(dǎo)航欄。
此時(shí)在需要拖動(dòng)元素上加一個(gè) data-tauri-drag-region 屬性,即可實(shí)現(xiàn)自定義區(qū)域拖動(dòng)窗口功能。這個(gè)功能有些類似 electron 中自定義拖拽 -webkit-app-region: drag

不過點(diǎn)擊窗口右鍵,會(huì)出現(xiàn)系統(tǒng)菜單。這樣顯得應(yīng)用不夠原生,可以簡(jiǎn)單的通過禁用右鍵菜單來屏蔽功能。

export function disableWinMenu() { document.addEventListener('contextmenu', e => e.preventDefault()) } disableWinMenu()
好了,基于 tauri+vue3 構(gòu)建多窗口桌面應(yīng)用就分享到這里。希望對(duì)大家有丟丟幫助哈~~ ??
目前最新版Tauri2.0+Vite5跨平臺(tái)實(shí)戰(zhàn)項(xiàng)目已經(jīng)同步到我的原創(chuàng)作品集。
Tauri2.0-Vue3OS桌面端os平臺(tái)|tauri2+vite6+arco電腦版OS管理系統(tǒng)
Tauri2.0+Vite5聊天室|vue3+tauri2+element-plus仿微信|tauri聊天應(yīng)用
tauri2.0-admin桌面端后臺(tái)系統(tǒng)|Tauri2+Vite5+ElementPlus管理后臺(tái)EXE程序
最后附上三個(gè)最新Electron+Vue3原創(chuàng)重磅跨平臺(tái)實(shí)例。
Electron32-ViteOS桌面版os系統(tǒng)|vue3+electron+arco客戶端OS管理模板
Vite5+Electron聊天室|electron31跨平臺(tái)仿微信EXE客戶端|vue3聊天程序
Electron31-Vue3Admin管理系統(tǒng)|vite5+electron+pinia桌面端后臺(tái)Exe


浙公網(wǎng)安備 33010602011771號(hào)