tauri-guide

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Tauri + React Architecture Guide

Tauri + React 桌面应用架构指南

This guide explains how to build well-structured Tauri + React desktop applications.
本指南将讲解如何构建结构清晰的Tauri + React桌面应用。

References

参考资料

For detailed documentation on specific topics:
如需特定主题的详细文档:

Distribution & Updates

分发与更新

  • Auto-Updates - Implementing automatic updates with signing, hosting, and best practices
  • Code Signing - Code signing, notarization, and entitlements for macOS distribution
  • 自动更新 - 实现带签名、托管功能的自动更新及最佳实践
  • 代码签名 - macOS分发所需的代码签名、公证及权限配置

macOS Native Features

macOS原生功能

  • Window Chrome - Title bars, traffic lights, draggable regions, and vibrancy effects
  • Menu System - Native app menus, keyboard shortcuts, and context menus
  • Permissions - Handling screen recording, camera, microphone, and other permissions
  • Deep Linking - URL schemes, OAuth callbacks, and opening content from URLs
  • 窗口边框 - 标题栏、交通灯按钮、可拖拽区域及毛玻璃效果
  • 菜单系统 - 原生应用菜单、键盘快捷键及上下文菜单
  • 权限管理 - 处理屏幕录制、摄像头、麦克风及其他权限
  • 深度链接 - URL协议、OAuth回调及通过URL打开内容

App Architecture

应用架构

  • Multi-Window - Managing multiple windows, spotlight panels, and inter-window communication
  • Data Storage - Where to store databases, files, caches, and preferences
  • First-Run Experience - Onboarding, permission priming, and app location verification
  • 多窗口管理 - 管理多窗口、聚焦面板及窗口间通信
  • 数据存储 - 数据库、文件、缓存及偏好设置的存储位置
  • 首次启动体验 - 引导流程、权限预请求及应用位置验证

Rust vs TypeScript: Where Does Code Live?

Rust 与 TypeScript:代码该放在哪里?

The most important architectural decision in a Tauri app is knowing what belongs in Rust vs what belongs in TypeScript. Here's a clear framework:
Tauri应用中最重要的架构决策就是明确哪些逻辑适合用Rust实现哪些适合用TypeScript。以下是清晰的判断框架:

Use Rust (src-tauri/) For:

选择 Rust(src-tauri/)的场景:

CapabilityWhy Rust?Example
Global shortcutsOS-level keyboard hooksAlt+Space to open from anywhere
System trayNative menu bar integrationTray icon with menu
Window managementNative window APIsSpotlight-style panels, vibrancy effects
Screenshot captureOS screen capture APIs
screencapture
on macOS
Deep linksURL scheme registration
myapp://open/123
File system watchingEfficient OS notificationsWatch for file changes
Native dialogsOS file pickersOpen/save dialogs
ClipboardSystem clipboard accessCopy/paste integration
NotificationsSystem notification centerPush notifications
Auto-updatesBinary replacementApp update flow
Process spawningRunning external processesMCP servers, CLI tools
Permission requestsOS permission dialogsScreen recording access
能力为什么选Rust?示例
全局快捷键系统级键盘钩子按下Alt+Space从任意位置打开应用
系统托盘原生菜单栏集成带菜单的托盘图标
窗口管理原生窗口API聚焦式面板、毛玻璃效果
截图捕获系统屏幕捕获APImacOS上的
screencapture
深度链接URL协议注册
myapp://open/123
文件系统监听高效的系统通知监听文件变化
原生对话框系统文件选择器打开/保存对话框
剪贴板系统剪贴板访问复制/粘贴集成
通知系统通知中心推送通知
自动更新二进制替换应用更新流程
进程启动运行外部进程MCP服务器、CLI工具
权限请求系统权限对话框屏幕录制权限申请

Use TypeScript (src/) For:

选择 TypeScript(src/)的场景:

CapabilityWhy TypeScript?Example
All UIReact ecosystemComponents, layouts
Business logicFaster iterationValidation, transformations
Database queriesSQL via pluginCRUD operations
API callsFetch/streamingAI provider integrations
State managementReact/TanStack QueryApp state, cache
RoutingReact RouterNavigation
FormsReact patternsUser input
能力为什么选TypeScript?示例
所有UI界面React生态支持组件、布局
业务逻辑迭代速度更快验证、数据转换
数据库查询通过插件执行SQLCRUD操作
API调用Fetch/流式处理AI提供商集成
状态管理React/TanStack Query应用状态、缓存
路由React Router页面导航
表单React设计模式用户输入

Decision Framework

判断框架

Ask these questions:
  1. Does it need OS-level access? → Rust
  2. Does it need to work when app isn't focused? → Rust
  3. Does it interact with native UI chrome? → Rust
  4. Is it purely data/UI logic? → TypeScript
  5. Does it need fast iteration? → TypeScript (hot reload)
可以通过以下问题快速判断:
  1. 是否需要系统级访问权限? → 选Rust
  2. 是否需要在应用未聚焦时运行? → 选Rust
  3. 是否需要与原生UI边框交互? → 选Rust
  4. 是否纯数据/UI逻辑? → 选TypeScript
  5. 是否需要快速迭代? → 选TypeScript(支持热重载)

Real-World Examples

实际案例

Spotlight-style quick launcher (like Raycast, Alfred):
Rust:
- Global shortcut registration (Cmd+Space)
- Panel window type (non-activating, floating)
- Vibrancy/blur effects
- Show/hide without focus stealing

TypeScript:
- Search UI
- Result rendering
- Keyboard navigation within the panel
- Search logic
Screenshot annotation tool:
Rust:
- Screen capture (needs permissions)
- Window enumeration
- Save to disk
- Clipboard integration

TypeScript:
- Annotation canvas
- Tool palette
- Undo/redo
- Export options UI
AI chat app (like the app that inspired this guide):
Rust:
- Global shortcut for quick chat
- System tray
- Deep links (chorus://chat/123)
- Screenshot capture for context
- Image resizing for LLM limits

TypeScript:
- Chat UI
- Message streaming
- Model provider integrations
- Database queries
- Settings UI
聚焦式快速启动器(类似Raycast、Alfred):
Rust:
- 全局快捷键注册(Cmd+Space)
- 面板窗口类型(不激活、悬浮)
- 毛玻璃/模糊效果
- 显示/隐藏时不抢夺焦点

TypeScript:
- 搜索UI
- 结果渲染
- 面板内键盘导航
- 搜索逻辑
截图标注工具
Rust:
- 屏幕捕获(需要权限)
- 窗口枚举
- 保存到磁盘
- 剪贴板集成

TypeScript:
- 标注画布
- 工具面板
- 撤销/重做
- 导出选项UI
AI聊天应用(启发本指南的应用):
Rust:
- 快速聊天全局快捷键
- 系统托盘
- 深度链接(chorus://chat/123)
- 上下文截图捕获
- 针对LLM限制的图片缩放

TypeScript:
- 聊天UI
- 消息流式传输
- 模型提供商集成
- 数据库查询
- 设置UI

Anti-Patterns to Avoid

需避免的反模式

Don't use Rust for:
  • Business logic that could be in TypeScript
  • UI rendering (use React)
  • API calls (use fetch in TypeScript)
  • Complex state management
Don't use TypeScript for:
  • Anything requiring
    sudo
    or elevated permissions
  • System-wide keyboard shortcuts
  • Native window decorations
  • Accessing restricted OS APIs
不要用Rust实现:
  • 可在TypeScript中实现的业务逻辑
  • UI渲染(使用React)
  • API调用(使用TypeScript的fetch)
  • 复杂状态管理
不要用TypeScript实现:
  • 任何需要
    sudo
    或提升权限的操作
  • 系统级键盘快捷键
  • 原生窗口装饰
  • 访问受限制的系统API

Communication Pattern

通信模式

When Rust and TypeScript need to talk:
TypeScript → Rust: invoke("command_name", { args })
Rust → TypeScript: events (emit/listen)
Keep the boundary thin. Pass simple data (strings, numbers, JSON), not complex objects.
当Rust与TypeScript需要交互时:
TypeScript → Rust: invoke("command_name", { args })
Rust → TypeScript: events (emit/listen)
保持边界简洁。传递简单数据(字符串、数字、JSON),而非复杂对象。

Core Architecture

核心架构

┌─────────────────────────────────────────────────────────────┐
│                    React Components                          │
│                    (src/ui/components/)                      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    TanStack Query                            │
│              (caching, state management)                     │
│                    (src/core/api/)                           │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                   Database Layer                             │
│                 (direct SQL queries)                         │
│                    (src/core/db/)                            │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      SQLite                                  │
│              (via tauri-plugin-sql)                          │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    React 组件                               │
│                    (src/ui/components/)                      │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    TanStack Query                            │
│              (缓存、状态管理)                     │
│                    (src/core/api/)                           │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                   数据库层                             │
│                 (直接SQL查询)                         │
│                    (src/core/db/)                            │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      SQLite                                  │
│              (通过tauri-plugin-sql)                          │
└─────────────────────────────────────────────────────────────┘

Project Structure

项目结构

my-app/
├── src/                          # Frontend (React/TypeScript)
│   ├── ui/                       # Presentation layer
│   │   ├── components/           # React components
│   │   ├── hooks/                # Custom React hooks
│   │   ├── providers/            # Context providers
│   │   └── App.tsx               # Root component
│   ├── core/                     # Business logic
│   │   ├── api/                  # TanStack Query queries/mutations
│   │   ├── db/                   # Database access functions
│   │   └── types/                # TypeScript types
│   └── main.tsx                  # Entry point
├── src-tauri/                    # Backend (Rust)
│   └── src/
│       ├── lib.rs                # Tauri initialization
│       ├── commands.rs           # Tauri commands (IPC)
│       └── migrations.rs         # SQLite schema migrations
└── index.html                    # Vite entry
my-app/
├── src/                          # 前端(React/TypeScript)
│   ├── ui/                       # 展示层
│   │   ├── components/           # React组件
│   │   ├── hooks/                # 自定义React Hooks
│   │   ├── providers/            # Context提供者
│   │   └── App.tsx               # 根组件
│   ├── core/                     # 业务逻辑
│   │   ├── api/                  # TanStack Query查询/变更
│   │   ├── db/                   # 数据库访问函数
│   │   └── types/                # TypeScript类型定义
│   └── main.tsx                  # 入口文件
├── src-tauri/                    # 后端(Rust)
│   └── src/
│       ├── lib.rs                # Tauri初始化
│       ├── commands.rs           # Tauri命令(IPC)
│       └── migrations.rs         # SQLite schema迁移
└── index.html                    # Vite入口

Data Flow Pattern

数据流模式

1. Define Types

1. 定义类型

typescript
// src/core/types/Note.ts
export interface Note {
    id: string;
    title: string;
    content: string;
    createdAt: Date;
    updatedAt: Date;
}
typescript
// src/core/types/Note.ts
export interface Note {
    id: string;
    title: string;
    content: string;
    createdAt: Date;
    updatedAt: Date;
}

2. Create Database Functions

2. 创建数据库函数

typescript
// src/core/db/notes.ts
import { db } from "./connection";
import type { Note } from "../types/Note";

export async function fetchNotes(): Promise<Note[]> {
    const rows = await db.select<NoteRow[]>("SELECT * FROM notes ORDER BY updated_at DESC");
    return rows.map(rowToNote);
}

export async function createNote(note: Omit<Note, "id" | "createdAt" | "updatedAt">): Promise<Note> {
    const id = crypto.randomUUID();
    const now = new Date().toISOString();

    await db.execute(
        "INSERT INTO notes (id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
        [id, note.title, note.content, now, now]
    );

    return { id, ...note, createdAt: new Date(now), updatedAt: new Date(now) };
}

// Helper to convert DB row to domain object
function rowToNote(row: NoteRow): Note {
    return {
        id: row.id,
        title: row.title,
        content: row.content,
        createdAt: new Date(row.created_at),
        updatedAt: new Date(row.updated_at),
    };
}
typescript
// src/core/db/notes.ts
import { db } from "./connection";
import type { Note } from "../types/Note";

export async function fetchNotes(): Promise<Note[]> {
    const rows = await db.select<NoteRow[]>("SELECT * FROM notes ORDER BY updated_at DESC");
    return rows.map(rowToNote);
}

export async function createNote(note: Omit<Note, "id" | "createdAt" | "updatedAt">): Promise<Note> {
    const id = crypto.randomUUID();
    const now = new Date().toISOString();

    await db.execute(
        "INSERT INTO notes (id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
        [id, note.title, note.content, now, now]
    );

    return { id, ...note, createdAt: new Date(now), updatedAt: new Date(now) };
}

// Helper to convert DB row to domain object
function rowToNote(row: NoteRow): Note {
    return {
        id: row.id,
        title: row.title,
        content: row.content,
        createdAt: new Date(row.created_at),
        updatedAt: new Date(row.updated_at),
    };
}

3. Create TanStack Query Layer

3. 创建TanStack Query层

typescript
// src/core/api/notes.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { fetchNotes, createNote } from "../db/notes";
import type { Note } from "../types/Note";

// Query keys - hierarchical for easy invalidation
export const noteKeys = {
    all: ["notes"] as const,
    lists: () => [...noteKeys.all, "list"] as const,
    detail: (id: string) => [...noteKeys.all, "detail", id] as const,
};

// Queries
export const noteQueries = {
    list: () => ({
        queryKey: noteKeys.lists(),
        queryFn: fetchNotes,
        staleTime: Infinity,  // Data is local, no need to refetch
    }),
};

// Hooks
export function useNotes() {
    return useQuery(noteQueries.list());
}

export function useCreateNote() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: createNote,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: noteKeys.lists() });
        },
    });
}
typescript
// src/core/api/notes.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { fetchNotes, createNote } from "../db/notes";
import type { Note } from "../types/Note";

// Query keys - hierarchical for easy invalidation
export const noteKeys = {
    all: ["notes"] as const,
    lists: () => [...noteKeys.all, "list"] as const,
    detail: (id: string) => [...noteKeys.all, "detail", id] as const,
};

// Queries
export const noteQueries = {
    list: () => ({
        queryKey: noteKeys.lists(),
        queryFn: fetchNotes,
        staleTime: Infinity,  // Data is local, no need to refetch
    }),
};

// Hooks
export function useNotes() {
    return useQuery(noteQueries.list());
}

export function useCreateNote() {
    const queryClient = useQueryClient();

    return useMutation({
        mutationFn: createNote,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: noteKeys.lists() });
        },
    });
}

4. Use in Components

4. 在组件中使用

tsx
// src/ui/components/NoteList.tsx
import { useNotes, useCreateNote } from "@core/api/notes";

export function NoteList() {
    const { data: notes, isLoading } = useNotes();
    const createNote = useCreateNote();

    if (isLoading) return <div>Loading...</div>;

    return (
        <div>
            <button
                onClick={() => createNote.mutate({ title: "New Note", content: "" })}
            >
                Add Note
            </button>

            {notes?.map(note => (
                <div key={note.id}>{note.title}</div>
            ))}
        </div>
    );
}
tsx
// src/ui/components/NoteList.tsx
import { useNotes, useCreateNote } from "@core/api/notes";

export function NoteList() {
    const { data: notes, isLoading } = useNotes();
    const createNote = useCreateNote();

    if (isLoading) return <div>加载中...</div>;

    return (
        <div>
            <button
                onClick={() => createNote.mutate({ title: "新建笔记", content: "" })}
            >
                添加笔记
            </button>

            {notes?.map(note => (
                <div key={note.id}>{note.title}</div>
            ))}
        </div>
    );
}

Database Patterns

数据库模式

Migrations

迁移

rust
// src-tauri/src/migrations.rs
pub fn get_migrations() -> Vec<Migration> {
    vec![
        Migration {
            version: 1,
            description: "create_notes_table",
            sql: r#"
                CREATE TABLE IF NOT EXISTS notes (
                    id TEXT PRIMARY KEY,
                    title TEXT NOT NULL,
                    content TEXT NOT NULL DEFAULT '',
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                );
            "#,
            kind: MigrationKind::Up,
        },
        // Add more migrations as your schema evolves
    ]
}
rust
// src-tauri/src/migrations.rs
pub fn get_migrations() -> Vec<Migration> {
    vec![
        Migration {
            version: 1,
            description: "create_notes_table",
            sql: r#"
                CREATE TABLE IF NOT EXISTS notes (
                    id TEXT PRIMARY KEY,
                    title TEXT NOT NULL,
                    content TEXT NOT NULL DEFAULT '',
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL
                );
            "#,
            kind: MigrationKind::Up,
        },
        // 随着schema演进添加更多迁移
    ]
}

Database Connection

数据库连接

typescript
// src/core/db/connection.ts
import Database from "@tauri-apps/plugin-sql";

let database: Database | null = null;

export async function initDatabase(): Promise<Database> {
    if (!database) {
        database = await Database.load("sqlite:app.db");
    }
    return database;
}

export { database as db };
typescript
// src/core/db/connection.ts
import Database from "@tauri-apps/plugin-sql";

let database: Database | null = null;

export async function initDatabase(): Promise<Database> {
    if (!database) {
        database = await Database.load("sqlite:app.db");
    }
    return database;
}

export { database as db };

Best Practices

最佳实践

  1. No foreign keys - They're hard to remove and cause migration headaches
  2. Use TEXT for dates - Store as ISO 8601 strings, convert in TypeScript
  3. Prefer
    undefined
    over
    null
    - Convert DB nulls:
    value ?? undefined
  4. Use UUIDs for IDs -
    crypto.randomUUID()
    works everywhere
  1. 不使用外键 - 外键难以移除,会导致迁移问题
  2. 用TEXT存储日期 - 以ISO 8601字符串存储,在TypeScript中转换
  3. 优先使用
    undefined
    而非
    null
    - 转换数据库null值:
    value ?? undefined
  4. 用UUID作为ID -
    crypto.randomUUID()
    可在所有环境使用

Tauri Commands (IPC)

Tauri命令(IPC)

For operations that need native capabilities:
对于需要原生能力的操作:

Define in Rust

在Rust中定义

rust
// src-tauri/src/commands.rs
use tauri::command;

#[command]
pub fn get_app_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

#[command]
pub async fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path)
        .map_err(|e| e.to_string())
}
rust
// src-tauri/src/commands.rs
use tauri::command;

#[command]
pub fn get_app_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

#[command]
pub async fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path)
        .map_err(|e| e.to_string())
}

Register in lib.rs

在lib.rs中注册

rust
// src-tauri/src/lib.rs
mod commands;

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            commands::get_app_version,
            commands::read_file,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
rust
// src-tauri/src/lib.rs
mod commands;

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            commands::get_app_version,
            commands::read_file,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Call from React

在React中调用

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

// Simple call
const version = await invoke<string>("get_app_version");

// With arguments
const content = await invoke<string>("read_file", { path: "/path/to/file" });
typescript
import { invoke } from "@tauri-apps/api/core";

// 简单调用
const version = await invoke<string>("get_app_version");

// 带参数调用
const content = await invoke<string>("read_file", { path: "/path/to/file" });

State Management

状态管理

When to Use What

选型指南

State TypeSolution
Server/DB stateTanStack Query
App-wide UI stateReact Context
Component stateuseState/useReducer
Form stateReact Hook Form or local state
状态类型解决方案
服务器/数据库状态TanStack Query
全局UI状态React Context
组件状态useState/useReducer
表单状态React Hook Form 或本地状态

Context Pattern

Context模式

tsx
// src/ui/providers/AppProvider.tsx
import { createContext, useContext, useState, type ReactNode } from "react";

interface AppState {
    sidebarOpen: boolean;
    toggleSidebar: () => void;
}

const AppContext = createContext<AppState | null>(null);

export function AppProvider({ children }: { children: ReactNode }) {
    const [sidebarOpen, setSidebarOpen] = useState(true);

    return (
        <AppContext.Provider value={{
            sidebarOpen,
            toggleSidebar: () => setSidebarOpen(prev => !prev),
        }}>
            {children}
        </AppContext.Provider>
    );
}

export function useApp() {
    const context = useContext(AppContext);
    if (!context) throw new Error("useApp must be used within AppProvider");
    return context;
}
tsx
// src/ui/providers/AppProvider.tsx
import { createContext, useContext, useState, type ReactNode } from "react";

interface AppState {
    sidebarOpen: boolean;
    toggleSidebar: () => void;
}

const AppContext = createContext<AppState | null>(null);

export function AppProvider({ children }: { children: ReactNode }) {
    const [sidebarOpen, setSidebarOpen] = useState(true);

    return (
        <AppContext.Provider value={{
            sidebarOpen,
            toggleSidebar: () => setSidebarOpen(prev => !prev),
        }}>
            {children}
        </AppContext.Provider>
    );
}

export function useApp() {
    const context = useContext(AppContext);
    if (!context) throw new Error("useApp必须在AppProvider内部使用");
    return context;
}

Styling

样式

Tailwind + Radix UI

Tailwind + Radix UI

tsx
import * as Dialog from "@radix-ui/react-dialog";

export function Modal({ children, open, onOpenChange }) {
    return (
        <Dialog.Root open={open} onOpenChange={onOpenChange}>
            <Dialog.Portal>
                <Dialog.Overlay className="fixed inset-0 bg-black/50" />
                <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 shadow-xl">
                    {children}
                </Dialog.Content>
            </Dialog.Portal>
        </Dialog.Root>
    );
}
tsx
import * as Dialog from "@radix-ui/react-dialog";

export function Modal({ children, open, onOpenChange }) {
    return (
        <Dialog.Root open={open} onOpenChange={onOpenChange}>
            <Dialog.Portal>
                <Dialog.Overlay className="fixed inset-0 bg-black/50" />
                <Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 shadow-xl">
                    {children}
                </Dialog.Content>
            </Dialog.Portal>
        </Dialog.Root>
    );
}

Theme Support

主题支持

tsx
// src/ui/providers/ThemeProvider.tsx
export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState<"light" | "dark">("light");

    useEffect(() => {
        document.documentElement.classList.toggle("dark", theme === "dark");
    }, [theme]);

    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}
tsx
// src/ui/providers/ThemeProvider.tsx
export function ThemeProvider({ children }) {
    const [theme, setTheme] = useState<"light" | "dark">("light");

    useEffect(() => {
        document.documentElement.classList.toggle("dark", theme === "dark");
    }, [theme]);

    return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
            {children}
        </ThemeContext.Provider>
    );
}

Path Aliases

路径别名

Configure in
tsconfig.json
:
json
{
  "compilerOptions": {
    "paths": {
      "@ui/*": ["./src/ui/*"],
      "@core/*": ["./src/core/*"],
      "@/*": ["./src/*"]
    }
  }
}
Use throughout:
typescript
import { Button } from "@ui/components/Button";
import { useNotes } from "@core/api/notes";
tsconfig.json
中配置:
json
{
  "compilerOptions": {
    "paths": {
      "@ui/*": ["./src/ui/*"],
      "@core/*": ["./src/core/*"],
      "@/*": ["./src/*"]
    }
  }
}
在代码中使用:
typescript
import { Button } from "@ui/components/Button";
import { useNotes } from "@core/api/notes";

Common Tauri Plugins

常用Tauri插件

PluginPurpose
tauri-plugin-sql
SQLite database
tauri-plugin-store
Key-value storage
tauri-plugin-fs
File system access
tauri-plugin-dialog
Native dialogs
tauri-plugin-clipboard
Clipboard access
tauri-plugin-notification
System notifications
tauri-plugin-updater
Auto-updates
tauri-plugin-global-shortcut
Global keyboard shortcuts
插件用途
tauri-plugin-sql
SQLite数据库
tauri-plugin-store
键值对存储
tauri-plugin-fs
文件系统访问
tauri-plugin-dialog
原生对话框
tauri-plugin-clipboard
剪贴板访问
tauri-plugin-notification
系统通知
tauri-plugin-updater
自动更新
tauri-plugin-global-shortcut
全局键盘快捷键

macOS-Specific Native Features

macOS专属原生功能

Tauri can access powerful macOS-specific APIs through Rust. Here are patterns for common features:
Tauri可通过Rust访问强大的macOS专属API。以下是常见功能的实现模式:

Spotlight-Style Panels

聚焦式面板

Convert a window into a floating panel that behaves like Spotlight:
rust
// Cargo.toml
[target.'cfg(target_os = "macos")'.dependencies]
tauri-nspanel = "0.1"

// src-tauri/src/lib.rs
#[cfg(target_os = "macos")]
use tauri_nspanel::{panel_delegate, WebviewWindowExt};

// Convert window to panel
#[cfg(target_os = "macos")]
fn setup_panel(window: &tauri::WebviewWindow) {
    let panel = window.to_panel().unwrap();

    // Floating above other windows
    panel.set_level(NSMainMenuWindowLevel + 1);

    // Non-activating (doesn't steal focus)
    panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);

    // Works on all spaces/desktops
    panel.set_collection_behavior(
        NSWindowCollectionBehaviorCanJoinAllSpaces |
        NSWindowCollectionBehaviorFullScreenAuxiliary
    );
}
将窗口转换为类似Spotlight的悬浮面板:
rust
// Cargo.toml
[target.'cfg(target_os = "macos")'.dependencies]
tauri-nspanel = "0.1"

// src-tauri/src/lib.rs
#[cfg(target_os = "macos")]
use tauri_nspanel::{panel_delegate, WebviewWindowExt};

// 转换窗口为面板
#[cfg(target_os = "macos")]
fn setup_panel(window: &tauri::WebviewWindow) {
    let panel = window.to_panel().unwrap();

    // 悬浮于其他窗口之上
    panel.set_level(NSMainMenuWindowLevel + 1);

    // 非激活模式(不抢夺焦点)
    panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);

    // 在所有空间/桌面可用
    panel.set_collection_behavior(
        NSWindowCollectionBehaviorCanJoinAllSpaces |
        NSWindowCollectionBehaviorFullScreenAuxiliary
    );
}

Vibrancy/Glassmorphism

毛玻璃效果

Add macOS blur effects:
rust
use tauri::window::Effect;

window.set_effects(
    EffectsBuilder::default()
        .effect(Effect::Popover)  // or HudWindow, Sidebar, etc.
        .state(EffectState::Active)
        .build()
);
添加macOS模糊效果:
rust
use tauri::window::Effect;

window.set_effects(
    EffectsBuilder::default()
        .effect(Effect::Popover)  // 或HudWindow、Sidebar等
        .state(EffectState::Active)
        .build()
);

Global Shortcuts

全局快捷键

rust
// src-tauri/src/lib.rs
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};

app.handle().plugin(
    tauri_plugin_global_shortcut::Builder::new()
        .with_handler(move |_app, shortcut, event| {
            if shortcut == &Shortcut::new(Some(Modifiers::ALT), Code::Space) {
                if event.state == ShortcutState::Pressed {
                    // Toggle your quick panel
                    toggle_panel();
                }
            }
        })
        .build(),
)?;
rust
// src-tauri/src/lib.rs
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};

app.handle().plugin(
    tauri_plugin_global_shortcut::Builder::new()
        .with_handler(move |_app, shortcut, event| {
            if shortcut == &Shortcut::new(Some(Modifiers::ALT), Code::Space) {
                if event.state == ShortcutState::Pressed {
                    // 切换快速面板
                    toggle_panel();
                }
            }
        })
        .build(),
)?;

Screenshot Capture

截图捕获

rust
#[tauri::command]
async fn capture_screen() -> Result<String, String> {
    let temp_path = std::env::temp_dir().join("screenshot.png");

    // Use native screencapture on macOS
    let output = std::process::Command::new("screencapture")
        .args(["-i", "-x", temp_path.to_str().unwrap()])  // -i: interactive, -x: no sound
        .output()
        .map_err(|e| e.to_string())?;

    if output.status.success() {
        let bytes = std::fs::read(&temp_path).map_err(|e| e.to_string())?;
        Ok(base64::encode(&bytes))
    } else {
        Err("Screenshot cancelled".to_string())
    }
}
rust
#[tauri::command]
async fn capture_screen() -> Result<String, String> {
    let temp_path = std::env::temp_dir().join("screenshot.png");

    // 使用macOS原生screencapture
    let output = std::process::Command::new("screencapture")
        .args(["-i", "-x", temp_path.to_str().unwrap()])  // -i: 交互式, -x: 无声音
        .output()
        .map_err(|e| e.to_string())?;

    if output.status.success() {
        let bytes = std::fs::read(&temp_path).map_err(|e| e.to_string())?;
        Ok(base64::encode(&bytes))
    } else {
        Err("截图已取消".to_string())
    }
}

Permission Handling

权限处理

Check and request permissions:
rust
#[tauri::command]
fn check_screen_recording_permission() -> bool {
    #[cfg(target_os = "macos")]
    {
        // CGPreflightScreenCaptureAccess returns true if permission granted
        unsafe {
            core_graphics::display::CGPreflightScreenCaptureAccess()
        }
    }
    #[cfg(not(target_os = "macos"))]
    true
}

#[tauri::command]
fn open_privacy_settings() {
    #[cfg(target_os = "macos")]
    {
        std::process::Command::new("open")
            .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
            .spawn()
            .ok();
    }
}
检查并请求权限:
rust
#[tauri::command]
fn check_screen_recording_permission() -> bool {
    #[cfg(target_os = "macos")]
    {
        // CGPreflightScreenCaptureAccess返回true表示权限已授予
        unsafe {
            core_graphics::display::CGPreflightScreenCaptureAccess()
        }
    }
    #[cfg(not(target_os = "macos"))]
    true
}

#[tauri::command]
fn open_privacy_settings() {
    #[cfg(target_os = "macos")]
    {
        std::process::Command::new("open")
            .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")
            .spawn()
            .ok();
    }
}

Menu Bar Integration

菜单栏集成

rust
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};

let menu = Menu::with_items(&app, &[
    &Submenu::with_items(&app, "File", true, &[
        &MenuItem::with_id(&app, "new", "New", true, Some("CmdOrCtrl+N"))?,
        &PredefinedMenuItem::separator(&app)?,
        &MenuItem::with_id(&app, "quit", "Quit", true, Some("CmdOrCtrl+Q"))?,
    ])?,
])?;

app.set_menu(menu)?;

// Handle menu events
app.on_menu_event(|app, event| {
    match event.id().as_ref() {
        "new" => { /* handle new */ }
        "quit" => app.exit(0),
        _ => {}
    }
});
rust
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};

let menu = Menu::with_items(&app, &[
    &Submenu::with_items(&app, "文件", true, &[
        &MenuItem::with_id(&app, "new", "新建", true, Some("CmdOrCtrl+N"))?,
        &PredefinedMenuItem::separator(&app)?,
        &MenuItem::with_id(&app, "quit", "退出", true, Some("CmdOrCtrl+Q"))?,
    ])?,
])?;

app.set_menu(menu)?;

// 处理菜单事件
app.on_menu_event(|app, event| {
    match event.id().as_ref() {
        "new" => { /* 处理新建操作 */ }
        "quit" => app.exit(0),
        _ => {}
    }
});

Auto-Updates

自动更新

Auto-updates are essential for desktop apps. Tauri provides a built-in updater plugin.
自动更新是桌面应用的必备功能。Tauri提供了内置的更新插件。

Quick Setup

快速设置

  1. Generate signing keys:
bash
pnpm tauri signer generate -w ~/.tauri/myapp.key
  1. Configure tauri.conf.json:
json
{
  "bundle": {
    "createUpdaterArtifacts": true
  },
  "plugins": {
    "updater": {
      "pubkey": "YOUR_PUBLIC_KEY",
      "endpoints": [
        "https://your-update-server.com/{{target}}-{{arch}}/{{current_version}}"
      ]
    }
  }
}
  1. Check for updates in React:
typescript
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";

const update = await check();
if (update) {
    await update.downloadAndInstall();
    await relaunch();
}
  1. 生成签名密钥:
bash
pnpm tauri signer generate -w ~/.tauri/myapp.key
  1. 配置tauri.conf.json:
json
{
  "bundle": {
    "createUpdaterArtifacts": true
  },
  "plugins": {
    "updater": {
      "pubkey": "YOUR_PUBLIC_KEY",
      "endpoints": [
        "https://your-update-server.com/{{target}}-{{arch}}/{{current_version}}"
      ]
    }
  }
}
  1. 在React中检查更新:
typescript
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";

const update = await check();
if (update) {
    await update.downloadAndInstall();
    await relaunch();
}

Best Practices

最佳实践

DoDon't
Download silently in backgroundBlock UI during download
Let user choose when to restartAuto-restart without warning
Poll every 5 minutesOnly check on startup
Handle errors gracefullyCrash on update failure
Skip update checks in dev modeAnnoy developers with prompts
建议做法避免做法
在后台静默下载下载时阻塞UI
让用户选择重启时机无提示自动重启
每5分钟轮询一次仅在启动时检查
优雅处理错误更新失败时崩溃
开发模式下跳过更新检查用更新提示打扰开发者

Hosting Options

托管选项

  • CrabNebula - Purpose-built for Tauri, zero config
  • GitHub Releases - Free, integrates with CI
  • Self-hosted - Full control, more work
For complete implementation details including production-ready code, CI/CD setup, and security considerations, see references/auto-updates.md.
  • CrabNebula - 专为Tauri打造,零配置
  • GitHub Releases - 免费,与CI集成
  • 自托管 - 完全可控,工作量更大
如需包含生产就绪代码、CI/CD设置及安全考量的完整实现细节,请查看**references/auto-updates.md**。

Building for Production

生产构建

bash
undefined
bash
undefined

Development

开发模式

pnpm tauri dev
pnpm tauri dev

Production build

生产构建

pnpm tauri build
pnpm tauri build

Output: src-tauri/target/release/bundle/

输出目录: src-tauri/target/release/bundle/

undefined
undefined

Further Reading

扩展阅读