作为 Electron 开发者,我们已经习惯了使用 BrowserWindow
来管理窗口,以及通过各种内置模块来实现系统集成。在 Tauri 2.0 中,这些功能虽然概念类似,但实现方式有所不同。本文将帮助你快速掌握 Tauri 的窗口管理和系统集成功能。
窗口管理
基础窗口操作
Electron 方式
// main.js
const { BrowserWindow } = require('electron')// 创建窗口
const win = new BrowserWindow({width: 800,height: 600,frame: true,transparent: false,webPreferences: {nodeIntegration: true,contextIsolation: false}
})// 加载内容
win.loadFile('index.html')
// 或加载 URL
win.loadURL('https://example.com')// 窗口事件监听
win.on('closed', () => {// 窗口关闭时的处理
})
Tauri 方式
// main.rs
use tauri::{Window, WindowBuilder, WindowUrl};// 创建窗口
#[tauri::command]
async fn create_window(app_handle: tauri::AppHandle) -> Result<(), String> {WindowBuilder::new(&app_handle,"main",WindowUrl::App("index.html".into())).title("My App").inner_size(800.0, 600.0).resizable(true).decorations(true).transparent(false).build().map_err(|e| e.to_string())?;Ok(())
}// 前端调用
// App.tsx
import { WebviewWindow } from '@tauri-apps/api/window'// 创建新窗口
const createWindow = async () => {const webview = new WebviewWindow('main', {url: 'index.html',width: 800,height: 600})// 窗口事件监听webview.once('tauri://created', () => {// 窗口创建完成})webview.once('tauri://error', (e) => {// 窗口创建错误})
}
多窗口管理
Electron 方式
// main.js
const windows = new Map()function createWindow(name) {const win = new BrowserWindow({width: 800,height: 600})windows.set(name, win)win.on('closed', () => {windows.delete(name)})return win
}// 获取窗口
function getWindow(name) {return windows.get(name)
}
Tauri 方式
// main.rs
#[tauri::command]
async fn manage_windows(app_handle: tauri::AppHandle,window_label: String,action: String
) -> Result<(), String> {match action.as_str() {"create" => {WindowBuilder::new(&app_handle, window_label, WindowUrl::App("index.html".into())).build().map_err(|e| e.to_string())?;}"close" => {if let Some(window) = app_handle.get_window(&window_label) {window.close().map_err(|e| e.to_string())?;}}_ => return Err("Unknown action".into())}Ok(())
}
// windows.ts
import { WebviewWindow, getAll } from '@tauri-apps/api/window'// 创建窗口
export const createWindow = async (label: string) => {const webview = new WebviewWindow(label, {url: 'index.html'})return webview
}// 获取所有窗口
export const getAllWindows = () => {return getAll()
}// 获取特定窗口
export const getWindow = (label: string) => {return WebviewWindow.getByLabel(label)
}
窗口通信
Electron 方式
// main.js
ipcMain.on('message-to-window', (event, windowName, message) => {const targetWindow = windows.get(windowName)if (targetWindow) {targetWindow.webContents.send('message', message)}
})// renderer.js
ipcRenderer.on('message', (event, message) => {console.log('Received:', message)
})
Tauri 方式
// main.rs
#[tauri::command]
async fn send_message(window: Window,target: String,message: String
) -> Result<(), String> {if let Some(target_window) = window.app_handle().get_window(&target) {target_window.emit("message", message).map_err(|e| e.to_string())?;}Ok(())
}
// App.tsx
import { emit, listen } from '@tauri-apps/api/event'// 发送消息
const sendMessage = async (target: string, message: string) => {await emit('message-to-window', {target,message})
}// 监听消息
const unlisten = await listen('message', (event) => {console.log('Received:', event.payload)
})
系统集成
系统托盘
Electron 方式
// main.js
const { app, Tray, Menu } = require('electron')let tray = nullapp.whenReady().then(() => {tray = new Tray('icon.png')const contextMenu = Menu.buildFromTemplate([{ label: 'Show App', click: () => win.show() },{ label: 'Quit', click: () => app.quit() }])tray.setToolTip('My App')tray.setContextMenu(contextMenu)
})
Tauri 方式
// main.rs
use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayMenuItem, SystemTrayEvent
};fn main() {let quit = CustomMenuItem::new("quit".to_string(), "Quit");let show = CustomMenuItem::new("show".to_string(), "Show App");let tray_menu = SystemTrayMenu::new().add_item(show).add_native_item(SystemTrayMenuItem::Separator).add_item(quit);let system_tray = SystemTray::new().with_menu(tray_menu);tauri::Builder::default().system_tray(system_tray).on_system_tray_event(|app, event| match event {SystemTrayEvent::MenuItemClick { id, .. } => {match id.as_str() {"quit" => {app.exit(0);}"show" => {if let Some(window) = app.get_window("main") {window.show().unwrap();}}_ => {}}}_ => {}}).run(tauri::generate_context!()).expect("error while running tauri application");
}
全局快捷键
Electron 方式
// main.js
const { globalShortcut } = require('electron')app.whenReady().then(() => {globalShortcut.register('CommandOrControl+X', () => {console.log('Shortcut triggered')})
})app.on('will-quit', () => {globalShortcut.unregisterAll()
})
Tauri 方式
// main.rs
use tauri::GlobalShortcutManager;fn main() {tauri::Builder::default().setup(|app| {let mut shortcut = app.global_shortcut_manager();shortcut.register("CommandOrControl+X", || {println!("Shortcut triggered");}).unwrap();Ok(())}).run(tauri::generate_context!()).expect("error while running tauri application");
}
文件拖放
Electron 方式
// renderer.js
document.addEventListener('drop', (e) => {e.preventDefault()e.stopPropagation()for (const f of e.dataTransfer.files) {console.log('File path:', f.path)}
})document.addEventListener('dragover', (e) => {e.preventDefault()e.stopPropagation()
})
Tauri 方式
// main.rs
#[tauri::command]
async fn handle_drop(window: Window,paths: Vec<String>
) -> Result<(), String> {for path in paths {println!("Dropped file: {}", path);}Ok(())
}
// App.tsx
import { listen } from '@tauri-apps/api/event'// 监听文件拖放
listen('tauri://file-drop', (event: any) => {const paths = event.payload as string[]console.log('Dropped files:', paths)
})
原生菜单
Electron 方式
// main.js
const { Menu } = require('electron')const template = [{label: 'File',submenu: [{ label: 'New', click: () => { /* ... */ } },{ type: 'separator' },{ role: 'quit' }]}
]const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
Tauri 方式
// main.rs
use tauri::{Menu, MenuItem, Submenu};fn main() {let file_menu = Submenu::new("File",Menu::new().add_item(CustomMenuItem::new("new", "New")).add_native_item(MenuItem::Separator).add_item(CustomMenuItem::new("quit", "Quit")));let menu = Menu::new().add_submenu(file_menu);tauri::Builder::default().menu(menu).on_menu_event(|event| {match event.menu_item_id() {"new" => {// 处理新建操作}"quit" => {event.window().app_handle().exit(0);}_ => {}}}).run(tauri::generate_context!()).expect("error while running tauri application");
}
实战案例:多窗口文件管理器
让我们通过一个实际的案例来综合运用这些功能:
// main.rs
use std::fs;
use tauri::{Window, WindowBuilder, WindowUrl};#[derive(serde::Serialize)]
struct FileItem {name: String,path: String,is_dir: bool,
}#[tauri::command]
async fn list_files(path: String) -> Result<Vec<FileItem>, String> {let entries = fs::read_dir(path).map_err(|e| e.to_string())?;let mut files = Vec::new();for entry in entries {let entry = entry.map_err(|e| e.to_string())?;let metadata = entry.metadata().map_err(|e| e.to_string())?;files.push(FileItem {name: entry.file_name().to_string_lossy().into_owned(),path: entry.path().to_string_lossy().into_owned(),is_dir: metadata.is_dir(),});}Ok(files)
}#[tauri::command]
async fn open_folder(app_handle: tauri::AppHandle,path: String
) -> Result<(), String> {WindowBuilder::new(&app_handle,path.clone(),WindowUrl::App("index.html".into())).title(format!("Folder: {}", path)).inner_size(800.0, 600.0).build().map_err(|e| e.to_string())?;Ok(())
}fn main() {tauri::Builder::default().invoke_handler(tauri::generate_handler![list_files,open_folder]).run(tauri::generate_context!()).expect("error while running tauri application");
}
// App.tsx
import { useState, useEffect } from 'react'
import { invoke } from '@tauri-apps/api/tauri'
import { WebviewWindow } from '@tauri-apps/api/window'interface FileItem {name: stringpath: stringis_dir: boolean
}function App() {const [files, setFiles] = useState<FileItem[]>([])const [currentPath, setCurrentPath] = useState('/')useEffect(() => {loadFiles(currentPath)}, [currentPath])const loadFiles = async (path: string) => {try {const items = await invoke<FileItem[]>('list_files', { path })setFiles(items)} catch (error) {console.error('Failed to load files:', error)}}const handleFileClick = async (file: FileItem) => {if (file.is_dir) {try {await invoke('open_folder', { path: file.path })} catch (error) {console.error('Failed to open folder:', error)}}}return (<div className="container"><h2>Current Path: {currentPath}</h2><div className="file-list">{files.map((file) => (<divkey={file.path}className={`file-item ${file.is_dir ? 'directory' : 'file'}`}onClick={() => handleFileClick(file)}><span>{file.name}</span></div>))}</div></div>)
}export default App
/* styles.css */
.container {padding: 20px;
}.file-list {display: grid;grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));gap: 10px;margin-top: 20px;
}.file-item {padding: 10px;border: 1px solid #ddd;border-radius: 4px;cursor: pointer;transition: background-color 0.2s;
}.file-item:hover {background-color: #f5f5f5;
}.directory {background-color: #e3f2fd;
}.file {background-color: #fff;
}
性能优化建议
窗口创建优化
- 延迟加载非必要窗口
- 使用窗口预加载
- 合理设置窗口属性
系统资源管理
- 及时释放不需要的窗口
- 使用事件解绑
- 避免内存泄漏
通信优化
- 批量处理消息
- 使用防抖和节流
- 避免频繁的跨进程通信
安全注意事项
窗口安全
- 限制窗口创建数量
- 验证加载的 URL
- 控制窗口权限
系统集成安全
- 限制文件系统访问
- 验证拖放文件
- 控制系统 API 访问
通信安全
- 验证消息来源
- 过滤敏感信息
- 使用安全的通信方式
小结
Tauri 2.0 的窗口管理特点:
- 更轻量的实现
- 更安全的权限控制
- 更灵活的定制能力
系统集成优势:
- 原生性能
- 更小的内存占用
- 更好的系统集成
开发建议:
- 合理使用窗口
- 注意性能优化
- 关注安全问题
下一篇文章,我们将深入探讨 Tauri 2.0 的 IPC 通信重构,帮助你更好地理解和使用这个核心功能。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍