作为 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 = null
app.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: string
path: string
is_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) => (
<div
key={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 通信重构,帮助你更好地理解和使用这个核心功能。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。