tauri-guide
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTauri + 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/)的场景:
| Capability | Why Rust? | Example |
|---|---|---|
| Global shortcuts | OS-level keyboard hooks | Alt+Space to open from anywhere |
| System tray | Native menu bar integration | Tray icon with menu |
| Window management | Native window APIs | Spotlight-style panels, vibrancy effects |
| Screenshot capture | OS screen capture APIs | |
| Deep links | URL scheme registration | |
| File system watching | Efficient OS notifications | Watch for file changes |
| Native dialogs | OS file pickers | Open/save dialogs |
| Clipboard | System clipboard access | Copy/paste integration |
| Notifications | System notification center | Push notifications |
| Auto-updates | Binary replacement | App update flow |
| Process spawning | Running external processes | MCP servers, CLI tools |
| Permission requests | OS permission dialogs | Screen recording access |
| 能力 | 为什么选Rust? | 示例 |
|---|---|---|
| 全局快捷键 | 系统级键盘钩子 | 按下Alt+Space从任意位置打开应用 |
| 系统托盘 | 原生菜单栏集成 | 带菜单的托盘图标 |
| 窗口管理 | 原生窗口API | 聚焦式面板、毛玻璃效果 |
| 截图捕获 | 系统屏幕捕获API | macOS上的 |
| 深度链接 | URL协议注册 | |
| 文件系统监听 | 高效的系统通知 | 监听文件变化 |
| 原生对话框 | 系统文件选择器 | 打开/保存对话框 |
| 剪贴板 | 系统剪贴板访问 | 复制/粘贴集成 |
| 通知 | 系统通知中心 | 推送通知 |
| 自动更新 | 二进制替换 | 应用更新流程 |
| 进程启动 | 运行外部进程 | MCP服务器、CLI工具 |
| 权限请求 | 系统权限对话框 | 屏幕录制权限申请 |
Use TypeScript (src/) For:
选择 TypeScript(src/)的场景:
| Capability | Why TypeScript? | Example |
|---|---|---|
| All UI | React ecosystem | Components, layouts |
| Business logic | Faster iteration | Validation, transformations |
| Database queries | SQL via plugin | CRUD operations |
| API calls | Fetch/streaming | AI provider integrations |
| State management | React/TanStack Query | App state, cache |
| Routing | React Router | Navigation |
| Forms | React patterns | User input |
| 能力 | 为什么选TypeScript? | 示例 |
|---|---|---|
| 所有UI界面 | React生态支持 | 组件、布局 |
| 业务逻辑 | 迭代速度更快 | 验证、数据转换 |
| 数据库查询 | 通过插件执行SQL | CRUD操作 |
| API调用 | Fetch/流式处理 | AI提供商集成 |
| 状态管理 | React/TanStack Query | 应用状态、缓存 |
| 路由 | React Router | 页面导航 |
| 表单 | React设计模式 | 用户输入 |
Decision Framework
判断框架
Ask these questions:
- Does it need OS-level access? → Rust
- Does it need to work when app isn't focused? → Rust
- Does it interact with native UI chrome? → Rust
- Is it purely data/UI logic? → TypeScript
- Does it need fast iteration? → TypeScript (hot reload)
可以通过以下问题快速判断:
- 是否需要系统级访问权限? → 选Rust
- 是否需要在应用未聚焦时运行? → 选Rust
- 是否需要与原生UI边框交互? → 选Rust
- 是否纯数据/UI逻辑? → 选TypeScript
- 是否需要快速迭代? → 选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 logicScreenshot annotation tool:
Rust:
- Screen capture (needs permissions)
- Window enumeration
- Save to disk
- Clipboard integration
TypeScript:
- Annotation canvas
- Tool palette
- Undo/redo
- Export options UIAI 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:
- 标注画布
- 工具面板
- 撤销/重做
- 导出选项UIAI聊天应用(启发本指南的应用):
Rust:
- 快速聊天全局快捷键
- 系统托盘
- 深度链接(chorus://chat/123)
- 上下文截图捕获
- 针对LLM限制的图片缩放
TypeScript:
- 聊天UI
- 消息流式传输
- 模型提供商集成
- 数据库查询
- 设置UIAnti-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 or elevated permissions
sudo - 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 entrymy-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
最佳实践
- No foreign keys - They're hard to remove and cause migration headaches
- Use TEXT for dates - Store as ISO 8601 strings, convert in TypeScript
- Prefer over
undefined- Convert DB nulls:nullvalue ?? undefined - Use UUIDs for IDs - works everywhere
crypto.randomUUID()
- 不使用外键 - 外键难以移除,会导致迁移问题
- 用TEXT存储日期 - 以ISO 8601字符串存储,在TypeScript中转换
- 优先使用而非
undefined- 转换数据库null值:nullvalue ?? undefined - 用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 Type | Solution |
|---|---|
| Server/DB state | TanStack Query |
| App-wide UI state | React Context |
| Component state | useState/useReducer |
| Form state | React 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.jsonjson
{
"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.jsonjson
{
"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插件
| Plugin | Purpose |
|---|---|
| SQLite database |
| Key-value storage |
| File system access |
| Native dialogs |
| Clipboard access |
| System notifications |
| Auto-updates |
| Global keyboard shortcuts |
| 插件 | 用途 |
|---|---|
| SQLite数据库 |
| 键值对存储 |
| 文件系统访问 |
| 原生对话框 |
| 剪贴板访问 |
| 系统通知 |
| 自动更新 |
| 全局键盘快捷键 |
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
快速设置
- Generate signing keys:
bash
pnpm tauri signer generate -w ~/.tauri/myapp.key- Configure tauri.conf.json:
json
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"pubkey": "YOUR_PUBLIC_KEY",
"endpoints": [
"https://your-update-server.com/{{target}}-{{arch}}/{{current_version}}"
]
}
}
}- 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();
}- 生成签名密钥:
bash
pnpm tauri signer generate -w ~/.tauri/myapp.key- 配置tauri.conf.json:
json
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"pubkey": "YOUR_PUBLIC_KEY",
"endpoints": [
"https://your-update-server.com/{{target}}-{{arch}}/{{current_version}}"
]
}
}
}- 在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
最佳实践
| Do | Don't |
|---|---|
| Download silently in background | Block UI during download |
| Let user choose when to restart | Auto-restart without warning |
| Poll every 5 minutes | Only check on startup |
| Handle errors gracefully | Crash on update failure |
| Skip update checks in dev mode | Annoy 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
undefinedbash
undefinedDevelopment
开发模式
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/
undefinedundefined