Tauri v2 实战代码示例
执行摘要
Tauri v2 是一个基于 Rust 构建的跨平台桌面应用框架,它将 Web 前端技术与 Rust 后端相结合,提供了体积小、速度快、安全性高的应用开发解决方案。本文档收集并整理了 Tauri v2 的完整实战代码示例,涵盖项目初始化配置、Rust 命令定义、前端交互、事件系统、插件开发、窗口管理、文件系统操作以及 HTTP 请求处理等核心使用场景。所有示例均来源于官方文档、GitHub 仓库及经过验证的开发者教程,确保代码的正确性和可运行性。
一、项目初始化与配置
1.1 项目创建
Tauri v2 项目的创建可以通过多种包管理器完成。创建过程中会提示选择前端框架(如 Vue、React、Svelte 或纯 HTML)以及 TypeScript 支持选项。创建完成后,项目会包含前端源码目录和 src-tauri Rust 后端目录两个主要部分[1]。
使用 pnpm 创建项目的命令如下:
pnpm create tauri-app
其他包管理器的创建命令包括 npm create tauri-app@latest、yarn create tauri-app、cargo create-tauri-app 等[2]。
1.2 tauri.conf.json 基础配置
tauri.conf.json 是 Tauri 应用程序的核心配置文件,位于 src-tauri 目录中。以下是一个完整的基础配置示例:
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"devtools": true
},
"package": {
"productName": "My Tauri App",
"version": "1.0.0"
},
"tauri": {
"windows": [
{
"title": "My Tauri Application",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": true,
"center": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.myapp.example",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
该配置文件定义了构建命令、开发服务器路径、窗口属性以及打包设置。其中 devtools 选项在开发阶段应设置为 true 以便于调试[3]。
1.3 Cargo.toml 依赖配置
Rust 后端的依赖管理通过 src-tauri/Cargo.toml 文件完成。以下是集成常用插件的依赖配置:
[package]
name = "my-tauri-app"
version = "1.0.0"
description = "A Tauri Application"
authors = ["Developer"]
edition = "2021"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-fs = "2"
tauri-plugin-http = "2"
tauri-plugin-shell = "2"
tauri-plugin-os = "2"
tauri-plugin-process = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
Tauri v2 采用插件化架构,文件系统、HTTP 请求、Shell 命令等功能都需要通过相应的插件来实现[4]。
1.4 平台特定配置
Tauri 支持平台特定的配置文件,包括 tauri.linux.conf.json、tauri.windows.conf.json 和 tauri.macos.conf.json。这些文件会与主配置文件合并,允许开发者为不同平台定制配置[3]。
1.5 权限与能力系统
Tauri v2 引入了基于能力的权限管理系统。权限配置文件位于 src-tauri/capabilities/ 目录中,每个窗口可以拥有独立的能力配置。以下是一个完整的能力配置示例:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Main window capabilities",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-close",
"core:window:allow-set-title",
"core:window:allow-start-dragging",
"fs:default",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"http:default",
{
"identifier": "http:allow-fetch",
"allow": [
{ "url": "https://*.api.example.com" },
{ "url": "https://jsonplaceholder.typicode.com" }
]
},
"shell:allow-open",
"os:default"
]
}
权限系统支持精细的作用域控制,可以限制对特定路径或域名的访问。变量如 $HOME、$APPDATA、$APPCACHE 等可用于路径配置[5]。
二、Rust 后端命令定义
2.1 基本命令定义
Tauri 的命令系统允许前端 JavaScript 调用 Rust 函数。命令通过 #[tauri::command] 属性宏定义,并使用 #[tauri::command] 注解的函数必须放在 src-tauri/src/lib.rs 文件中(非 main.rs)[6]。
以下是最基本的命令定义示例:
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JavaScript!");
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2.2 命令模块化
当命令较多时,建议将命令分离到单独的文件或模块中:
// src-tauri/src/commands.rs
#[tauri::command]
pub fn command_a() -> String {
"Command A result".into()
}
#[tauri::command]
pub fn command_b(value: String) -> Result<String, String> {
if value.is_empty() {
Err("Value cannot be empty".into())
} else {
Ok(format!("Received: {}", value))
}
}
// src-tauri/src/lib.rs
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::command_a,
commands::command_b
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2.3 带参数的命令
命令可以接受参数,参数类型支持 Rust 的基本类型和实现了 serde::Deserialize 的自定义类型:
#[tauri::command]
fn greet(name: String, age: u32) -> String {
format!("Hello, {}! You are {} years old.", name, age)
}
// 使用 snake_case 重命名规则
#[tauri::command(rename_all = "snake_case")]
fn process_data(my_value: String, another_field: bool) -> String {
format!("Value: {}, Field: {}", my_value, another_field)
}
2.4 返回数据与错误处理
命令可以返回结果,也可以返回错误。以下是完整的错误处理模式:
use serde::Serialize;
// 基本错误处理
#[tauri::command]
fn login(user: String, password: String) -> Result<String, String> {
if user == "admin" && password == "admin123" {
Ok("login_success".to_string())
} else {
Err("Invalid credentials".to_string())
}
}
// 使用 thiserror 库定义自定义错误类型
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(String),
}
impl Serialize for AppError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[tauri::command]
fn read_file_content(path: String) -> Result<String, AppError> {
std::fs::read_to_string(&path).map_err(AppError::from)
}
2.5 异步命令
异步命令使用 async fn 语法,需要注意借用规则:
use tokio::time::{sleep, Duration};
// 方式一:使用 String 替代 &str 避免借用问题
#[tauri::command]
async fn async_operation(value: String) -> String {
sleep(Duration::from_secs(1)).await;
format!("Processed: {}", value)
}
// 方式二:返回 Result
#[tauri::command]
async fn async_with_result(url: String) -> Result<String, String> {
// 模拟异步操作
sleep(Duration::from_millis(100)).await;
Ok(format!("Fetched from: {}", url))
}
2.6 访问状态管理
Tauri 提供状态管理功能,可以在应用生命周期内共享数据:
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
config: Mutex<AppConfig>,
}
#[derive(Debug, Clone, serde::Serialize)]
struct AppConfig {
theme: String,
language: String,
}
#[tauri::command]
fn increment_counter(state: tauri::State<'_, AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
#[tauri::command]
fn get_config(state: tauri::State<'_, AppState>) -> AppConfig {
state.config.lock().unwrap().clone()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let app_state = AppState {
counter: Mutex::new(0),
config: Mutex::new(AppConfig {
theme: "dark".to_string(),
language: "en".to_string(),
}),
};
tauri::Builder::default()
.manage(app_state)
.invoke_handler(tauri::generate_handler![
increment_counter,
get_config
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2.7 访问 WebviewWindow 和 AppHandle
命令可以接收 WebviewWindow 或 AppHandle 参数以执行窗口操作或访问应用级功能:
#[tauri::command]
async fn open_settings_window(app_handle: tauri::AppHandle) -> Result<(), String> {
use tauri::{WebviewUrl, WebviewWindowBuilder};
WebviewWindowBuilder::new(
&app_handle,
"settings",
WebviewUrl::App("settings.html".into()),
)
.title("Settings")
.inner_size(600.0, 400.0)
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn get_window_label(window: tauri::Window) -> String {
window.label().to_string()
}
#[tauri::command]
async fn send_notification(app_handle: tauri::AppHandle, title: String, body: String) {
use tauri::Manager;
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.emit("notification", serde_json::json!({
"title": title,
"body": body
}));
}
}
2.8 使用 Channel 进行流式数据传输
Channel 允许从 Rust 端向前端发送流式数据:
use tauri::{AppHandle, ipc::Channel};
use serde::Serialize;
#[derive(Clone, Serialize)]
#[serde(tag = "event", content = "data")]
enum DownloadEvent {
Started { url: String, size: u64 },
Progress { bytes_downloaded: u64 },
Finished { success: bool },
}
#[tauri::command]
async fn download_file(
app: AppHandle,
url: String,
on_progress: Channel<DownloadEvent>,
) -> Result<(), String> {
let total_size: u64 = 1000;
on_progress
.send(DownloadEvent::Started {
url: url.clone(),
size: total_size,
})
.map_err(|e| e.to_string())?;
for i in (0..total_size).step_by(100) {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
on_progress
.send(DownloadEvent::Progress {
bytes_downloaded: i,
})
.map_err(|e| e.to_string())?;
}
on_progress
.send(DownloadEvent::Finished { success: true })
.map_err(|e| e.to_string())?;
Ok(())
}
三、前端调用 Rust 代码
3.1 使用 npm 包调用命令
Tauri 2.x 推荐使用 @tauri-apps/api 包中的 invoke 函数来调用 Rust 命令:
npm install @tauri-apps/api
基础调用方式如下:
import { invoke } from '@tauri-apps/api/core';
// 无参数命令
async function callBasicCommand() {
await invoke('my_custom_command');
}
// 带参数命令
async function callWithArgs() {
const result = await invoke('greet', { name: 'Alice', age: 30 });
console.log(result);
}
// 返回复杂对象
async function getData() {
const data = await invoke('get_config');
console.log(data.theme, data.language);
}
3.2 TypeScript 类型支持
可以为命令定义 TypeScript 类型以获得完整的类型提示:
interface CustomResponse {
message: string;
otherVal: number;
}
interface ErrorKind {
kind: 'io' | 'utf8';
message: string;
}
async function fetchData(): Promise<CustomResponse> {
try {
const response = await invoke<CustomResponse>('my_custom_command', {
number: 42,
});
return response;
} catch (error) {
const typedError = error as ErrorKind;
console.error(`Error (${typedError.kind}): ${typedError.message}`);
throw error;
}
}
3.3 错误处理
前端可以通过 Promise 的 catch 方法处理命令返回的错误:
async function login(username, password) {
try {
const result = await invoke('login', {
user: username,
password: password
});
console.log('Login result:', result);
return result;
} catch (error) {
console.error('Login failed:', error);
return null;
}
}
3.4 调用异步命令
异步命令的调用方式与普通命令相同,invoke 会自动等待异步操作完成:
async function performAsyncOperation() {
try {
const result = await invoke('async_operation', {
value: 'test data'
});
console.log('Async result:', result);
} catch (error) {
console.error('Async operation failed:', error);
}
}
3.5 使用 Global Tauri(可选)
如果不想使用 npm 包,可以在 tauri.conf.json 中启用全局 Tauri:
{
"tauri": {
"app": {
"withGlobalTauri": true
}
}
}
然后使用全局对象调用命令:
// 使用全局脚本
const invoke = window.__TAURI__.invoke;
await invoke('my_custom_command');
3.6 原始请求调用
对于需要发送原始字节数据的场景,Tauri 2 支持原始请求调用:
const data = new Uint8Array([1, 2, 3, 4, 5]);
await invoke('upload', data, {
headers: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/octet-stream'
}
});
四、事件系统
4.1 从 Rust 发送全局事件
Tauri 的事件系统允许 Rust 后端向所有前端监听器广播消息:
use tauri::{AppHandle, Emitter};
#[tauri::command]
fn download(app: AppHandle, url: String) {
// 发送初始事件
app.emit("download-started", &url).unwrap();
// 发送进度事件
for progress in [10, 30, 50, 70, 90, 100] {
app.emit("download-progress", progress).unwrap();
std::thread::sleep(std::time::Duration::from_millis(200));
}
// 发送完成事件
app.emit("download-finished", &url).unwrap();
}
4.2 发送事件到特定 Webview
使用 emit_to 方法可以将事件发送到特定窗口:
use tauri::{AppHandle, Emitter};
#[tauri::command]
fn login(app: AppHandle, user: String, password: String) {
let result = if user == "admin" && password == "admin" {
"success"
} else {
"failed"
};
// 只向 login 窗口发送结果
app.emit_to("login", "login-result", result).unwrap();
}
4.3 使用事件过滤器
emit_filter 允许根据条件选择目标窗口:
use tauri::{AppHandle, Emitter, EventTarget};
#[tauri::command]
fn broadcast_to_main(app: AppHandle, message: String) {
app.emit_filter("broadcast", message, |target| {
match target {
EventTarget::WebviewWindow { label } => {
label == "main" || label == "dashboard"
},
_ => false,
}
}).unwrap();
}
4.4 前端监听全局事件
JavaScript 端使用 listen 函数监听事件:
import { listen } from '@tauri-apps/api/event';
interface DownloadStarted {
url: string;
downloadId: number;
contentLength: number;
}
// 监听下载开始事件
const unlisten = await listen<DownloadStarted>('download-started', (event) => {
console.log(`开始下载: ${event.payload.url}`);
console.log(`文件大小: ${event.payload.contentLength} bytes`);
});
// 监听进度更新
await listen<number>('download-progress', (event) => {
updateProgressBar(event.payload);
});
// 监听下载完成
await listen<string>('download-finished', (event) => {
console.log(`下载完成: ${event.payload}`);
showNotification('Download complete!');
});
// 停止监听
unlisten();
4.5 前端监听特定窗口事件
获取特定窗口实例并监听其事件:
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
const appWindow = getCurrentWebviewWindow();
// 监听来自该窗口的事件
appWindow.listen<string>('login-result', (event) => {
if (event.payload === 'success') {
navigateToDashboard();
} else {
showError('Login failed');
}
});
4.6 监听一次事件
使用 once 函数监听仅触发一次的事件:
import { once } from '@tauri-apps/api/event';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
// 全局事件监听一次
once('app-ready', (event) => {
console.log('App is ready!');
});
// 特定窗口事件监听一次
const appWindow = getCurrentWebviewWindow();
appWindow.once('initial-data-loaded', () => {
renderApp();
});
4.7 从 Rust 端监听事件
在 Rust 端也可以监听前端发送的事件:
use tauri::{Listener, Manager};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 监听全局事件
app.listen("my-event", |event| {
println!("Received global event: {:?}", event.payload());
});
// 监听特定窗口事件
if let Some(window) = app.get_webview_window("main") {
window.listen("frontend-event", |event| {
println!("Frontend event: {:?}", event.payload());
});
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
4.8 停止监听
可以通过 unlisten 函数停止事件监听:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen('status-update', (event) => {
console.log('Status:', event.payload);
});
// 稍后停止监听
setTimeout(() => {
unlisten();
}, 5000);
五、插件开发
5.1 插件项目结构
Tauri 插件拥有独立的目录结构,包含 Rust 实现和 JavaScript API:
tauri-plugin-[name]/
├── src/
│ ├── lib.rs # 插件入口和初始化
│ ├── commands.rs # 命令定义
│ ├── desktop.rs # 桌面平台实现
│ ├── mobile.rs # 移动平台实现
│ ├── error.rs # 错误类型定义
│ └── models.rs # 共享数据结构
├── permissions/ # 权限定义文件
├── guest-js/ # JavaScript API 源码
├── dist-js/ # 编译后的 JavaScript
├── android/ # Android 原生代码
├── ios/ # iOS 原生代码
├── Cargo.toml
└── package.json
5.2 插件初始化
插件通过 Builder 模式初始化:
// src/lib.rs
use tauri::plugin::{Builder, Runtime, TauriPlugin};
use serde::Deserialize;
#[derive(Deserialize)]
struct PluginConfig {
timeout: usize,
}
pub fn init<R: Runtime>() -> TauriPlugin<R, PluginConfig> {
Builder::<R, PluginConfig>::new("my-plugin")
.setup(|app, api| {
let timeout = api.config().timeout;
println!("Plugin initialized with timeout: {}", timeout);
Ok(())
})
.build()
}
5.3 生命周期钩子
插件支持多种生命周期钩子:
use tauri::{Manager, plugin::Builder, RunEvent};
Builder::new("my-plugin")
// 插件初始化
.setup(|app, _api| {
app.manage(MyState::default());
Ok(())
})
// WebView 导航拦截
.on_navigation(|window, url| {
println!("Window {} navigating to {}", window.label(), url);
url.scheme() != "blocked" // 返回 false 阻止导航
})
// WebView 创建完成
.on_webview_ready(|window| {
window.listen("content-loaded", |_| {
println!("Content loaded in window");
});
})
// 事件循环事件
.on_event(|app, event| {
match event {
RunEvent::ExitRequested { api, .. } => {
// 阻止退出
api.prevent_exit();
}
RunEvent::Exit => {
// 清理资源
println!("App is exiting");
}
_ => {}
}
})
// 插件销毁
.on_drop(|app| {
println!("Plugin destroyed");
})
5.4 定义命令
在插件中定义可从调用的命令:
// src/commands.rs
use tauri::{command, AppHandle, Runtime, Window, ipc::Channel};
#[command]
pub async fn start_server<R: Runtime>(
app: AppHandle<R>,
port: u16,
on_event: Channel,
) -> Result<(), String> {
// 实现服务器启动逻辑
on_event.send("server-started").map_err(|e| e.to_string())?;
// 发送定期心跳
for i in 0..10 {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
on_event.send(format!("heartbeat-{}", i)).map_err(|e| e.to_string())?;
}
Ok(())
}
5.5 注册命令和 JavaScript API
// src/lib.rs
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::<R>::new("my-plugin")
.invoke_handler(tauri::generate_handler![
commands::start_server
])
.build()
}
// guest-js/index.ts
import { invoke, Channel } from '@tauri-apps/api/core';
export async function startServer(
port: number,
onEventHandler: (event: string) => void
): Promise<void> {
const onEvent = new Channel<string>();
onEvent.onmessage = onEventHandler;
await invoke('plugin:my-plugin|start_server', { port, onEvent });
}
5.6 权限定义
插件通过 TOML 文件定义权限:
# permissions/start-server.toml
"$schema" = "../gen/schemas/schema.json"
[[permission]]
identifier = "allow-start-server"
description = "Enables the start_server command"
commands.allow = ["start_server"]
[[permission]]
identifier = "deny-start-server"
description = "Denies the start_server command"
commands.deny = ["start_server"]
作用域权限定义:
# permissions/spawn-process.toml
[[permission]]
identifier = "allow-spawn"
description = "Allows spawning processes"
[[permission.scope.allow]]
binary = "node"
args = ["--version"]
[[permission.scope.deny]]
binary = "rm"
5.7 在应用中使用插件
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(my_plugin::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
5.8 自动生成权限
通过构建脚本自动生成权限:
// build.rs
const COMMANDS: &[&str] = &["upload", "download"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}
这会自动生成 allow-upload、deny-upload、allow-download、deny-download 等权限。
六、窗口管理
6.1 窗口配置
通过 tauri.conf.json 配置窗口属性:
{
"tauri": {
"windows": [
{
"label": "main",
"title": "My Application",
"width": 1024,
"height": 768,
"minWidth": 640,
"minHeight": 480,
"resizable": true,
"fullscreen": false,
"decorations": true,
"transparent": false,
"center": true,
"x": 100,
"y": 100,
"visible": true,
"focus": true
}
]
}
}
6.2 运行时创建窗口
使用 WebviewWindowBuilder 在运行时创建新窗口:
use tauri::{WebviewUrl, WebviewWindowBuilder, Manager};
#[tauri::command]
async fn open_settings(app: tauri::AppHandle) -> Result<(), String> {
WebviewWindowBuilder::new(
&app,
"settings", // 唯一标签
WebviewUrl::App("settings.html".into()),
)
.title("Settings")
.inner_size(600.0, 400.0)
.resizable(true)
.center()
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
6.3 自定义标题栏
实现自定义标题栏需要禁用原生窗口装饰:
{
"tauri": {
"windows": [
{
"decorations": false,
"transparent": true
}
]
}
}
HTML 结构:
<div class="titlebar" data-tauri-drag-region>
<div class="title">My App</div>
<div class="controls">
<button id="btn-minimize">
<svg><!-- 最小化图标 --></svg>
</button>
<button id="btn-maximize">
<svg><!-- 最大化图标 --></svg>
</button>
<button id="btn-close">
<svg><!-- 关闭图标 --></svg>
</button>
</div>
</div>
CSS 样式:
.titlebar {
height: 32px;
background: #2d3748;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.titlebar button {
width: 46px;
height: 32px;
border: none;
background: transparent;
color: white;
cursor: pointer;
}
.titlebar button:hover {
background: rgba(255, 255, 255, 0.1);
}
[data-tauri-drag-region] {
flex: 1;
height: 100%;
cursor: move;
}
JavaScript 控制逻辑:
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
document.getElementById('btn-minimize')?.addEventListener('click', () => {
appWindow.minimize();
});
document.getElementById('btn-maximize')?.addEventListener('click', async () => {
const isMaximized = await appWindow.isMaximized();
if (isMaximized) {
appWindow.unmaximize();
} else {
appWindow.maximize();
}
});
document.getElementById('btn-close')?.addEventListener('click', () => {
appWindow.close();
});
6.4 窗口操作
完整的窗口操作 API:
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
// 最小化
await appWindow.minimize();
// 最大化
await appWindow.maximize();
// 恢复窗口
await appWindow.unmaximize();
// 关闭窗口
await appWindow.close();
// 设置窗口标题
await appWindow.setTitle('New Title');
// 获取窗口是否最大化
const isMax = await appWindow.isMaximized();
// 获取窗口是否最小化
const isMin = await appWindow.isMinimized();
// 设置焦点
await appWindow.setFocus();
// 设置全屏
await appWindow.setFullscreen(true);
// 检查是否全屏
const isFullscreen = await appWindow.isFullscreen();
// 设置窗口始终在最前
await appWindow.setAlwaysOnTop(true);
// 开始拖动窗口
await appWindow.startDragging();
6.5 多窗口管理
创建和管理多个窗口:
use tauri::{AppHandle, Manager, Emitter};
#[tauri::command]
async fn open_window(app: AppHandle, label: String, url: String) -> Result<(), String> {
if app.get_webview_window(&label).is_some() {
// 窗口已存在,聚焦
if let Some(window) = app.get_webview_window(&label) {
window.set_focus().map_err(|e| e.to_string())?;
}
return Ok(());
}
// 创建新窗口
tauri::WebviewWindowBuilder::new(&app, &label, url.parse().unwrap())
.title(&label)
.inner_size(800.0, 600.0)
.center()
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn close_window(app: AppHandle, label: String) -> Result<(), String> {
if let Some(window) = app.get_webview_window(&label) {
window.close().map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
async fn list_windows(app: AppHandle) -> Vec<String> {
app.webview_windows()
.keys()
.cloned()
.collect()
}
6.6 窗口事件监听
监听窗口状态变化:
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
// 监听窗口大小变化
appWindow.onResized(async ({ payload }) => {
const size = await appWindow.innerSize();
console.log(`Window resized to ${size.width}x${size.height}`);
});
// 监听窗口移动
appWindow.onMoved(async ({ payload }) => {
const position = await appWindow.outerPosition();
console.log(`Window moved to ${position.x}, ${position.y}`);
});
// 监听窗口关闭请求
appWindow.onCloseRequested(async (event) => {
const shouldClose = confirm('Are you sure you want to close?');
if (!shouldClose) {
event.preventClose();
}
});
// 监听窗口焦点变化
appWindow.onFocusChanged(({ payload: focused }) => {
console.log(`Window focus: ${focused}`);
});
6.7 macOS 透明标题栏
针对 macOS 平台的特殊配置:
use tauri::{TitleBarStyle, WebviewUrl, WebviewWindowBuilder};
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let win = WebviewWindowBuilder::new(
app,
"main",
WebviewUrl::default(),
)
.title("My App")
.inner_size(800.0, 600.0);
#[cfg(target_os = "macos")]
let win = win.title_bar_style(TitleBarStyle::Transparent);
win.build().unwrap();
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
七、文件系统操作
7.1 插件安装与初始化
Tauri 的文件系统功能通过 tauri-plugin-fs 提供:
cargo add tauri-plugin-fs
在 Rust 端初始化:
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
7.2 权限配置
在 capabilities 中配置文件系统权限:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "fs-capability",
"windows": ["main"],
"permissions": [
"fs:default",
{
"identifier": "fs:allow-read-text-file",
"allow": [
{ "path": "$APPDATA/*" },
{ "path": "$HOME/test.txt" }
]
},
{
"identifier": "fs:allow-write-text-file",
"allow": [
{ "path": "$APPDATA" },
{ "path": "$APPDATA/**" }
]
}
]
}
7.3 读取文本文件
import { readTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
// 使用基础目录读取
async function loadConfig() {
const content = await readTextFile('config.json', {
baseDir: BaseDirectory.AppConfig,
});
return JSON.parse(content);
}
// 使用路径 API 组合完整路径
import { homeDir, join } from '@tauri-apps/plugin-path';
async function readUserFile() {
const home = await homeDir();
const filePath = await join(home, 'documents', 'notes.txt');
const content = await readTextFile(filePath);
return content;
}
7.4 写入文本文件
import { writeTextFile, BaseDirectory, mkdir, exists } from '@tauri-apps/plugin-fs';
async function saveData(data) {
const dir = 'myapp';
const file = 'data.json';
// 确保目录存在
if (!(await exists(dir, { baseDir: BaseDirectory.AppData }))) {
await mkdir(dir, { baseDir: BaseDirectory.AppData, recursive: true });
}
// 写入文件
await writeTextFile(
`${dir}/${file}`,
JSON.stringify(data, null, 2),
{ baseDir: BaseDirectory.AppData }
);
}
7.5 二进制文件操作
import { readFile, writeFile, BaseDirectory } from '@tauri-apps/plugin-fs';
// 读取二进制文件(如图片)
async function loadImage() {
const bytes = await readFile('icon.png', {
baseDir: BaseDirectory.Resource,
});
return bytes;
}
// 写入二进制文件
async function saveImage(data) {
const bytes = new Uint8Array(data);
await writeFile('output.png', bytes, {
baseDir: BaseDirectory.Picture,
});
}
7.6 目录操作
import {
readDir,
mkdir,
remove,
exists,
BaseDirectory,
} from '@tauri-apps/plugin-fs';
// 读取目录
async function listDirectory() {
const entries = await readDir('projects', {
baseDir: BaseDirectory.Home,
});
for (const entry of entries) {
console.log(`${entry.isDirectory ? '[DIR]' : '[FILE]'} ${entry.name}`);
}
}
// 创建目录
async function createProject(name) {
const projectPath = `projects/${name}`;
if (!(await exists(projectPath, { baseDir: BaseDirectory.Home }))) {
await mkdir(projectPath, {
baseDir: BaseDirectory.Home,
recursive: true,
});
}
}
// 删除目录
async function deleteProject(name) {
const projectPath = `projects/${name}`;
if (await exists(projectPath, { baseDir: BaseDirectory.Home })) {
await remove(projectPath, {
baseDir: BaseDirectory.Home,
recursive: true,
});
}
}
7.7 文件操作
import {
copyFile,
rename,
truncate,
stat,
exists,
remove,
BaseDirectory,
} from '@tauri-apps/plugin-fs';
// 复制文件
async function backupFile() {
await copyFile('data.db', 'data.db.bak', {
fromPathBaseDir: BaseDirectory.AppData,
toPathBaseDir: BaseDirectory.AppData,
});
}
// 重命名文件
async function renameFile(oldName, newName) {
await rename(oldName, newName, {
fromPathBaseDir: BaseDirectory.AppData,
toPathBaseDir: BaseDirectory.AppData,
});
}
// 获取文件元数据
async function getFileInfo(filename) {
const metadata = await stat(filename, {
baseDir: BaseDirectory.AppData,
});
console.log('Size:', metadata.size);
console.log('Created:', metadata.created);
console.log('Modified:', metadata.modified);
console.log('Is Directory:', metadata.isDirectory);
}
// 截断文件
async function clearLogFile() {
await truncate('app.log', 0, {
baseDir: BaseDirectory.AppLog,
});
}
// 删除文件
async function deleteFile(filename) {
if (await exists(filename, { baseDir: BaseDirectory.AppData })) {
await remove(filename, {
baseDir: BaseDirectory.AppData,
});
}
}
7.8 监视文件变化
需要启用 watch 功能:
# Cargo.toml
[dependencies]
tauri-plugin-fs = { version = "2", features = ["watch"] }
import { watch, watchImmediate, BaseDirectory } from '@tauri-apps/plugin-fs';
// 防抖监视(适合文件编辑场景)
async function watchConfigFile() {
await watch(
'config.json',
(event) => {
console.log('Event type:', event.type);
console.log('Paths:', event.paths);
},
{
baseDir: BaseDirectory.AppConfig,
delayMs: 500,
}
);
}
// 即时监视(适合日志文件等实时更新场景)
async function watchLogFile() {
await watchImmediate(
'app.log',
(event) => {
console.log('Log file changed:', event);
},
{
baseDir: BaseDirectory.AppLog,
recursive: true,
}
);
}
7.9 使用 open 和 create 函数
import { open, create, BaseDirectory } from '@tauri-apps/plugin-fs';
// 以写模式打开文件
async function appendToFile() {
const file = await open('notes.txt', {
write: true,
append: true,
baseDir: BaseDirectory.Document,
});
await file.write(new TextEncoder().encode('New content\n'));
await file.close();
}
// 使用 create 创建新文件
async function createNewFile() {
const file = await create('newfile.txt', {
baseDir: BaseDirectory.Desktop,
});
await file.write(new TextEncoder().encode('Hello, World!'));
await file.close();
}
// 以只读模式打开并读取
async function readFileWithOpen() {
const file = await open('report.pdf', {
read: true,
baseDir: BaseDirectory.Document,
});
const stat = await file.stat();
const buffer = new Uint8Array(stat.size);
await file.read(buffer);
await file.close();
return buffer;
}
八、HTTP 请求处理
8.1 插件安装与初始化
HTTP 功能通过 tauri-plugin-http 提供:
cargo add tauri-plugin-http
Rust 端初始化:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
安装 JavaScript 包:
npm install @tauri-apps/plugin-http
8.2 权限配置
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "http-capability",
"windows": ["main"],
"permissions": [
"http:default",
{
"identifier": "http:allow-fetch",
"allow": [
{ "url": "https://api.example.com" },
{ "url": "https://*.github.com" },
{ "url": "https://jsonplaceholder.typicode.com" }
]
}
]
}
8.3 基础 GET 请求
import { fetch } from '@tauri-apps/plugin-http';
async function getUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'GET',
});
if (response.ok) {
const users = await response.json();
return users;
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
8.4 POST 请求
async function createUser(userData) {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
return await response.json();
}
8.5 带认证的请求
async function fetchWithAuth() {
const response = await fetch('https://api.example.com/protected', {
method: 'GET',
headers: {
'Authorization': 'Bearer your-token-here',
'Accept': 'application/json',
},
});
if (response.status === 401) {
// Token 过期,需要刷新
await refreshToken();
return fetchWithAuth();
}
return await response.json();
}
8.6 处理不同响应类型
async function handleVariousResponses() {
const response = await fetch('https://api.example.com/data', {
method: 'GET',
});
const contentType = response.headers.get('content-type');
if (contentType.includes('application/json')) {
return await response.json();
} else if (contentType.includes('text/')) {
return await response.text();
} else if (contentType.includes('application/octet-stream')) {
return await response.arrayBuffer();
} else {
// 处理其他类型
return await response.blob();
}
}
8.7 文件上传
async function uploadFile(file, uploadUrl) {
const formData = new FormData();
formData.append('file', file);
formData.append('filename', file.name);
const response = await fetch(uploadUrl, {
method: 'POST',
body: formData,
});
return await response.json();
}
8.8 Rust 端使用 reqwest
在 Rust 端可以直接使用 reqwest 库:
use tauri_plugin_http::reqwest;
#[tauri::command]
async fn fetch_api_data() -> Result<String, String> {
let response = reqwest::get("https://api.example.com/data")
.await
.map_err(|e| e.to_string())?;
let text = response.text()
.await
.map_err(|e| e.to_string())?;
Ok(text)
}
8.9 处理网络错误
async function robustFetch(url, options = {}) {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.warn(`Attempt ${i + 1} failed:`, error);
if (i < maxRetries - 1) {
await new Promise(resolve =>
setTimeout(resolve, 1000 * (i + 1))
);
}
}
}
throw new Error(`All attempts failed: ${lastError.message}`);
}
8.10 启用不安全请求头
某些请求头(如 Authorization)在默认情况下被禁止。如需启用,需要在 Cargo.toml 中启用 unsafe-headers 特性:
[dependencies]
tauri-plugin-http = { version = "2", features = ["unsafe-headers"] }
九、总结与最佳实践
9.1 项目结构建议
一个成熟的 Tauri v2 项目应该采用清晰的模块化结构。前端代码组织应遵循所选框架的最佳实践,Rust 后端建议按功能模块划分,将命令、事件处理、插件初始化等分离到独立文件中。
9.2 安全考虑
Tauri v2 的权限系统是保障应用安全的关键。开发者应遵循最小权限原则,仅授予应用实际需要的权限。路径访问应限制在必要的目录范围内,网络请求应明确限定允许的域名列表。所有用户输入都应在 Rust 后端进行验证和清理。
9.3 性能优化建议
异步命令应优先使用 async fn 语法以避免阻塞事件循环。大量数据传输推荐使用 Channel 而非直接返回值。文件操作应考虑批量处理以减少 I/O 次数。窗口创建和销毁应谨慎管理,避免频繁创建销毁导致的资源消耗。
9.4 跨平台兼容性
虽然 Tauri 抽象了大部分平台差异,但某些功能(如透明标题栏、文件路径格式)仍需要平台特定处理。建议使用条件编译处理平台相关代码,并通过检测运行时环境提供合适的回退方案。