<!-- title: Claude Code + Tauri桌面应用全实战 -->

# Claude Code + Tauri v2 桌面应用开发：从零到交付全实战

> **学习目标**：掌握使用 Claude Code 全流程驱动 Tauri v2 桌面应用开发，涵盖项目初始化、IPC 架构、前后端联调、打包分发。学完可独立交付跨平台桌面应用。
>
> **适用人群**：有基本前端/命令行经验的开发者，对 Rust 不要求深入了解，但需愿意阅读 Rust 代码。

---

## 一、为什么选择 Tauri + Claude Code

### 1.1 Tauri vs Electron：真实数据对比

2026 年，Tauri v2 已进入生产就绪阶段。社区数据对比：

| 维度 | Electron | Tauri v2 |
|------|----------|----------|
| 安装包体积 | 80–200 MB | 3–15 MB |
| 内存占用 | 200–500 MB | 30–80 MB |
| 启动速度 | 2–5 秒 | 0.3–1 秒 |
| 后端语言 | Node.js | Rust |
| 渲染引擎 | 内置 Chromium | 系统 WebView |
| 移动端支持 | 无（需 Capacitor） | 原生 iOS/Android |
| 安全模型 | 完整 Node.js 权限 | 最小权限 + 能力系统 |

Tauri 的核心优势：**不打包 Chromium，使用操作系统原生 WebView**。这意味着 macOS 用 WebKit、Windows 用 WebView2、Linux 用 WebKitGTK——安装包从 100MB+ 压缩到 5MB 级别。

### 1.2 Claude Code 驱动 Tauri 开发的独特价值

Claude Code 对 Tauri 开发有三个关键加速点：

1. **Rust 后端代码生成**：Claude Code 能直接编写 Rust 命令（`#[tauri::command]`），处理序列化/反序列化，减少语言切换成本
2. **IPC 桥接自动生成**：Claude Code 能同时维护 Rust 后端和 TypeScript/前端调用层，保持两端类型一致
3. **跨文件重构**：修改一个 Rust 结构体时，自动更新所有引用该类型的前端代码

实际案例：开发者 Steve Bolch 使用 Claude Code 作为"整个开发团队"，独立完成了 Tauri 2.0 + SvelteKit 5 的桌面应用交付，涵盖 UI、Rust 后端、打包发布全流程。另一位开发者 Caio Ricciuti 用两天时间构建了 Glyphic——一个管理 Claude Code 会话的桌面 GUI 工具。

---

## 二、环境准备

### 2.1 系统要求

```bash
# macOS
xcode-select --install
brew install rustup
rustup-init

# Linux (Ubuntu/Debian)
sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows
# 安装 Visual Studio Build Tools + WebView2
# https://visualstudio.microsoft.com/visual-cpp-build-tools/
winget install Microsoft.WebView2Runtime
```

### 2.2 核心工具安装

```bash
# Rust 工具链（Tauri 要求 1.77.2+）
rustup default stable
rustc --version  # 确认 >= 1.77.2

# Node.js（前端构建）
node --version   # 建议 >= 20.x

# Claude Code
npm install -g @anthropic-ai/claude-code
claude --version

# Tauri CLI
cargo install tauri-cli
```

### 2.3 验证环境

```bash
# 创建一个最小化测试项目
cargo tauri init --template vanilla
cargo tauri dev
# 应弹出空白桌面窗口，环境OK
```

---

## 三、项目初始化：CLAUDE.md 配置先行

在开始编码前，先在项目根目录创建 `CLAUDE.md`，这是让 Claude Code 高效理解 Tauri 项目的关键。

### 3.1 Tauri 项目专用 CLAUDE.md

```markdown
# Project: Clipboard Manager Desktop App

## Tech Stack
- Tauri v2 (Rust backend)
- Svelte 5 + SvelteKit (frontend)
- Tailwind CSS 4 (styling)
- SQLite via rusqlite (local storage)

## Project Structure
- `src/` - SvelteKit frontend code
- `src-tauri/` - Rust backend code
- `src-tauri/src/main.rs` - Rust entry point
- `src-tauri/src/commands/` - Tauri commands (IPC handlers)
- `src-tauri/tauri.conf.json` - Tauri configuration
- `src-tauri/capabilities/` - Security capability files

## Architecture
- Frontend communicates with Rust backend ONLY via Tauri invoke()
- All file system, database, OS operations happen in Rust
- Frontend never directly accesses Node.js APIs or filesystem

## Key Rules
- Rust commands must use #[tauri::command] macro
- All command parameters must implement Serialize + Deserialize
- Use Result<T, String> for error handling in commands
- Frontend calls: import { invoke } from '@tauri-apps/api/core'
- Events use tauri::AppHandle::emit() from Rust, listen() from frontend

## Build & Run
- `cargo tauri dev` - development mode
- `cargo tauri build` - production build
- `npm run dev` - frontend-only dev (no Rust)
```

### 3.2 为什么这个 CLAUDE.md 如此重要

Tauri 项目横跨两种语言（Rust + TypeScript/Svelte），Claude Code 需要明确知道：

- **命令放哪里**：`src-tauri/src/commands/` 而非 `src-tauri/src/main.rs`
- **错误处理约定**：统一用 `Result<T, String>` 而非自定义错误类型
- **前后端通信边界**：只通过 `invoke()` 走 IPC，不要尝试在 Rust 端操作 DOM

没有这个文件，Claude Code 可能将 Rust 命令写在 `main.rs` 里导致文件膨胀，或在前端直接使用 Node.js API（Tauri 环境中没有 Node.js）。

---

## 四、创建项目：SvelteKit + Tauri v2

### 4.1 用 Claude Code 初始化

在终端中启动 Claude Code，直接对话式创建项目：

```bash
mkdir clipboard-manager && cd clipboard-manager
claude
```

在 Claude Code 中输入：

```
创建一个 Tauri v2 + SvelteKit 项目。
- 使用 npm create tauri-app@latest -- --template sveltekit
- 项目名 clipboard-manager
- 前端用 SvelteKit + Tailwind CSS
- 创建 CLAUDE.md（使用上面准备的内容）
- 创建 src-tauri/src/commands/ 目录
```

Claude Code 会执行以下操作序列：

```bash
# Step 1: 脚手架
npm create tauri-app@latest -- clipboard-manager --template sveltekit

# Step 2: 安装依赖
cd clipboard-manager
npm install
npm install -D @tailwindcss/vite tailwindcss

# Step 3: 配置 Tailwind
# 在 vite.config.js 添加 Tailwind 插件

# Step 4: Tauri 依赖
cd src-tauri
cargo add serde --features derive
cargo add rusqlite --features bundled
```

### 4.2 项目结构验证

```
clipboard-manager/
├── CLAUDE.md                    # 项目规则（核心！）
├── package.json
├── src/                         # SvelteKit 前端
│   ├── routes/
│   │   └── +page.svelte        # 主页面
│   ├── lib/
│   │   └── api.ts              # Tauri invoke 封装
│   └── app.html
├── src-tauri/                   # Rust 后端
│   ├── Cargo.toml
│   ├── tauri.conf.json
│   ├── capabilities/
│   │   └── default.json        # 安全能力配置
│   └── src/
│       ├── main.rs              # 入口 + 插件注册
│       ├── lib.rs               # 命令注册
│       └── commands/
│           ├── mod.rs
│           └── clipboard.rs     # 剪贴板操作命令
└── static/
```

---

## 五、核心架构：IPC 桥接实战

Tauri v2 的 IPC 架构是整个开发的关键。理解这张图，就理解了 Tauri 的数据流：

```mermaid
graph LR
    subgraph Frontend["前端 (WebView)"]
        UI["Svelte 组件"]
        API["api.ts 封装层"]
    end

    subgraph IPC["IPC 桥接"]
        Invoke["invoke()"]
        Event["事件系统"]
        Channel["Channel 流"]
    end

    subgraph Backend["Rust 后端"]
        Cmd["#[tauri::command]"]
        State["StateManager"]
        DB["SQLite / 文件系统"]
    end

    UI --> API --> Invoke --> Cmd
    Cmd --> State --> DB
    DB --> State --> Event --> UI
```

### 5.1 定义 Rust 命令

`src-tauri/src/commands/clipboard.rs`：

```rust
use serde::{Deserialize, Serialize};
use tauri::State;
use std::sync::Mutex;

#[derive(Serialize, Deserialize, Clone)]
pub struct ClipItem {
    pub id: i64,
    pub content: String,
    pub content_type: String,  // "text" | "image" | "file"
    pub created_at: String,
    pub pinned: bool,
}

pub struct AppState {
    pub db: Mutex<rusqlite::Connection>,
}

#[tauri::command]
pub fn get_clips(
    state: State<'_, AppState>,
    limit: Option<u32>,
) -> Result<Vec<ClipItem>, String> {
    let limit = limit.unwrap_or(50);
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    
    let mut stmt = conn
        .prepare("SELECT id, content, content_type, created_at, pinned FROM clips ORDER BY created_at DESC LIMIT ?1")
        .map_err(|e| e.to_string())?;
    
    let items = stmt
        .query_map([limit], |row| {
            Ok(ClipItem {
                id: row.get(0)?,
                content: row.get(1)?,
                content_type: row.get(2)?,
                created_at: row.get(3)?,
                pinned: row.get(4)?,
            })
        })
        .map_err(|e| e.to_string())?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())?;
    
    Ok(items)
}

#[tauri::command]
pub fn save_clip(
    state: State<'_, AppState>,
    content: String,
    content_type: String,
) -> Result<ClipItem, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    
    conn.execute(
        "INSERT INTO clips (content, content_type) VALUES (?1, ?2)",
        [&content, &content_type],
    ).map_err(|e| e.to_string())?;
    
    let id = conn.last_insert_rowid();
    
    Ok(ClipItem {
        id,
        content,
        content_type,
        created_at: chrono::Utc::now().to_rfc3339(),
        pinned: false,
    })
}

#[tauri::command]
pub fn delete_clip(state: State<'_, AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM clips WHERE id = ?1", [id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn toggle_pin(state: State<'_, AppState>, id: i64) -> Result<bool, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE clips SET pinned = NOT pinned WHERE id = ?1",
        [id],
    ).map_err(|e| e.to_string())?;
    
    let pinned: bool = conn
        .query_row("SELECT pinned FROM clips WHERE id = ?1", [id], |row| row.get(0))
        .map_err(|e| e.to_string())?;
    
    Ok(pinned)
}
```

### 5.2 注册命令

`src-tauri/src/lib.rs`：

```rust
mod commands;
use commands::clipboard::{AppState, get_clips, save_clip, delete_clip, toggle_pin};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let db = rusqlite::Connection::open_in_memory()
        .expect("failed to open database");
    
    db.execute_batch(
        "CREATE TABLE IF NOT EXISTS clips (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            content TEXT NOT NULL,
            content_type TEXT NOT NULL DEFAULT 'text',
            created_at TEXT DEFAULT (datetime('now')),
            pinned INTEGER DEFAULT 0
        )"
    ).expect("failed to create table");
    
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .manage(AppState {
            db: std::sync::Mutex::new(db),
        })
        .invoke_handler(tauri::generate_handler![
            get_clips,
            save_clip,
            delete_clip,
            toggle_pin,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

### 5.3 前端 API 封装层

`src/lib/api.ts`：

```typescript
import { invoke } from '@tauri-apps/api/core';

export interface ClipItem {
  id: number;
  content: string;
  content_type: string;
  created_at: string;
  pinned: boolean;
}

export async function getClips(limit?: number): Promise<ClipItem[]> {
  return invoke<ClipItem[]>('get_clips', { limit });
}

export async function saveClip(content: string, contentType: string): Promise<ClipItem> {
  return invoke<ClipItem>('save_clip', { content, contentType });
}

export async function deleteClip(id: number): Promise<void> {
  return invoke('delete_clip', { id });
}

export async function togglePin(id: number): Promise<boolean> {
  return invoke<boolean>('toggle_pin', { id });
}
```

### 5.4 前端 UI 组件

`src/routes/+page.svelte`：

```svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { getClips, saveClip, deleteClip, togglePin, type ClipItem } from '$lib/api';

  let clips: ClipItem[] = $state([]);
  let inputText: string = $state('');
  let loading: boolean = $state(false);

  onMount(async () => {
    await refreshClips();
  });

  async function refreshClips() {
    loading = true;
    try {
      clips = await getClips(100);
    } catch (e) {
      console.error('Failed to load clips:', e);
    } finally {
      loading = false;
    }
  }

  async function handleSave() {
    if (!inputText.trim()) return;
    await saveClip(inputText, 'text');
    inputText = '';
    await refreshClips();
  }

  async function handleDelete(id: number) {
    await deleteClip(id);
    await refreshClips();
  }

  async function handlePin(id: number) {
    await togglePin(id);
    await refreshClips();
  }
</script>

<main class="min-h-screen bg-gray-900 text-white p-6">
  <h1 class="text-2xl font-bold mb-6">📋 Clipboard Manager</h1>
  
  <div class="flex gap-2 mb-6">
    <input
      bind:value={inputText}
      onkeydown={(e) => e.key === 'Enter' && handleSave()}
      placeholder="粘贴或输入内容..."
      class="flex-1 px-4 py-2 rounded bg-gray-800 border border-gray-700"
    />
    <button onclick={handleSave} class="px-6 py-2 bg-blue-600 rounded hover:bg-blue-500">
      保存
    </button>
  </div>

  {#if loading}
    <p class="text-gray-400">加载中...</p>
  {:else if clips.length === 0}
    <p class="text-gray-400">暂无剪贴板记录</p>
  {:else}
    <div class="space-y-2">
      {#each clips as clip (clip.id)}
        <div class="flex items-center gap-3 p-3 rounded bg-gray-800">
          <span class="flex-1 text-sm truncate">{clip.content}</span>
          <span class="text-xs text-gray-500">{clip.created_at}</span>
          <button
            onclick={() => handlePin(clip.id)}
            class="px-2 py-1 text-xs rounded {clip.pinned ? 'bg-yellow-600' : 'bg-gray-700'}"
          >
            {clip.pinned ? '📌 已置顶' : '置顶'}
          </button>
          <button
            onclick={() => handleDelete(clip.id)}
            class="px-2 py-1 text-xs bg-red-700 rounded hover:bg-red-600"
          >
            删除
          </button>
        </div>
      {/each}
    </div>
  {/if}
</main>
```

### 5.5 关键：invoke() 的命名转换规则

Tauri 的 `invoke()` 有一个容易出错的命名映射：

| Rust 函数名 | invoke() 调用名 |
|------------|----------------|
| `get_clips` | `'get_clips'` |
| `save_clip` | `'save_clip'` |
| `togglePin` | `'toggle_pin'`（camelCase → snake_case） |

**Claude Code 特别注意**：如果 Rust 命令用 snake_case 命名（如 `get_clips`），前端 `invoke` 直接用同名。如果 Rust 用 camelCase（如 `togglePin`），Tauri 会自动转为 `toggle_pin`。在 CLAUDE.md 中统一约定使用 snake_case 可避免此问题。

---

## 六、安全能力配置

Tauri v2 的安全模型基于"能力"（Capabilities），而非 Electron 的全权限模型。

### 6.1 能力配置文件

`src-tauri/capabilities/default.json`：

```json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Default permissions for the app",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:default",
    "core:window:allow-close",
    "core:window:allow-set-title",
    "opener:default",
    {
      "identifier": "fs:default",
      "allow": [
        { "path": "$APPDATA/**" }
      ]
    }
  ]
}
```

### 6.2 安全原则

```mermaid
graph TD
    A["用户操作"] --> B{"需要 OS 权限？"}
    B -->|"否"| C["前端直接处理"]
    B -->|"是"| D["通过 invoke 调用 Rust"]
    D --> E{"能力配置允许？"}
    E -->|"否"| F["❌ 拒绝，返回权限错误"]
    E -->|"是"| G["✅ 执行 Rust 命令"]
    G --> H["返回结果到前端"]

    style F fill:#ef4444
    style G fill:#22c55e
```

与 Electron 对比：Electron 的 renderer 进程（如果不启用 contextIsolation）可以直访问 Node.js API，包括文件系统、网络、子进程。Tauri 前端完全无法直接访问系统资源——所有操作必须经过能力系统审核。

---

## 七、用 Claude Code 加速开发的实战技巧

### 7.1 分层 Prompt 策略

与其让 Claude Code 一次性生成整个应用，不如按 Tauri 的架构分层驱动：

**第一轮：定义数据模型和命令签名**

```
在 src-tauri/src/commands/clipboard.rs 中定义以下命令：
- get_clips(limit: Option<u32>) -> Vec<ClipItem>
- save_clip(content: String, content_type: String) -> ClipItem
- delete_clip(id: i64) -> ()
- search_clips(query: String) -> Vec<ClipItem>

ClipItem 结构体包含：id, content, content_type, created_at, pinned
使用 rusqlite，数据库路径用 tauri::api::path::app_data_dir
```

**第二轮：生成前端 API 层**

```
根据 src-tauri/src/commands/clipboard.rs 中的 Rust 命令，
生成对应的 TypeScript 封装到 src/lib/api.ts。
使用 invoke() 调用，导出类型安全的函数。
```

**第三轮：生成 UI 组件**

```
基于 src/lib/api.ts 中的函数，
创建 src/routes/+page.svelte：
- 顶部输入框 + 保存按钮
- 剪贴板列表，每项有：内容预览、时间、置顶按钮、删除按钮
- 置顶项排在最前
- 使用 Tailwind CSS，深色主题
```

这种分层方式的优势：每一步的输出都是下一步的输入，Claude Code 的上下文利用率最高。

### 7.2 使用 CLAUDE.md 锁定架构约定

```markdown
## Tauri Command Rules
- 命令文件统一放在 src-tauri/src/commands/ 目录
- 每个领域一个文件：clipboard.rs, settings.rs, export.rs
- 错误处理统一用 Result<T, String>
- 数据库操作使用 State<AppState> 注入
- 不在 main.rs 中写业务逻辑

## Frontend Rules
- 所有 Tauri 调用封装在 src/lib/api.ts
- UI 组件不直接 import invoke
- 使用 TypeScript 类型对应 Rust 结构体
- CSS 只用 Tailwind 工具类
```

### 7.3 调试 IPC 问题的 Prompt 模板

当遇到 `invoke` 调用失败时：

```
调试以下 Tauri IPC 调用错误：
- 前端调用：invoke('get_clips', { limit: 50 })
- 错误信息：[粘贴完整错误]
- Rust 命令签名：[粘贴函数签名]
- 检查：参数名是否匹配 snake_case/camelCase 转换规则
```

常见错误排查表：

| 错误信息 | 原因 | 解决方案 |
|---------|------|---------|
| `Command getClips not found` | 命令未注册 | 检查 `lib.rs` 的 `invoke_handler` |
| `Missing field limit` | 参数名不匹配 | Rust 用 `limit`，前端也必须用 `{ limit: 50 }` |
| `Permission denied` | 能力配置缺失 | 在 `capabilities/default.json` 添加权限 |
| `Serialization error` | 类型不匹配 | 检查 Rust `Serialize` 和 TS 类型是否一致 |

---

## 八、事件系统：Rust → 前端的实时推送

除了请求-响应模式的 `invoke()`，Tauri 还支持事件驱动的实时通信。

### 8.1 Rust 端发射事件

```rust
use tauri::{AppHandle, Emitter};

#[tauri::command]
pub fn watch_clipboard(app: AppHandle) -> Result<(), String> {
    let handle = app.clone();
    std::thread::spawn(move || {
        // 模拟剪贴板监听
        loop {
            let content = read_clipboard_content();
            if let Some(text) = content {
                let _ = handle.emit("clipboard-changed", text);
            }
            std::thread::sleep(std::time::Duration::from_secs(1));
        }
    });
    Ok(())
}
```

### 8.2 前端监听事件

```typescript
import { listen } from '@tauri-apps/api/event';

onMount(async () => {
  const unlisten = await listen<string>('clipboard-changed', (event) => {
    console.log('新剪贴板内容:', event.payload);
    refreshClips(); // 自动刷新列表
  });
  
  return () => unlisten();
});
```

### 8.3 双向通信架构

```mermaid
sequenceDiagram
    participant F as 前端 (Svelte)
    participant T as Tauri Core
    participant R as Rust 后端
    
    F->>T: invoke("watch_clipboard")
    T->>R: 路由到 watch_clipboard 命令
    R->>R: 启动后台线程监听剪贴板
    
    loop 每秒检测
        R->>T: emit("clipboard-changed", payload)
        T->>F: 事件回调触发
        F->>T: invoke("get_clips")
        T->>R: 查询数据库
        R-->>F: 返回最新列表
    end
```

---

## 九、打包与分发

### 9.1 构建生产版本

```bash
# 构建所有平台的安装包
cargo tauri build

# 输出位置
# macOS: src-tauri/target/release/bundle/dmg/
# Windows: src-tauri/target/release/bundle/msi/
# Linux: src-tauri/target/release/bundle/deb/ 或 appimage/
```

### 9.2 配置应用元数据

`src-tauri/tauri.conf.json` 关键字段：

```json
{
  "productName": "Clipboard Manager",
  "version": "1.0.0",
  "identifier": "com.example.clipboard-manager",
  "build": {
    "beforeBuildCommand": "npm run build",
    "beforeDevCommand": "npm run dev",
    "devUrl": "http://localhost:5173",
    "frontendDist": "../build"
  },
  "app": {
    "windows": [
      {
        "title": "Clipboard Manager",
        "width": 800,
        "height": 600,
        "resizable": true,
        "fullscreen": false
      }
    ],
    "security": {
      "csp": "default-src 'self'; style-src 'unsafe-inline'"
    }
  },
  "bundle": {
    "active": true,
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ],
    "category": "Utility"
  }
}
```

### 9.3 代码签名（生产必需）

```bash
# macOS
export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)"
cargo tauri build --target universal-apple-darwin

# Windows
# 需要购买代码签名证书
cargo tauri build
```

---

## 十、完整实战案例：Markdown 笔记应用

以下是一个比剪贴板管理器更完整的案例——支持 Markdown 编辑、实时预览、本地持久化的笔记应用。

### 10.1 项目结构

```
markdown-notes/
├── CLAUDE.md
├── src/
│   ├── routes/
│   │   ├── +page.svelte          # 笔记列表
│   │   └── edit/[id]/+page.svelte # 编辑页面
│   └── lib/
│       ├── api.ts                # IPC 封装
│       └── markdown.ts           # Markdown 解析
├── src-tauri/
│   └── src/commands/
│       ├── mod.rs
│       ├── notes.rs              # 笔记 CRUD
│       └── export.rs             # 导出 PDF/HTML
```

### 10.2 用 Claude Code 一键生成

在项目根目录启动 Claude Code 后：

```
创建一个 Tauri v2 + SvelteKit 的 Markdown 笔记应用。

要求：
1. Rust 后端使用 SQLite 存储，包含 notes 表（id, title, content, created_at, updated_at, tags）
2. 实现命令：list_notes, get_note, create_note, update_note, delete_note, search_notes, export_html
3. 前端：
   - 左侧笔记列表（支持搜索、标签过滤）
   - 右侧 Markdown 编辑器（实时预览）
   - 顶部工具栏（新建、删除、导出）
4. 使用 marked.js 解析 Markdown
5. 深色主题，响应式布局
6. 所有文件操作通过 Rust 后端

请按照 CLAUDE.md 中的架构规则，分层实现：
先 Rust 命令，再 API 封装，最后 UI 组件。
```

Claude Code 会自动按 CLAUDE.md 约定的架构分层输出代码。

### 10.3 预期成果

| 文件 | 行数 | 功能 |
|------|------|------|
| `commands/notes.rs` | ~120 | 7 个 CRUD + 搜索命令 |
| `commands/export.rs` | ~40 | HTML 导出 |
| `api.ts` | ~50 | TypeScript 封装 |
| `+page.svelte` | ~80 | 笔记列表 |
| `edit/[id]/+page.svelte` | ~150 | 编辑器 + 预览 |

整体代码量约 440 行，Claude Code 单次对话即可完成。

---

## 十一、移动端扩展

Tauri v2 的重大升级是支持 iOS/Android。同一套 Rust 后端 + 前端代码，可编译到移动端。

```bash
# 添加移动端目标
cargo tauri android init
cargo tauri ios init

# 运行 Android
cargo tauri android dev

# 运行 iOS
cargo tauri ios dev
```

**注意事项**：
- 移动端使用原生 WebView（Android WebView / iOS WKWebView）
- 文件系统路径需要使用 Tauri 的 `app_data_dir()` 而非硬编码
- 某些 Rust crate 可能不支持移动目标编译
- UI 需要适配触摸交互和移动端布局

---

## 十二、常见问题与排错

### Q1: `cargo tauri dev` 编译报错 WebKit 相关

**Linux 用户**：确保安装了 `libwebkit2gtk-4.1-dev`（注意是 4.1 版本，不是 4.0）：
```bash
sudo apt install libwebkit2gtk-4.1-dev
```

### Q2: 前端热更新不生效

检查 `tauri.conf.json` 中的 `devUrl` 是否匹配 SvelteKit 开发服务器端口（默认 5173）。

### Q3: Rust 编译时间过长

```toml
# Cargo.toml - 开发时使用优化
[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3
```

### Q4: SQLite 数据持久化

开发模式下使用内存数据库（`Connection::open_in_memory`），生产环境需改为文件路径：
```rust
let db_path = app.path().app_data_dir()?.join("data.db");
let db = rusqlite::Connection::open(&db_path)?;
```

### Q5: invoke 参数传递 undefined

TypeScript 的可选参数（`limit?: number`）在传递给 Rust 时，如果值为 `undefined`，Tauri 会将其视为缺失参数。确保 Rust 端参数类型为 `Option<T>`。

---

## 十三、开发工作流总结

```mermaid
graph TD
    A["1. 项目初始化<br/>cargo tauri init --template sveltekit"] --> B["2. 编写 CLAUDE.md<br/>定义架构规则"]
    B --> C["3. Claude Code 分层驱动<br/>Rust命令 → API层 → UI"]
    C --> D["4. cargo tauri dev<br/>实时联调"]
    D --> E{"功能完整？"}
    E -->|"否"| C
    E -->|"是"| F["5. cargo tauri build<br/>打包安装包"]
    F --> G["6. 代码签名 + 分发"]

    style A fill:#3b82f6,color:#fff
    style B fill:#8b5cf6,color:#fff
    style C fill:#f59e0b,color:#fff
    style D fill:#22c55e,color:#fff
    style F fill:#ef4444,color:#fff
    style G fill:#ec4899,color:#fff
```

---

## 延伸阅读

- [Tauri v2 官方文档](https://v2.tauri.app/learn/) — 完整的 API 参考和教程
- [awesome-tauri-ai](https://github.com/Bp7-dev/awesome-tauri-ai) — Tauri + AI 开发资源合集
- [Tauri IPC 官方文档](https://v2.tauri.app/concept/inter-process-communication/) — IPC 架构深度解析
- [Claude Code 最佳实践](https://code.claude.com/docs/en/best-practices) — Anthropic 官方最佳实践指南
- [Tauri-with-AI CLAUDE.md 模板](https://github.com/maxart/Tauri-with-AI) — 现成的 Tauri 项目 CLAUDE.md 模板

## 训练营适用性评估

| 维度 | 评估 |
|------|------|
| 适合训练营类型 | 桌面应用开发训练营、Rust 入门训练营、跨平台开发工坊 |
| 预计教学时长 | 4–6 小时（含动手实操） |
| 学员基础要求 | 基本前端知识 + 命令行操作能力 |
| 硬件要求 | 任意操作系统，4GB+ 内存 |
| 核心产出 | 每人交付一个可安装的桌面应用 |

## 参考来源

1. [I built and shipped a full desktop app entirely with Claude Code](https://dev.to/blacktape/i-built-and-shipped-a-full-desktop-app-entirely-with-claude-code-heres-what-actually-happened-4b8l) — 开发者使用 Claude Code 独立完成 Tauri 2.0 + SvelteKit 桌面应用交付的完整经验分享
2. [Tauri Practical Guide with Claude Code | ClaudeCodeLab](https://claudecode-lab.com/en/blog/claude-code-tauri-development/) — Claude Code 驱动 Tauri 开发的实战指南
3. [Build a Desktop App with Tauri v2 in 2026](https://rustify.rs/articles/rust-tauri-v2-desktop-app-tutorial-2026) — Tauri v2 桌面应用分步教程
4. [Claude Code with Tauri: Build Rust-Backed Desktop Apps](https://claudify.tech/blog/claude-code-tauri) — CLAUDE.md 配置与 IPC 架构最佳实践
5. [Tauri v2 IPC Architecture](https://v2.tauri.app/concept/inter-process-communication/) — Tauri 官方进程间通信文档

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
