| name | tauri-commands |
| description | Tauri Command 高级开发技能,覆盖异步 Command、状态注入、流式传输、事件通知等高级模式。
触发场景:
- 需要开发复杂的 Tauri Command
- 需要 Command 中访问 AppHandle/Window
- 需要实现进度回报/流式数据
- 需要 Command 之间共享逻辑
触发词:Command、tauri::command、invoke、高级Command、async command、进度、stream
|
Tauri Command 高级开发
模块化组织(三层架构)
按功能拆分 Command
src-tauri/src/
├── commands/ → Command 模块(薄 IPC 包装)
│ ├── mod.rs → 导出所有 Command 模块
│ ├── config.rs → 配置管理 Commands
│ ├── system.rs → 系统信息 Commands
│ └── user.rs → 用户管理 Commands (示例)
├── services/ → Service 模块(业务逻辑)
│ ├── mod.rs
│ ├── config.rs → 配置业务逻辑
│ └── user.rs → 用户业务逻辑
├── database/
│ ├── mod.rs → Database 结构体 + CRUD 方法
│ └── schema.rs → 版本化 Schema 迁移
├── models/
│ └── mod.rs → 所有数据模型
└── lib.rs → 统一注册 Commands
commands/mod.rs 模式
pub mod config;
pub mod system;
pub mod user;
实现 Command 模块(薄包装模式)
Command 只做:接收参数 → 调用 Service → 转换错误。不包含业务逻辑。
use crate::services::config::ConfigService;
use crate::state::AppState;
use tauri::State;
#[tauri::command]
pub fn get_all_config(state: State<'_, AppState>) -> Result<Vec<AppConfig>, String> {
ConfigService::get_all(&state.db).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn set_config(
state: State<'_, AppState>,
key: String,
value: String,
) -> Result<(), String> {
ConfigService::set(&state.db, &key, &value).map_err(|e| e.to_string())
}
use tauri::Manager;
use crate::models::SystemInfo;
#[tauri::command]
pub fn get_system_info(app: tauri::AppHandle) -> Result<SystemInfo, String> {
let data_dir = app
.path()
.app_data_dir()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| "unknown".into());
Ok(SystemInfo {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
app_version: app.package_info().version.to_string(),
data_dir,
})
}
#[tauri::command]
pub fn greet(name: &str) -> Result<String, String> {
if name.is_empty() {
return Err("名称不能为空".into());
}
Ok(format!("Hello, {}! 来自 Rust 的问候!", name))
}
lib.rs 统一注册
mod commands;
mod database;
mod error;
mod models;
mod services;
mod state;
use database::Database;
use state::AppState;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(|app| {
let data_dir = app.path().app_data_dir()?;
std::fs::create_dir_all(&data_dir)?;
let db_path = data_dir.join("app.db");
let db = Database::init(db_path.to_str().unwrap())?;
app.manage(AppState { db });
Ok(())
})
.invoke_handler(tauri::generate_handler![
commands::config::get_all_config,
commands::config::get_config,
commands::config::set_config,
commands::config::delete_config,
commands::system::get_system_info,
commands::system::greet,
commands::user::list_users,
commands::user::create_user,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Command 注入参数
Tauri Command 除了接收前端传来的参数,还可以注入框架对象。
注入 AppHandle(应用句柄)
#[tauri::command]
fn with_app(app: tauri::AppHandle) -> Result<String, String> {
let data_dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?;
Ok(data_dir.to_string_lossy().into())
}
注入 Window(当前窗口)
#[tauri::command]
fn with_window(window: tauri::Window) -> Result<(), String> {
window.set_title("新标题")
.map_err(|e| e.to_string())?;
Ok(())
}
注入 State(全局状态)
use tauri::State;
use crate::database::Database;
#[tauri::command]
fn with_state(db: State<'_, Database>) -> Result<Vec<String>, String> {
db.get_all_config()
.map(|configs| configs.into_iter().map(|c| c.key).collect())
.map_err(|e| e.to_string())
}
组合注入
#[tauri::command]
async fn complex_cmd(
app: tauri::AppHandle,
window: tauri::Window,
db: State<'_, Database>,
user_id: u32,
name: String,
) -> Result<String, String> {
let version = app.package_info().version.to_string();
window.set_title(&format!("用户 {}", name))
.map_err(|e| e.to_string())?;
let config = db.get_config("theme")
.map_err(|e| e.to_string())?;
Ok(format!("版本: {}, 用户: {}, ID: {}", version, name, user_id))
}
异步 Command
基础异步 Command
#[tauri::command]
async fn fetch_url(url: String) -> Result<String, String> {
reqwest::get(&url)
.await
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())
}
依赖配置
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["full"] }
前端调用(无需改变)
const result = await invoke<string>("fetch_url", {
url: "https://example.com/api/data"
});
进度回报模式
后端:使用事件发送进度
use tauri::{Emitter, Window};
#[tauri::command]
async fn long_task(window: Window) -> Result<String, String> {
for i in 0..100 {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
window.emit("progress", i)
.map_err(|e| e.to_string())?;
}
Ok("任务完成".into())
}
前端:监听进度事件
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { Progress, message } from "antd";
function LongTaskComponent() {
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
async function startTask() {
setLoading(true);
const unlisten = await listen<number>("progress", (event) => {
setProgress(event.payload);
});
try {
const result = await invoke<string>("long_task");
message.success(result);
} catch (error) {
message.error(`任务失败: ${error}`);
} finally {
setLoading(false);
unlisten();
}
}
return (
<div>
<Button onClick={startTask} loading={loading}>
开始任务
</Button>
{loading && <Progress percent={progress} />}
</div>
);
}
Command 中执行子进程(Windows 防弹窗)
强制规则: 在 Command 中使用 std::process::Command 或 tokio::process::Command 启动子进程时,必须在 Windows 上设置 CREATE_NO_WINDOW 标志,否则打包后每次调用都会弹出 CMD 黑窗口。
原理
- 开发模式 (
tauri dev):Rust 进程运行在终端中,子进程继承父进程控制台,不弹窗
- 打包后 (
.exe):应用是 GUI 进程(无控制台),Windows 自动为子进程创建新控制台窗口
std::process::Command(同步)
#[tauri::command]
pub fn detect_tool() -> Result<String, String> {
let mut cmd = std::process::Command::new("tool");
cmd.arg("--version");
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
let output = cmd.output().map_err(|e| e.to_string())?;
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
tokio::process::Command(异步)
#[tauri::command]
pub async fn run_npm_command() -> Result<String, String> {
let npm_cmd = if cfg!(target_os = "windows") { "npm.cmd" } else { "npm" };
let mut cmd = tokio::process::Command::new(npm_cmd);
cmd.args(["view", "some-package", "--json"]);
#[cfg(target_os = "windows")]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
let output = cmd.output().await.map_err(|e| e.to_string())?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
辅助函数模式(多处调用时推荐)
#[cfg(target_os = "windows")]
fn silent_command(program: &str) -> std::process::Command {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let mut cmd = std::process::Command::new(program);
cmd.creation_flags(CREATE_NO_WINDOW);
cmd
}
关键区别
| API | creation_flags 来源 | 需要额外 import |
|---|
std::process::Command | std::os::windows::process::CommandExt trait | 需要 use std::os::windows::process::CommandExt; |
tokio::process::Command | 内置方法 | 不需要额外 import |
错误处理最佳实践
使用 AppError 枚举
use crate::error::AppError;
#[tauri::command]
fn safe_read(path: String) -> Result<String, String> {
std::fs::read_to_string(&path)
.map_err(|e| AppError::Io(e).to_string())
}
结构化错误响应
use serde::Serialize;
#[derive(Debug, Serialize)]
struct CommandError {
code: String,
message: String,
}
#[tauri::command]
fn structured_error() -> Result<String, CommandError> {
Err(CommandError {
code: "NOT_FOUND".into(),
message: "资源不存在".into(),
})
}
try {
await invoke("structured_error");
} catch (e) {
const error = e as { code: string; message: string };
if (error.code === "NOT_FOUND") {
message.error("资源不存在");
}
}
批量操作模式
后端:批量处理
use crate::database::Database;
use tauri::State;
#[derive(serde::Deserialize)]
struct BatchConfigInput {
configs: Vec<(String, String)>,
}
#[tauri::command]
fn set_batch_config(
db: State<'_, Database>,
input: BatchConfigInput,
) -> Result<(), String> {
for (key, value) in input.configs {
db.set_config(&key, &value)
.map_err(|e| e.to_string())?;
}
Ok(())
}
前端:批量调用
await invoke("set_batch_config", {
input: {
configs: [
["theme", "dark"],
["language", "zh-CN"],
["fontSize", "14"]
]
}
});
Command 参数验证模式
#[tauri::command]
fn create_user(name: String, age: u32) -> Result<String, String> {
if name.is_empty() {
return Err("名称不能为空".into());
}
if age < 18 {
return Err("年龄必须大于等于18".into());
}
Ok(format!("用户 {} ({} 岁) 创建成功", name, age))
}
常见错误
| 错误做法 | 正确做法 |
|---|
| 同步 Command 做网络请求 | 使用 async Command |
| 不用 emit 通知进度 | 长任务通过事件回报进度 |
| 所有 Command 写在 lib.rs | 按模块拆分到 commands/ 目录 |
| 忘记 pub 导出 Command 函数 | 跨模块 Command 必须 pub fn |
| 组合注入时参数顺序错误 | 先注入对象,后前端参数 |
| 前端不清理事件监听 | useEffect 中返回 unlisten |
| 异步 Command 阻塞线程 | 使用 tokio::time::sleep 而非 std::thread::sleep |
Command 中裸用 Command::new() 启动子进程 | Windows 必须设置 CREATE_NO_WINDOW (0x08000000) 标志,否则打包后弹 CMD 窗口 |
完整示例:文件批量处理
后端 Command
use tauri::{Emitter, Window};
use std::path::PathBuf;
#[derive(serde::Deserialize)]
struct ProcessFilesInput {
files: Vec<String>,
}
#[tauri::command]
async fn process_files(
window: Window,
input: ProcessFilesInput,
) -> Result<u32, String> {
let total = input.files.len();
let mut processed = 0;
for (idx, file_path) in input.files.iter().enumerate() {
window.emit("file-progress", (idx + 1, total))
.map_err(|e| e.to_string())?;
match process_single_file(file_path).await {
Ok(_) => {
processed += 1;
window.emit("file-success", file_path)
.map_err(|e| e.to_string())?;
}
Err(e) => {
window.emit("file-error", (file_path, e.to_string()))
.map_err(|e| e.to_string())?;
}
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
Ok(processed)
}
async fn process_single_file(path: &str) -> Result<(), String> {
Ok(())
}
前端调用
import { listen, UnlistenFn } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { Progress, message, List } from "antd";
function FileProcessor() {
const [progress, setProgress] = useState<[number, number]>([0, 0]);
const [logs, setLogs] = useState<string[]>([]);
async function handleProcess(files: string[]) {
const listeners: UnlistenFn[] = [];
listeners.push(
await listen<[number, number]>("file-progress", (e) => {
setProgress(e.payload);
})
);
listeners.push(
await listen<string>("file-success", (e) => {
setLogs((prev) => [...prev, `✓ ${e.payload}`]);
})
);
listeners.push(
await listen<[string, string]>("file-error", (e) => {
setLogs((prev) => [...prev, `✗ ${e.payload[0]}: ${e.payload[1]}`]);
})
);
try {
const processed = await invoke<number>("process_files", {
input: { files }
});
message.success(`成功处理 ${processed} 个文件`);
} catch (error) {
message.error(`处理失败: ${error}`);
} finally {
listeners.forEach((unlisten) => unlisten());
}
}
const [current, total] = progress;
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
return (
<div>
<Progress percent={percent} status="active" />
<List
dataSource={logs}
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
</div>
);
}
检查清单