Tauri 入门与实践:用 Rust 构建你的下一个桌面应用

0 阅读12分钟

"我只是想做个桌面 App,为什么要下载半个 Chrome?"
——每一个被 Electron 支配过的前端开发者


一、为什么是 Tauri?(WHY)

Electron 的困境

前端开发者历史上一直有一个执念:用 Web 技术写桌面应用。Electron 把这个梦想变成了现实,同时也带来了一个副作用——你的"记事本"应用可能比 Visual Studio Code 还大。

Electron 的核心问题在于:它在每个应用里都打包了一个完整的 Chromium 浏览器引擎。这就像你搬家的时候,不是带走自己的家具,而是把整栋楼都拆了打包带走。结果就是:

  • 一个"Hello World" 应用动辄 100MB+
  • 内存占用堪比多开十几个标签页
  • 打包速度……去泡杯茶再回来吧

Tauri 的出现

2020 年,Tauri 横空出世,带来了一个截然不同的哲学:

复用系统已有的 WebView,而不是打包一个浏览器。

这听起来很简单,但效果是革命性的:

  • macOS 上用 WKWebView(Safari 内核)
  • Windows 上用 WebView2(Edge 内核)
  • Linux 上用 WebKitGTK

用户的操作系统里本来就有这些东西,我们何必再带一份?

于是,一个最小化的 Tauri 应用,体积可以 小于 600KB。对,你没看错,是 KB。

Tauri vs Electron 一眼看懂

对比维度ElectronTauri
最小包体积~50-100 MB< 600 KB
内存占用较高(独立 Chromium)较低(系统 WebView)
后端语言Node.js (JavaScript)Rust
安全性需要手动配置默认安全,权限最小化
跨平台桌面(Win/Mac/Linux)桌面 + 移动端(iOS/Android)
前端兼容性任意 Web 框架任意 Web 框架
首次编译速度慢(Rust 编译)

一句话总结:如果你想要一个体积小、性能好、安全性强的跨平台应用,Tauri 是当下最值得关注的选择。如果你对 Rust 望而却步,没关系——入门阶段你几乎不需要写多少 Rust。


二、Tauri 是什么?(WHAT)

核心架构

理解 Tauri 的关键,在于理解它的双进程架构

Tauri 双进程架构图

前端进程:运行在 WebView 中,负责 UI 渲染,可以是你熟悉的任何 Web 框架。

核心进程(Rust Core):负责与操作系统交互,处理文件、网络、系统托盘等"重活"。

IPC(进程间通信):两个进程之间的桥梁,也是整个安全模型的核心。前端通过 invoke 调用 Rust 函数,Rust 也可以向前端发送事件。

核心概念速览

1. Commands(命令)—— 前端调用 Rust

这是 Tauri 中最常用的通信方式。你在 Rust 里写一个函数,加上 #[tauri::command] 标记,前端就可以像调用普通 JavaScript 函数一样调用它。

2. Events(事件)—— 双向消息传递

Commands 是"请求-响应"模式,Events 则更像广播。适合那些不需要立即响应、但需要持续通知的场景,比如下载进度、后台任务状态更新等。

3. Plugins(插件)—— 开箱即用的功能

Tauri 官方维护了一套丰富的插件生态,覆盖了文件系统、Shell、通知、剪贴板、全局快捷键、系统托盘等常见需求,大多数情况下你不需要自己写底层代码。

4. Capabilities & Permissions(权限系统)

Tauri 2.0 引入了细粒度的权限控制系统。每个功能(Command)默认都是禁用的,你必须在配置中显式开启。这是 Tauri 安全设计哲学的集中体现:最小权限原则


三、如何上手?(HOW)

3.1 环境准备

磨刀不误砍柴工。Tauri 的环境配置比 Electron 稍微麻烦一点(Rust 的锅),但一旦搭好就永远受益。

第一步:安装系统依赖

Windows 用户需要:

  1. Microsoft C++ Build Tools:下载 Visual C++ Build Tools,安装时勾选 "Desktop development with C++"
  2. WebView2:访问 Microsoft WebView2 下载页,下载 "Evergreen Bootstrapper" 并安装(Win 11 默认已内置)

macOS 用户:

xcode-select --install

Linux(以 Debian/Ubuntu 为例):

sudo apt update
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
  libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev

第二步:安装 Rust

Rust 是 Tauri 的灵魂,必须安装。

# Linux/macOS
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

# Windows:直接去 https://rustup.rs 下载安装程序

安装完成后,重启终端,然后验证:

rustc --version
# rustc 1.78.0 (之类的版本号)
cargo --version
# cargo 1.78.0

小贴士:Rust 的首次编译非常慢,这是正常现象。换句话说,第一次 tauri dev 时,去泡杯茶,甚至可以考虑冲个咖啡。之后由于缓存,重新编译会快很多。

第三步:安装 Node.js(如果使用前端框架)

前往 Node.js 官网 下载 LTS 版本安装即可。


3.2 创建第一个 Tauri 项目

Tauri 提供了 create-tauri-app 脚手架工具,让你快速上手:

# 使用 npm
npm create tauri-app@latest

# 或 pnpm
pnpm create tauri-app

# 或 yarn
yarn create tauri-app

按照提示选择:

  • 项目名称
  • 前端框架(React、Vue、Svelte、Vanilla JS……随你喜欢)
  • 语言(TypeScript / JavaScript)

以 React + TypeScript 为例,最终你会得到这样的目录结构:

my-tauri-app/
├── src/                    # 前端代码(React)
│   ├── App.tsx
│   ├── main.tsx
│   └── styles.css
├── src-tauri/              # Rust 后端代码(核心所在)
│   ├── src/
│   │   ├── lib.rs          # 应用逻辑入口
│   │   └── main.rs         # 程序入口
│   ├── Cargo.toml          # Rust 依赖配置(相当于 package.json)
│   ├── Cargo.lock          # Rust 依赖锁文件(务必提交到 Git!)
│   ├── tauri.conf.json     # Tauri 核心配置
│   └── capabilities/       # 权限配置目录
│       └── default.json
├── package.json
└── index.html

3.3 启动开发模式

npm run tauri dev

第一次运行:Cargo 会下载并编译所有 Rust 依赖,耗时 5~10 分钟很正常。
后续运行:只编译变更部分,通常几秒钟。

启动后,一个原生窗口会弹出,显示你的前端页面。修改前端代码,页面热更新;修改 Rust 代码,应用自动重启

打开开发者工具:在窗口内右键 → "Inspect",或使用快捷键 Ctrl+Shift+I(Windows)。


3.4 核心开发:前端与 Rust 的通信

这是 Tauri 开发最核心的部分,掌握了通信机制,就掌握了 Tauri 的精髓。

场景一:前端调用 Rust 函数(Commands)

Rust 端src-tauri/src/lib.rs):

// 1. 定义命令:加上 #[tauri::command] 标记
#[tauri::command]
fn greet(name: &str) -> String {
    format!("你好,{}!来自 Rust 的问候。", name)
}

// 2. 注册命令:在 Builder 中声明
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

前端端src/App.tsx):

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

async function handleGreet() {
  // 调用 Rust 命令,参数使用 camelCase 命名
  const message = await invoke<string>('greet', { name: '世界' });
  console.log(message); // "你好,世界!来自 Rust 的问候。"
}

命名规则要注意:Rust 函数名用 snake_case(如 my_command),调用时仍用 snake_case;但参数名在前端传递时要用 camelCase(如 invokeMessage 对应 Rust 的 invoke_message)。

场景二:异步命令(推荐方式)

涉及 I/O 操作时,务必使用 async,否则会阻塞 UI:

use std::time::Duration;
use tokio::time::sleep;

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    // 模拟异步网络请求
    sleep(Duration::from_millis(100)).await;
    Ok(format!("从 {} 获取的数据", url))
}
invoke<string>('fetch_data', { url: 'https://example.com' })
  .then(data => console.log(data))
  .catch(err => console.error('出错了:', err));

场景三:错误处理

不要用 unwrap(),要优雅地返回错误:

// 推荐方式:使用 thiserror 定义语义化错误类型
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("文件未找到: {0}")]
    FileNotFound(String),
    #[error("IO 错误: {0}")]
    Io(#[from] std::io::Error),
}

// 手动实现 Serialize(Tauri 要求错误类型可序列化)
impl serde::Serialize for AppError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.to_string().as_ref())
    }
}

#[tauri::command]
async fn read_config() -> Result<String, AppError> {
    let content = std::fs::read_to_string("config.toml")?;
    Ok(content)
}
invoke<string>('read_config')
  .then(config => { /* 处理成功 */ })
  .catch((err: string) => {
    // err 就是 AppError 序列化后的字符串
    console.error('配置读取失败:', err);
  });

场景四:事件系统(后台任务通知)

当 Rust 后台需要主动推送消息给前端时(比如下载进度),用 Events:

Rust 端(使用 Channel 推送流式数据):

use tauri::ipc::Channel;

#[tauri::command]
async fn download_file(url: String, on_progress: Channel<u32>) {
    // 模拟下载进度
    for progress in [10, 30, 50, 80, 100] {
        tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
        on_progress.send(progress).unwrap();
    }
}

前端端

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

const onProgress = new Channel<number>();
onProgress.onmessage = (progress) => {
  console.log(`下载进度: ${progress}%`);
};

await invoke('download_file', {
  url: 'https://example.com/file.zip',
  onProgress,
});

也可以用更传统的事件监听方式:

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

// 监听事件(记得在组件卸载时取消监听!)
const unlisten = await listen<string>('task-status', (event) => {
  console.log('任务状态更新:', event.payload);
});

// 组件卸载时清理
onUnmounted(() => unlisten());

3.5 权限配置:必须踩的一关

Tauri 2.0 的权限系统是新手最容易卡住的地方。

默认情况下,所有能力都是关闭的。当你使用官方插件(如文件系统、Shell 等)时,必须在 capabilities/default.json 中明确授权:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "默认权限配置",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:read-all",
    "fs:write-all",
    "shell:open"
  ]
}

别被吓到:大多数情况下,跟着插件文档走,把需要的权限加上就好。这个机制的存在是为了防止恶意前端代码偷偷访问系统资源。


3.6 打包发布

npm run tauri build

这会在 src-tauri/target/release/bundle/ 下生成对应平台的安装包:

  • Windows:.msi.exe(NSIS 安装程序)
  • macOS:.dmg.app
  • Linux:.deb.rpm.AppImage

首次打包也很慢(Rust 全量编译),后续增量编译会快很多。


四、最佳实践

✅ 实践 1:Rust 做"重活",前端做 UI

Tauri 的设计哲学很清晰:前端负责视图,Rust 负责系统交互

不要尝试在前端用 fetch 访问本地文件,而是写一个 Rust Command 来读取;不要在前端做加密,而是用 Rust 来处理。

✅ 实践 2:异步命令是默认选择

凡是涉及 I/O(文件、网络、数据库)的操作,一律写成 async fn。同步命令会阻塞 Tauri 的主线程,导致界面卡顿。

✅ 实践 3:用 thiserror 处理错误

不要用 unwrap(),不要用 expect()(生产代码中),使用 thiserror 定义语义化的错误类型,让前端能收到有意义的错误信息。

✅ 实践 4:一定要提交 Cargo.lock

与 Node.js 项目的 package-lock.json 类似,src-tauri/Cargo.lock 确保了在不同机器上构建结果的一致性。务必将其提交到 Git,而不要加入 .gitignore

与此同时,src-tauri/target/ 目录应该被 .gitignore 忽略(它是编译产物,体积巨大)。

✅ 实践 5:善用官方插件,不要重复造轮子

Tauri 官方维护了大量高质量插件,覆盖了绝大多数常见场景:

插件功能
@tauri-apps/plugin-fs文件系统读写
@tauri-apps/plugin-shell执行 Shell 命令
@tauri-apps/plugin-notification系统通知
@tauri-apps/plugin-clipboard-manager剪贴板操作
@tauri-apps/plugin-global-shortcut全局快捷键
@tauri-apps/plugin-store持久化键值存储
@tauri-apps/plugin-sqlSQLite 数据库
@tauri-apps/plugin-httpHTTP 请求(绕过 CSP)
@tauri-apps/plugin-updater应用自动更新

✅ 实践 6:充分利用 CSP 保护应用安全

tauri.conf.json 中配置内容安全策略(CSP),可以有效防止 XSS 攻击:

{
  "app": {
    "security": {
      "csp": "default-src 'self'; script-src 'self'"
    }
  }
}

五、常见误区与坑

❌ 误区 1:以为不需要学 Rust

入门阶段确实可以少写 Rust,但随着应用复杂度提升,你不可避免地需要编写 Rust 代码。提前学习 Rust 基础知识(所有权、借用、Result/Option 类型)是明智之举。

推荐资源:The Rust Book(官方书,有中文版)

❌ 误区 2:忘记注册 Command

写好了 Rust Command,但忘记在 invoke_handler 中注册,然后前端一直报"command not found"。这是最高频的新手错误。

// ❌ 错误:写了函数但没注册
#[tauri::command]
fn my_command() {}

// ✅ 正确:必须在这里声明
.invoke_handler(tauri::generate_handler![my_command])

❌ 误区 3:在 Rust 中用 unwrap() 处处"爽"

开发阶段 unwrap() 可以快速跑通流程,但生产代码中一个 unwrap() 就可能让整个应用 panic 崩溃。养成用 ? 操作符 + Result 类型处理错误的习惯。

❌ 误区 4:参数命名不匹配导致的"神秘"错误

Rust 函数参数是 snake_case,前端传参是 camelCase,Tauri 会自动转换。但如果你的 Rust 参数是 user_name,前端就必须传 userName,写错了会得到"missing required argument"之类令人困惑的报错。

❌ 误区 5:用系统 WebView 导致样式兼容问题

Tauri 使用系统 WebView,这意味着在不同操作系统上,CSS/JS 的支持程度可能略有不同。

  • Windows 上是 Edge(Chromium 内核)✅
  • macOS 上是 Safari(WebKit 内核)⚠️ 部分 CSS 特性需要前缀
  • Linux 上是 WebKitGTK ⚠️ 版本可能较旧

解决办法:开发时在多个平台测试;使用 PostCSS/Autoprefixer 自动处理 CSS 前缀。

❌ 误区 6:混淆前端路由和窗口管理

在多窗口场景下,Tauri 的"窗口(Window)"和前端路由(如 React Router)是两个不同的概念。如果你只是要做页面跳转,用前端路由就好;如果需要独立的原生窗口(比如设置页面单独弹出),才需要用 Tauri 的窗口 API。


六、总结

回顾一下我们走过的路:

  • WHY:Tauri 用"复用系统 WebView"这一优雅设计,解决了 Electron 包体积臃肿、内存占用高的痛点,同时还默认提供了更好的安全性。

  • WHAT:Tauri 的核心是前端 WebView + Rust Core 的双进程架构,通过 IPC(Commands 和 Events)进行通信,并有细粒度的权限系统保障安全。

  • HOW:环境搭建 → 创建项目 → 理解目录结构 → 掌握 Command/Event 通信 → 配置权限 → 打包发布,这是完整的开发路径。

适合用 Tauri 的场景

  • 需要跨平台(Windows/macOS/Linux,甚至移动端)的桌面应用
  • 安装包体积内存占用有要求的场景
  • 需要访问系统 API(文件、通知、快捷键、系统托盘等)
  • 团队已有 Web 开发能力,希望低成本拓展到桌面端

不太适合的场景

  • 需要像素级跨平台 UI 一致性(系统 WebView 差异可能导致渲染略有不同)
  • 完全不愿意碰 Rust 的团队
  • 需要极致图形性能的游戏类应用(这种场景考虑 native 方案)

延伸阅读


Tauri 是一个相当年轻的框架,生态还在快速成长。你在探索过程中遇到的问题,很可能就是这个社区正在解决的问题。加入进来,说不定你就是下一个贡献者。

——本文基于 Tauri 2.x 版本编写,持续更新中。