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

作为 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;
}

性能优化建议

  1. 窗口创建优化

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

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

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

安全注意事项

  1. 窗口安全

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

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

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

小结

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

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

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

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

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

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

### Tauri 2.0 配置方法和设置指南 #### 创建 Tauri 2.0 项目 由于官方尚未完全完善针对 Tauri 2.0 的文档支持,创建项目的流程可能会遇到一定挑战。可以通过 Issue 和社区讨论找到解决方案[^2]。以下是基于现有信息总结的配置步骤: 1. **初始化项目** 使用 `npm` 或 `yarn` 初始化一个新的前端项目,并确保安装了必要的工具链(Node.js、Rust 等)。可以参考以下命令完成初始环境搭建: ```bash npm init tauri@next my-tauri-app cd my-tauri-app ``` 这里通过指定 `@next` 版本来获取最新的预览版本。 2. **更新到 Tauri 2.0** 如果默认创建的是旧版本,则需要手动调整依赖项至最新版本。编辑 `package.json` 文件中的 Tauri 相关字段,将其指向 `2.x` 分支或标签。例如: ```json { "dependencies": { "@tauri-apps/api": "^2.0", "tauri": "^2.0" } } ``` 3. **验证安装** 完成修改后运行以下命令以确认安装无误: ```bash npm install npx tauri info ``` 此命令会显示当前环境的状态以及所使用的 Tauri 版本号。 --- #### 基础配置选项 Tauri 提供了一个灵活的配置机制,允许开发者自定义应用程序的行为。核心配置文件位于项目根目录下的 `src-tauri/tauri.conf.json` 中。以下是几个重要的配置参数及其作用说明: 1. **窗口管理** 设置窗口大小、位置以及其他外观属性: ```json { "build": { "distDir": "../dist", "devPath": "http://localhost:8080" }, "windows": [ { "title": "My App", "width": 800, "height": 600, "resizable": true } ] } ``` 2. **打包分发** 调整目标平台和支持的功能模块: ```json { "bundle": { "active": true, "identifier": "com.example.myapp", "targets": ["debian", "macos", "windows"] } } ``` 3. **安全性增强** 利用内置的安全策略保护数据传输过程免受攻击风险: ```json { "allowlist": { "all": false, "shell": { "open": true, "execute": false } } } ``` 更多高级配置详情可查阅官方文档或相关教程[^3]。 --- #### 后端开发集成 如果计划扩展应用功能并引入 Rust 编写的业务逻辑,则需熟悉前后端交互方式。通常借助 API 接口实现调用关系,具体如下所示: 1. **注册命令** 在 `src-tauri/src/main.rs` 文件中声明可用的操作入口点: ```rust use tauri::Manager; #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {}!", name) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` 2. **从前端触发** JavaScript 层面可通过 invoke 方法访问上述定义的服务接口: ```javascript window.tauri.invoke('greet', { name: 'World' }).then((response) => { console.log(response); // 输出 Hello, World! }); ``` 此模式充分利用了 Rust 的高性能特点同时保持良好的用户体验一致性[^4]。 --- ### 总结 以上介绍了有关于 Tauri 2.0 的基本配置流程及相关注意事项。尽管目前存在一定的学习曲线,但凭借其卓越的技术优势相信未来会有更加完善的生态体系形成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值