使用 Claude Code 开发 Tauri 桌面应用:Vite、Rust 命令与权限设计
用 Claude Code 构建 Tauri v2 桌面应用,覆盖 Vite、Rust 命令、invoke、文件权限、测试与审查提示词。
Tauri 可以把 Web UI 打包成 Windows、macOS 和 Linux 桌面应用。界面可以继续使用 React、Svelte 或其他前端技术,系统级能力则放在 Rust 侧实现。对 Claude Code 来说,这种边界很适合拆任务:一次只改一个界面、一个 Rust command、一个 capability 或一个测试。
本文以 Tauri v2 的本地笔记应用为例,讲清楚什么时候应该选 Tauri,如何用 Vite + React 或 Svelte 初始化,如何写 Rust command,如何从前端用 invoke 调用,如何限制文件系统权限,最后如何用 Claude Code 做权限审查。
本文核对的官方资料包括 Tauri Create a Project、Calling Rust from the Frontend、Tauri Capabilities、File System plugin、Vite Getting Started、Cargo test 和 Claude Code setup。如果想先补 Rust 基础,可以看 Claude Code Rust 开发指南;如果在 Electron 与 Tauri 之间选择,可以对照 Electron 桌面开发。
什么时候选择 Tauri
适合 Tauri 的场景,是你需要本地应用形态,同时又想保留 Web UI 的开发效率。比如内部 CSV 转换器、私有笔记、日志查看器、离线录入工具、只在公司电脑上运行的小工具。这些应用往往要读取本地文件、写入本地数据、调用系统对话框,普通网站不适合直接承担这些权限。
不适合 Tauri 的场景也很明确。如果产品只是登录后访问服务器 API,没有本地文件、离线能力或系统集成,那么普通 Web 应用或 PWA 更简单。Tauri 虽然轻,但你仍然要处理 Rust 工具链、打包、签名、更新、应用 ID 和不同操作系统的差异。
给 Claude Code 的第一条提示词建议这样写:
我们要做一个 Tauri v2 本地笔记应用。
先不要实现代码。
请把任务拆成 React UI、Rust command、capability、build/test 四层。
文件访问只能限制在 app data 目录内,不要建议任意路径读写。
这个提示词的目的,是先让代理理解安全边界,而不是直接生成一个权限过大的应用。
架构边界
Tauri 的核心模式是:前端不直接访问操作系统,而是通过 @tauri-apps/api/core 的 invoke 调用 Rust command。Rust command 负责验证输入、访问本地资源,并返回结构化结果。
flowchart LR
UI["React 或 Svelte UI"] --> Invoke["invoke from @tauri-apps/api/core"]
Invoke --> Command["Rust command"]
Command --> Guard["路径白名单与验证"]
Guard --> AppData["app data 目录"]
Command --> Result["类型化结果"]
Result --> UI
Capability["Tauri capability"] --> UI
初学者最容易忽略的一点是:Tauri capability 主要限制前端能调用哪些 Tauri API 和插件权限,但不会自动保护你自己写的 Rust command。如果 Rust command 接收任意路径并写入,capability 再窄也挡不住这个 command。正确做法是 capability 和 Rust 侧路径验证都要审查。
用 React 或 Svelte 初始化
新项目优先使用官方的 create-tauri-app。在交互中选择 React 或 Svelte,并选择 TypeScript。
npm create tauri-app@latest taskdesk
cd taskdesk
npm install
npm run tauri dev
已有 Vite 项目时,可以先创建 Vite,再初始化 Tauri。Vite 官方文档说明 npm create vite@latest 是标准入口,并且当前 Vite 需要 Node.js 20.19+ 或 22.12+。如果 Node 太旧,错误可能看起来像 Tauri 问题。
node --version
npm create vite@latest taskdesk -- --template react-ts
cd taskdesk
npm install
npm install -D @tauri-apps/cli@latest
npm install @tauri-apps/api@latest
npx tauri init
npx tauri dev
Svelte 只需要换模板:
npm create vite@latest taskdesk -- --template svelte-ts
初始化后,先让 Claude Code 读取生成文件,而不是直接改:
请阅读 package.json、tauri.conf.json、src-tauri/src/lib.rs。
总结当前脚本、目录和 Tauri 入口。
不要修改任何文件。
这样可以避免把旧文章里的目录结构硬套到最新模板上。
Tauri 配置示例
tauri.conf.json 连接 Vite dev server、前端构建产物、窗口配置和 capability。下面是一个适合 Vite 的最小例子。
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "TaskDesk",
"version": "0.1.0",
"identifier": "com.example.taskdesk",
"build": {
"beforeDevCommand": "npm run dev",
"devUrl": "http://localhost:5173",
"beforeBuildCommand": "npm run build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "TaskDesk",
"width": 1000,
"height": 700
}
],
"security": {
"capabilities": ["main-capability"]
}
},
"bundle": {
"active": true,
"targets": "all"
}
}
审查时重点看三件事:identifier 不要停留在示例值;devUrl 要和 Vite 端口一致;frontendDist 要指向真实的构建输出。
Rust command 与路径验证
下面的 command 支持读取、写入和列出笔记。示例把路径限制在 app data 内,并拒绝绝对路径、.. 和平台前缀。
// src-tauri/src/note_commands.rs
use serde::Serialize;
use std::{
fs,
path::{Component, Path, PathBuf},
};
use tauri::{AppHandle, Manager};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoteFile {
name: String,
path: String,
bytes: u64,
is_dir: bool,
}
fn reject_unsafe_relative(path: &Path) -> Result<(), String> {
for component in path.components() {
match component {
Component::Normal(_) | Component::CurDir => {}
_ => return Err("use a relative path inside app data".to_string()),
}
}
Ok(())
}
fn app_data_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
.app_data_dir()
.map_err(|error| format!("failed to get app data dir: {error}"))?;
fs::create_dir_all(&root).map_err(|error| format!("failed to create app data dir: {error}"))?;
root.canonicalize()
.map_err(|error| format!("failed to resolve app data dir: {error}"))
}
fn existing_path(app: &AppHandle, relative: &str) -> Result<PathBuf, String> {
let root = app_data_root(app)?;
let requested = Path::new(relative);
reject_unsafe_relative(requested)?;
let full = root
.join(requested)
.canonicalize()
.map_err(|error| format!("path does not exist: {error}"))?;
if !full.starts_with(&root) {
return Err("path escapes app data".to_string());
}
Ok(full)
}
#[tauri::command]
pub fn read_note(app: AppHandle, path: String) -> Result<String, String> {
let safe_path = existing_path(&app, &path)?;
fs::read_to_string(safe_path).map_err(|error| format!("failed to read note: {error}"))
}
实际项目还要加 write_note 和 list_notes,并在 lib.rs 中注册。
// src-tauri/src/lib.rs
mod note_commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
note_commands::read_note
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
前端 invoke 包装
前端建议写一个很薄的 API 包装层,不要在多个组件里重复 command 名称。
// src/lib/notesApi.ts
import { invoke } from "@tauri-apps/api/core";
export type NoteFile = {
name: string;
path: string;
bytes: number;
isDir: boolean;
};
export const notesApi = {
read(path: string) {
return invoke<string>("read_note", { path });
},
write(path: string, content: string) {
return invoke<void>("write_note", { path, content });
},
list(dir = ".") {
return invoke<NoteFile[]>("list_notes", { dir });
},
};
React 页面可以先保持很小:
import { useState } from "react";
import { notesApi } from "./lib/notesApi";
export default function App() {
const [content, setContent] = useState("");
const [message, setMessage] = useState("Ready");
async function saveNote() {
await notesApi.write("daily-note.txt", content);
setMessage("Saved");
}
return (
<main>
<textarea value={content} onChange={(event) => setContent(event.target.value)} />
<button onClick={saveNote}>Save</button>
<p>{message}</p>
</main>
);
}
权限与 capability
如果前端直接使用 File System plugin,就要在 src-tauri/capabilities 中写清楚范围。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Main window permissions for TaskDesk.",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-app-read-recursive",
"fs:allow-app-write-recursive"
]
}
本文示例通过 Rust command 操作文件,所以不需要为了保存笔记而给前端增加下载目录或文档目录的广泛权限。让 Claude Code 审查权限时,可以使用:
只审查,不修改。
检查 src-tauri/capabilities 和 src-tauri/src/note_commands.rs。
列出所有前端可调用 API 和 Rust command。
说明每个 API 能触达哪些文件路径。
标记绝对路径、..、过宽 wildcard、app data 外写入。
给出能满足功能的最小权限方案。
构建、测试与真实用途
把检查拆开执行:
npm run build
cd src-tauri
cargo test
cd ..
npm run tauri build
npm run build 验证 Vite,cargo test 验证 Rust,npm run tauri build 验证桌面打包。路径验证可以先写纯 Rust 测试:
#[cfg(test)]
mod tests {
use super::reject_unsafe_relative;
use std::path::Path;
#[test]
fn rejects_parent_directory() {
assert!(reject_unsafe_relative(Path::new("../secret.txt")).is_err());
}
}
真实用途至少有三类:本地笔记或日报、CSV/Markdown 转换器、开发者日志查看器。第四类是离线录入工具,比如活动签到、门店盘点、现场检查。它们共同点是需要本地数据和清晰权限边界。
常见坑也很具体:把 capability 当成 Rust command 的安全保证;让 Claude Code 一次生成整个应用;开发配置带到生产;只在一个操作系统上测试;为了省事给前端加过宽文件权限。
变现 CTA 与试用结果
如果要把这套流程变成团队习惯,可以从 ClaudeCodeLab 产品与模板 开始,把权限审查、CLAUDE.md、构建命令和 review prompt 固定下来。团队落地或真实仓库评审,可以继续看 Claude Code 培训与咨询。
本文背后的试用顺序里,最稳的是先写 Rust 路径边界,再写 TypeScript invoke 包装,最后审查 capability。先做 UI 看起来更快,但保存位置和权限后来变化时,返工会更大。Tauri + Claude Code 的关键不是先把界面做漂亮,而是先把本地权限边界定清楚。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。