mcpField
T
tauri-app-dev

tauri-app-dev

github.com/xiaolai开发87876 次安装今天更新
代码仓库

关于 tauri-app-dev

面向使用 Tauri 2.0 的应用开发者的开发技能,介绍以 Web 前端和 Rust 后端构建桌面应用的常见模式。内容涵盖 IPC、安全权限、插件、打包及测试。

主要功能

  1. 01设计 Web 前端与 Rust 后端架构
  2. 02通过命令和事件处理 IPC
  3. 03配置能力与 API 权限
  4. 04使用文件系统等插件
  5. 05构建、打包和测试应用

适用场景

  1. 01从 Web 前端调用 Rust 命令
  2. 02为窗口配置文件访问权限
  3. 03实现文件对话框与读写操作
  4. 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.md
commands-and-ipc.md
patterns.md
plugins.md
security.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 CrateNPM Package用途
文件系统tauri-plugin-fs@tauri-apps/plugin-fs读写文件
对话框tauri-plugin-dialog@tauri-apps/plugin-dialog打开/保存对话框
剪贴板tauri-plugin-clipboard-manager@tauri-apps/plugin-clipboard-manager复制/粘贴
Shelltauri-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 测试前端
E2ETauri 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 日志: 使用 log crate + 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 后端(命令、菜单项、文件系统)

评论

支持 Markdown