关于 tauri-app-dev
面向使用 Tauri 2.0 的应用开发者的开发技能,介绍以 Web 前端和 Rust 后端构建桌面应用的常见模式。内容涵盖 IPC、安全权限、插件、打包及测试。
主要功能
- 01设计 Web 前端与 Rust 后端架构
- 02通过命令和事件处理 IPC
- 03配置能力与 API 权限
- 04使用文件系统等插件
- 05构建、打包和测试应用
适用场景
- 01从 Web 前端调用 Rust 命令
- 02为窗口配置文件访问权限
- 03实现文件对话框与读写操作
- 04测试正在运行的 Tauri 应用
MCP 相关
提及通过 Tauri MCP 工具对运行中的应用执行端到端测试,并明确不使用 chrome-devtools MCP 测试应用。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ xiaolai/ vmark --skill tauri-app-dev安装后在 Claude Code 中输入/tauri-app-dev确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/bundling.mdcommands-and-ipc.mdpatterns.mdplugins.mdsecurity.md详细介绍
Tauri 2.0 应用开发
Tauri 是一个框架,可使用 Web 前端和 Rust 后端构建小巧、快速且安全的桌面应用。
架构概览
┌─────────────────────────────────────────┐
│ Frontend (Webview) │
│ HTML/CSS/JS • React/Vue/Svelte │
└────────────────┬────────────────────────┘
│ IPC (invoke/events)
┌────────────────▼────────────────────────┐
│ Tauri Core (Rust) │
│ Commands • State • Plugins • Events │
└────────────────┬────────────────────────┘
│ TAO (windows) + WRY (webview)
┌────────────────▼────────────────────────┐
│ Operating System │
│ macOS • Windows • Linux • Mobile │
└─────────────────────────────────────────┘
项目结构
my-app/
├── src/ # Frontend source
├── src-tauri/
│ ├── Cargo.toml # Rust dependencies
│ ├── tauri.conf.json # Tauri configuration
│ ├── capabilities/ # Security permissions (v2)
│ │ └── default.json
│ ├── src/
│ │ ├── main.rs # Desktop entry point
│ │ └── lib.rs # Main app logic + mobile entry
│ └── icons/
└── package.json
命令(前端 → Rust)
在 Rust 中使用 #[tauri::command] 定义命令:
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path).map_err(|e| e.to_string())
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, read_file])
.run(tauri::generate_context!())
.expect("error running app");
}
从前端调用(直接调用):
import { invoke } from '@tauri-apps/api/core';
const greeting = await invoke<string>('greet', { name: 'World' });
const content = await invoke<string>('read_file', { path: '/tmp/test.txt' });
项目约定: 使用 TanStack Query 封装 invoke(),以进行缓存和状态管理:
import { useQuery, useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
// Query (read operations)
const { data: content } = useQuery({
queryKey: ['file', path],
queryFn: () => invoke<string>('read_file', { path }),
});
// Mutation (write operations)
const { mutate: saveFile } = useMutation({
mutationFn: (content: string) => invoke('write_file', { path, content }),
});
关键规则:
- 参数必须实现
serde::Deserialize - 返回类型必须实现
serde::Serialize - 对可能失败的操作使用
Result<T, E> - 异步命令在工作线程池上运行(不会阻塞)
- Rust 中的 snake_case → JS 参数中的 camelCase
状态管理
在多个命令间共享状态:
use std::sync::Mutex;
use tauri::State;
struct AppState {
counter: Mutex<i32>,
db: Mutex<Option<Database>>,
}
#[tauri::command]
fn increment(state: State<'_, AppState>) -> i32 {
let mut counter = state.counter.lock().unwrap();
*counter += 1;
*counter
}
pub fn run() {
tauri::Builder::default()
.manage(AppState {
counter: Mutex::new(0),
db: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![increment])
.run(tauri::generate_context!())
.expect("error running app");
}
通过 AppHandle 访问(用于后台线程):
use tauri::Manager;
#[tauri::command]
async fn background_task(app: tauri::AppHandle) {
let state = app.state::<AppState>();
// use state...
}
事件(Rust → 前端)
从 Rust 发出事件:
use tauri::Emitter;
#[tauri::command]
fn start_process(app: tauri::AppHandle) {
std::thread::spawn(move || {
for i in 0..100 {
app.emit("progress", i).unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
}
app.emit("complete", "Done!").unwrap();
});
}
在前端监听:
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<number>('progress', (event) => {
console.log(`Progress: ${event.payload}%`);
});
// Clean up when done
unlisten();
必备插件
安装插件:在 src-tauri 中运行 cargo add <plugin>,在前端运行 pnpm add <package>。
| 插件 | Cargo Crate | NPM Package | 用途 |
|---|---|---|---|
| 文件系统 | tauri-plugin-fs | @tauri-apps/plugin-fs | 读写文件 |
| 对话框 | tauri-plugin-dialog | @tauri-apps/plugin-dialog | 打开/保存对话框 |
| 剪贴板 | tauri-plugin-clipboard-manager | @tauri-apps/plugin-clipboard-manager | 复制/粘贴 |
| Shell | tauri-plugin-shell | @tauri-apps/plugin-shell | 运行外部命令 |
| 存储 | tauri-plugin-store | @tauri-apps/plugin-store | 键值持久化 |
| 更新器 | tauri-plugin-updater | @tauri-apps/plugin-updater | 自动更新 |
在 Rust 中注册:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_clipboard_manager::init())
.run(tauri::generate_context!())
.expect("error running app");
}
安全:Capabilities 与权限
Tauri 2.0 使用 capabilities(位于 src-tauri/capabilities/)控制每个窗口可访问的 API。
src-tauri/capabilities/default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-read-text-file",
"dialog:default",
{
"identifier": "fs:scope",
"allow": [{ "path": "$APPDATA/**" }, { "path": "$DOCUMENT/**" }]
}
]
}
作用域变量: $APPDATA、$APPCONFIG、$DOCUMENT、$DOWNLOAD、$HOME、$TEMP 等。
文件操作(编辑器模式)
import { open, save } from '@tauri-apps/plugin-dialog';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
// Open file dialog
const path = await open({
filters: [{ name: 'Markdown', extensions: ['md'] }],
multiple: false,
});
if (path) {
const content = await readTextFile(path);
// Edit content...
await writeTextFile(path, modifiedContent);
}
// Save as dialog
const savePath = await save({
filters: [{ name: 'Markdown', extensions: ['md'] }],
defaultPath: 'untitled.md',
});
if (savePath) {
await writeTextFile(savePath, content);
}
窗口管理
在运行时创建窗口:
use tauri::{WebviewUrl, WebviewWindowBuilder};
#[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)
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
在 tauri.conf.json 中配置:
{
"app": {
"windows": [
{
"label": "main",
"title": "My App",
"width": 1200,
"height": 800,
"decorations": true,
"resizable": true
}
]
}
}
自定义标题栏
在配置中设置 decorations: false,然后:
<div data-tauri-drag-region class="titlebar">
<span>My App</span>
<button id="minimize">−</button>
<button id="maximize">□</button>
<button id="close">×</button>
</div>
import { getCurrentWindow } from '@tauri-apps/api/window';
const appWindow = getCurrentWindow();
document.getElementById('minimize')?.addEventListener('click', () => appWindow.minimize());
document.getElementById('maximize')?.addEventListener('click', () => appWindow.toggleMaximize());
document.getElementById('close')?.addEventListener('click', () => appWindow.close());
构建与分发
# Development
pnpm tauri dev
# Production build
pnpm tauri build
# Build specific targets
pnpm tauri build --target universal-apple-darwin # macOS universal
pnpm tauri build --bundles deb,appimage # Linux only
pnpm tauri build --bundles nsis # Windows NSIS
输出位置:
- macOS:
target/release/bundle/macos/*.app、*.dmg - Windows:
target/release/bundle/nsis/*-setup.exe、msi/*.msi - Linux:
target/release/bundle/deb/*.deb、appimage/*.AppImage
快速参考
| 任务 | 资源 |
|---|---|
| 命令、IPC、通道 | 参见 references/commands-and-ipc.md |
| 插件使用与开发 | 参见 references/plugins.md |
| 安全配置 | 参见 references/security.md |
| 打包与分发 | 参见 references/bundling.md |
| 常见应用模式 | 参见 references/patterns.md |
测试驱动开发(TDD)
重要: 始终遵循 TDD——在实现之前编写测试。
TDD 工作流
1. RED → Write failing test first
2. GREEN → Write minimal code to pass
3. REFACTOR → Clean up, keep tests green
测试技术栈
| 层级 | 工具 | 用途 |
|---|---|---|
| Rust 单元测试 | cargo test | 测试命令和业务逻辑 |
| React 单元测试 | Vitest | 测试组件、hooks、stores |
| 集成测试 | Vitest + MSW | 使用模拟 IPC 测试前端 |
| E2E | Tauri MCP | 测试正在运行的应用(不要使用 Chrome DevTools) |
使用 Tauri MCP 进行 E2E 测试
重要: 始终使用 tauri_* MCP 工具测试正在运行的应用。不要使用 chrome-devtools MCP——它仅用于浏览器页面。
// Tauri MCP workflow for E2E tests:
// 1. Start session (connect to running Tauri app)
tauri_driver_session({ action: 'start', port: 9223 })
// 2. Take snapshot (get DOM state)
tauri_webview_screenshot()
tauri_webview_find_element({ selector: '.editor-content' })
// 3. Interact with app
tauri_webview_interact({ action: 'click', selector: '#save-button' })
tauri_webview_keyboard({ action: 'type', selector: 'input', text: 'hello' })
// 4. Wait for results
tauri_webview_wait_for({ type: 'selector', value: '.success-toast' })
// 5. Verify IPC calls
tauri_ipc_monitor({ action: 'start' })
tauri_ipc_get_captured({ filter: 'save_file' })
// 6. Check backend state
tauri_ipc_execute_command({ command: 'get_app_state' })
// 7. Read logs for debugging
tauri_read_logs({ source: 'console', lines: 50 })
Rust 单元测试
// src-tauri/src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let result = greet("World".to_string());
assert_eq!(result, "Hello, World!");
}
#[test]
fn test_parse_markdown() {
let input = "# Hello";
let result = parse_markdown(input);
assert!(result.is_ok());
assert_eq!(result.unwrap().title, "Hello");
}
#[tokio::test]
async fn test_async_command() {
let result = read_file("/tmp/test.txt".to_string()).await;
// Test with temp files or mocks
}
}
运行:cd src-tauri && cargo test
React 组件测试(Vitest)
// src/components/Editor.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Editor } from './Editor'
// Mock Tauri invoke
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn()
}))
describe('Editor', () => {
it('should render editor content', () => {
render(<Editor initialValue="# Hello" />)
expect(screen.getByText('Hello')).toBeInTheDocument()
})
it('should call save on Ctrl+S', async () => {
const { invoke } = await import('@tauri-apps/api/core')
render(<Editor initialValue="test" />)
await userEvent.keyboard('{Control>}s{/Control}')
expect(invoke).toHaveBeenCalledWith('save_file', expect.any(Object))
})
})
Zustand Store 测试
// src/stores/editorStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useEditorStore } from './editorStore'
describe('editorStore', () => {
beforeEach(() => {
// Reset store before each test
useEditorStore.setState({
content: '',
isDirty: false,
filePath: null
})
})
it('should update content and mark dirty', () => {
const { setContent } = useEditorStore.getState()
setContent('new content')
const state = useEditorStore.getState()
expect(state.content).toBe('new content')
expect(state.isDirty).toBe(true)
})
it('should clear dirty flag after save', () => {
useEditorStore.setState({ isDirty: true })
const { markSaved } = useEditorStore.getState()
markSaved()
expect(useEditorStore.getState().isDirty).toBe(false)
})
})
使用模拟 IPC 进行集成测试
// src/features/file/useFileOperations.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useFileOperations } from './useFileOperations'
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn()
}))
vi.mock('@tauri-apps/plugin-dialog', () => ({
open: vi.fn(),
save: vi.fn()
}))
describe('useFileOperations', () => {
let queryClient: QueryClient
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }
})
vi.clearAllMocks()
})
it('should open file and load content', async () => {
const { invoke } = await import('@tauri-apps/api/core')
const { open } = await import('@tauri-apps/plugin-dialog')
vi.mocked(open).mockResolvedValue('/path/to/file.md')
vi.mocked(invoke).mockResolvedValue('# File Content')
const { result } = renderHook(() => useFileOperations(), {
wrapper: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
})
await result.current.openFile()
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith('read_file', { path: '/path/to/file.md' })
})
})
})
TDD 示例:添加新功能
// Step 1: RED - Write failing test first
// src/features/wordcount/useWordCount.test.ts
describe('useWordCount', () => {
it('should count words in content', () => {
const { result } = renderHook(() => useWordCount('hello world'))
expect(result.current.words).toBe(2)
})
it('should handle empty content', () => {
const { result } = renderHook(() => useWordCount(''))
expect(result.current.words).toBe(0)
})
it('should count characters', () => {
const { result } = renderHook(() => useWordCount('hello'))
expect(result.current.characters).toBe(5)
})
})
// Step 2: GREEN - Minimal implementation
// src/features/wordcount/useWordCount.ts
export function useWordCount(content: string) {
return {
words: content.trim() ? content.trim().split(/\s+/).length : 0,
characters: content.length
}
}
// Step 3: REFACTOR - Add memoization, types, etc.
export function useWordCount(content: string): WordCountResult {
return useMemo(() => ({
words: content.trim() ? content.trim().split(/\s+/).length : 0,
characters: content.length,
charactersNoSpaces: content.replace(/\s/g, '').length
}), [content])
}
运行测试
# All tests
pnpm test
# Watch mode
pnpm test:watch
# Coverage
pnpm test:coverage
# Rust tests only
cd src-tauri && cargo test
# Type check + lint + test
pnpm check:all
调试技巧
- 开发者工具: 右键 → 检查,或按
Cmd+Option+I(macOS)/Ctrl+Shift+I(Windows/Linux) - Rust 日志: 使用
logcrate +tauri-plugin-log,或使用println!(在终端中可见) - 检查权限配置:“Not allowed”错误表示权限配置中缺少相应权限
- IPC 错误: 确保参数名称匹配(Rust 的 snake_case → JS 的 camelCase)
- E2E 调试: 使用
tauri_read_logs({ source: 'console' })查看 webview 控制台
相关技能
tauri-v2-integration— VMark 专用的 Tauri IPC 模式(invoke/emit 桥接、菜单快捷键)tauri-mcp-testing— 通过 MCP 工具对正在运行的 Tauri 应用进行 E2E 测试rust-tauri-backend— VMark Rust 后端(命令、菜单项、文件系统)