Electron 开发者的 Tauri 2.0 实战指南:窗口管理与系统集成

作为 Electron 开发者,我们已经习惯了使用 BrowserWindow 来管理窗口,以及通过各种内置模块来实现系统集成。在 Tauri 2.0 中,这些功能虽然概念类似,但实现方式有所不同。本文将帮助你快速掌握 Tauri 的窗口管理和系统集成功能。

窗口管理

基础窗口操作

Electron 方式

javascript 复制代码
// 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 方式

rust 复制代码
// 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(())
}

// 前端调用
typescript 复制代码
// 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 方式

javascript 复制代码
// 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 方式

rust 复制代码
// 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(())
}
typescript 复制代码
// 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 方式

javascript 复制代码
// 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 方式

rust 复制代码
// 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(())
}
typescript 复制代码
// 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 方式

javascript 复制代码
// 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 方式

rust 复制代码
// 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 方式

javascript 复制代码
// main.js
const { globalShortcut } = require('electron')

app.whenReady().then(() => {
  globalShortcut.register('CommandOrControl+X', () => {
    console.log('Shortcut triggered')
  })
})

app.on('will-quit', () => {
  globalShortcut.unregisterAll()
})

Tauri 方式

rust 复制代码
// 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 方式

javascript 复制代码
// 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 方式

rust 复制代码
// main.rs
#[tauri::command]
async fn handle_drop(
    window: Window,
    paths: Vec<String>
) -> Result<(), String> {
    for path in paths {
        println!("Dropped file: {}", path);
    }
    Ok(())
}
typescript 复制代码
// 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 方式

javascript 复制代码
// 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 方式

rust 复制代码
// 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");
}

实战案例:多窗口文件管理器

让我们通过一个实际的案例来综合运用这些功能:

rust 复制代码
// 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");
}
typescript 复制代码
// 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
css 复制代码
/* 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;
}

性能优化建议

  1. 窗口创建优化

    • 延迟加载非必要窗口
    • 使用窗口预加载
    • 合理设置窗口属性
  2. 系统资源管理

    • 及时释放不需要的窗口
    • 使用事件解绑
    • 避免内存泄漏
  3. 通信优化

    • 批量处理消息
    • 使用防抖和节流
    • 避免频繁的跨进程通信

安全注意事项

  1. 窗口安全

    • 限制窗口创建数量
    • 验证加载的 URL
    • 控制窗口权限
  2. 系统集成安全

    • 限制文件系统访问
    • 验证拖放文件
    • 控制系统 API 访问
  3. 通信安全

    • 验证消息来源
    • 过滤敏感信息
    • 使用安全的通信方式

小结

  1. Tauri 2.0 的窗口管理特点:

    • 更轻量的实现
    • 更安全的权限控制
    • 更灵活的定制能力
  2. 系统集成优势:

    • 原生性能
    • 更小的内存占用
    • 更好的系统集成
  3. 开发建议:

    • 合理使用窗口
    • 注意性能优化
    • 关注安全问题

下一篇文章,我们将深入探讨 Tauri 2.0 的 IPC 通信重构,帮助你更好地理解和使用这个核心功能。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍

相关推荐
初遇你时动了情4 小时前
react Hooks 父组件调用子组件函数、获取子组件属性
前端·javascript·react.js
GISer_Jing4 小时前
MERN全栈脚手架(MongoDB、Express、React、Node)与Yeoman详解
mongodb·react.js·express
小满zs6 小时前
React第二十二章(useDebugValue)
前端·react.js
疯狂的沙粒15 小时前
对React的高阶组件的理解?应用场景?
前端·javascript·react.js·前端框架
星云code15 小时前
【HM-React】08. Layout模块
javascript·react.js·ecmascript
lryh_15 小时前
react 中使用ant 的 Table时警告:没有设置key
前端·react.js·前端框架
GISer_Jing1 天前
React中Element&Fiber对象、WorkInProgress双缓存、Reconcile&Render&Commit、第一次挂载过程详解
javascript·react.js·前端框架
星云code1 天前
【HM-React】07. 登录模块
javascript·react.js·ecmascript
疯狂的沙粒2 天前
如何更轻松的对React refs 的理解?都有哪些应用场景?
前端·react.js·前端框架