一个用 Rust 做后端、用系统 WebView 做前端渲染的框架,凭什么把安装包从 150MB 砍到 5MB,把内存砍掉 70%,还把安全模型做成了默认加固?
一、Tauri 是什么:先有一张全景图
一句话定义:Tauri 是一个用 Web 技术写 UI、用 Rust 写后端逻辑的跨平台应用框架,支持 Windows / macOS / Linux / Android / iOS 全平台。
它的核心思路可以用一个对比说清楚:
| Electron | Tauri | |
|---|---|---|
| 前端 | HTML/CSS/JS | HTML/CSS/JS(一样) |
| 后端 | Node.js | Rust |
| 渲染引擎 | 内嵌 Chromium(~150MB) | 系统 WebView(0 额外体积) |
| 安全模型 | 需手动配置 | 默认最小权限 + CSP |
| 空项目包体 | ~150MB | ~3-10MB |
案例说明:DBX 数据库管理工具(GitHub 7.1k+ Stars),选择 Tauri 2 + Rust + Vue 3 构建。安装包仅约 15MB,三秒启动,连上数据库就能用。如果用 Electron 做同等功能,安装包至少 150MB 起步。之前写过一篇博文来介绍它:DBX:15MB 开源数据库管理工具,一个工具搞定 50+ 种数据库
二、架构拆解:三层分工,各司其职
Tauri 的架构不是"一个大黑盒",而是三个分工明确的层:
┌──────────────────────────────────────────────┐
│ 前端层 (WebView) │
│ HTML / CSS / JS + 任意前端框架 │
│ 负责:UI 渲染、用户交互 │
├──────────────────────────────────────────────┤
│ IPC 通信层 │
│ invoke / event / channel │
│ 负责:前后端数据传输 │
├──────────────────────────────────────────────┤
│ Rust 核心层 │
│ TAO (窗口) + WRY (WebView) + 业务逻辑 │
│ 负责:系统调用、文件操作、网络请求、安全控制 │
└──────────────────────────────────────────────┘
2.1 前端层:你写什么框架都行
Tauri 不绑定任何前端框架。create-tauri-app 官方提供的模板:
| 框架 | 模板支持 |
|---|---|
| React | ✓ |
| Vue | ✓ |
| Svelte | ✓ |
| SolidJS | ✓ |
| Angular | ✓ |
| Preact | ✓ |
| 纯 HTML/CSS/JS | ✓ |
| Yew / Leptos / Sycamore(Rust 前端) | ✓ |
案例说明:DBX 用 Vue 3 + TypeScript 写前端,另一个知名 Tauri 应用 Clipy 用 React 写前端。选择哪个框架完全取决于团队习惯——Tauri 只管提供 WebView 容器和 IPC 通道,不管你用什么画 UI。
2.2 Rust 核心层:两个关键库
Tauri 自己不造窗口和 WebView,而是维护两个底层库:
| 库 | 职责 | 类比 |
|---|---|---|
| TAO | 窗口创建和管理 | 相当于 Electron 的 BrowserWindow,但不捆绑 Chromium |
| WRY | WebView 渲染 | 相当于把系统自带的浏览器引擎套进来用 |
各平台实际使用的 WebView:
| 平台 | WebView 引擎 |
|---|---|
| Windows | WebView2(基于 Chromium,Win10/11 内置) |
| macOS | WKWebView(Safari 内核,系统自带) |
| Linux | WebKitGTK(Linux 桌面标配) |
| Android | System WebView |
| iOS | WKWebView |
关键理解:Tauri 不内嵌浏览器,而是"借用"系统已经有的。这就是为什么 Tauri 的空项目可以小于 600KB——因为浏览器引擎那 150MB 的重量,操作系统已经替你背了。
案例说明:在 Windows 11 上,系统自带 WebView2 Runtime。Tauri 应用直接调用它,不需要额外打包。用户下载你的应用时,只下载你的代码和资源——几 MB 搞定。而 Electron 用户每次下载都要背上完整的 Chromium。
2.3 IPC 通信层:前后端的桥梁
Tauri 前端(JS)和后端(Rust)之间通过 IPC(进程间通信)交互。三种机制各有分工:
| 机制 | 方向 | 用途 | 特点 |
|---|---|---|---|
| invoke | 前端 → 后端 | 调用 Rust 函数,获取返回值 | 请求-响应模式,类似 HTTP |
| event | 双向 | 发送和监听事件 | 发布-订阅模式,支持跨窗口 |
| channel | 后端 → 前端 | 流式传输大量数据 | 逐帧推送,适合进度和日志 |
案例说明:一个文件加密工具的典型通信场景——
- 用户点击"加密"按钮 → 前端用
invoke调用 Rust 的encrypt_file命令 - Rust 开始加密,用
channel逐帧推送进度(0%...25%...50%...) - 加密完成后,Rust 用
event通知所有窗口"文件已加密"
三、命令机制(invoke):从 JS 调用 Rust 的完整流程
这是 Tauri 最核心的开发模式,必须彻底搞懂。
3.1 四步走通一个命令
第一步:在 Rust 端定义命令
// src-tauri/src/lib.rs
#[tauri::command]
fn greet(name: &str) -> String {
format!("你好, {}! 欢迎使用 Tauri。", name)
}
#[tauri::command]
async fn fetch_user(id: u32) -> Result<User, String> {
// 异步命令,可以做网络请求、文件读写等耗时操作
let user = db::find_user(id).await
.map_err(|e| e.to_string())?;
Ok(user)
}
两个要点:
- #[tauri::command] 宏把这个函数注册为可被前端调用的命令
- 参数和返回值必须可序列化(实现 Serialize / Deserialize)
第二步:注册命令到应用
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![greet, fetch_user])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
第三步:在前端调用命令
// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
async function onGreet() {
const result = await invoke<string>('greet', { name: '开发者' });
console.log(result); // "你好, 开发者! 欢迎使用 Tauri。"
}
async function onFetchUser() {
try {
const user = await invoke<User>('fetch_user', { id: 42 });
console.log(user);
} catch (error) {
console.error('获取用户失败:', error);
}
}
第四步:配置权限
Tauri 2 的安全模型要求你在配置中显式声明前端可以调用哪些命令:
// src-tauri/capabilities/default.json
{
"permissions": [
"core:default",
"core:window:default",
"core:window:allow-create"
]
}
案例说明:DBX 数据库管理工具中,前端用 invoke 调用 Rust 端的数据库驱动执行 SQL。Rust 端负责连接管理、SQL 解析、结果序列化;前端只管渲染表格。这种分工让数据库操作的性能由 Rust 而非 JS 决定。
3.2 命令参数的类型映射
JS 和 Rust 之间的类型自动转换:
| JS 类型 | Rust 类型 | 说明 |
|---|---|---|
string |
String / &str |
字符串 |
number |
i32 / u32 / f64 |
数字 |
boolean |
bool |
布尔值 |
object |
自定义 struct(需 Deserialize) |
对象 |
array |
Vec<T> |
数组 |
null |
Option<T> |
可空类型 |
#[derive(serde::Serialize, serde::Deserialize)]
struct QueryParams {
sql: String,
limit: Option<u32>,
timeout: u64,
}
#[tauri::command]
async fn execute_query(params: QueryParams) -> Result<QueryResult, String> {
// params.sql, params.limit, params.timeout 都可直接使用
todo!()
}
案例说明:一个 SSH 隧道管理工具,前端传入 { host: "192.168.1.1", port: 22, username: "admin", keyPath: "/home/.ssh/id_rsa" } 这样的对象,Rust 端自动反序列化为 struct,然后调用系统 SSH 客户端建立连接。
3.3 命令的状态管理
Rust 端可以在命令之间共享状态:
use std::sync::Mutex;
struct AppState {
db_connections: Mutex<HashMap<String, Connection>>,
}
#[tauri::command]
fn list_connections(state: tauri::State<AppState>) -> Vec<String> {
state.db_connections.lock().unwrap().keys().cloned().collect()
}
#[tauri::command]
fn add_connection(name: String, state: tauri::State<AppState>) {
state.db_connections.lock().unwrap().insert(name, Connection::new());
}
// 注册时管理状态
tauri::Builder::default()
.manage(AppState {
db_connections: Mutex::new(HashMap::new()),
})
.invoke_handler(tauri::generate_handler![list_connections, add_connection])
案例说明:DBX 用这种方式管理多个数据库连接的状态。前端可以随时调用 list_connections 查看所有活跃连接,调用 add_connection 新建连接。所有连接状态保存在 Rust 端的 Mutex<HashMap> 中,线程安全且高性能。
四、事件系统(event):不只是前后端通信
Tauri 的事件系统比 invoke 更灵活——它支持四种通信方向:
| 通信方向 | 方法 | 典型场景 |
|---|---|---|
| 前端 → 后端 | app.listen() |
Rust 监听前端触发的动作 |
| 后端 → 前端 | app.emit() |
Rust 推送通知给前端 |
| 前端 → 前端 | window.emit() |
跨窗口通信 |
| 后端 → 后端 | app.emit() |
多窗口间的 Rust 侧协调 |
4.1 后端向前端推送事件
// Rust 端:文件下载完成后通知前端
#[tauri::command]
async fn download_file(url: String, app: tauri::AppHandle) -> Result<(), String> {
let data = http::get(&url).await.map_err(|e| e.to_string())?;
fs::write("downloaded.dat", &data).await.map_err(|e| e.to_string())?;
// 向所有前端窗口推送事件
app.emit("download-complete", DownloadResult {
url,
size: data.len(),
path: "downloaded.dat".into(),
}).unwrap();
Ok(())
}
// 前端:监听下载完成事件
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<DownloadResult>('download-complete', (event) => {
console.log(`下载完成: ${event.payload.url}`);
console.log(`文件大小: ${event.payload.size} bytes`);
showNotification('下载完成!');
});
案例说明:一个日志查看器工具,Rust 端用 inotify 监听日志文件变化,每次文件追加内容就用 emit 推送给前端,前端实时滚动显示新日志。这比轮询方式延迟更低、CPU 占用更少。
4.2 跨窗口通信
// 窗口 A:发送消息
import { getCurrent } from '@tauri-apps/api/window';
getCurrent().emit('data-updated', { key: 'value' });
// 窗口 B:接收消息
import { listen } from '@tauri-apps/api/event';
await listen('data-updated', (event) => {
console.log('收到窗口 A 的数据:', event.payload);
});
案例说明:一个 Markdown 编辑器,主窗口编辑文档,预览窗口实时渲染。编辑窗口每次内容变化都通过 emit 通知预览窗口刷新,两个窗口独立渲染互不阻塞。
五、Channel:流式数据传输
Channel 是 Tauri 2 新增的机制,解决"后端持续向前端推送数据"的问题——invoke 是请求-响应模式,event 是广播模式,channel 是点对点流式传输。
#[tauri::command]
async fn process_large_file(path: String, on_progress: tauri::ipc::Channel<Progress>) {
let file = File::open(path).unwrap();
let total_size = file.metadata().unwrap().len();
for chunk in read_chunks(file) {
process_chunk(&chunk);
on_progress.send(Progress {
processed: chunk.len() as u64,
total: total_size,
}).unwrap();
}
}
const onProgress = new Channel<Progress>();
onProgress.onmessage = (msg) => {
updateProgressBar(msg.processed / msg.total * 100);
};
await invoke('process_large_file', { path: '/data/large.csv', onProgress });
案例说明:DBX 导出大数据集时,Rust 端逐行读取数据库、序列化为 CSV,通过 Channel 逐帧推送进度。前端显示实时进度条,用户体验比"卡住等结果"好得多。
六、安全模型:默认加固,而非事后补丁
这是 Tauri 和 Electron 最大的哲学差异。Electron 假设"默认都是安全的,出了问题再修",Tauri 假设"默认都是危险的,必须显式授权"。
6.1 最小权限原则
Tauri 2 引入了能力系统(Capability System),取代了 Tauri 1 的 allowlist:
// src-tauri/capabilities/default.json
{
"identifier": "default",
"description": "默认能力配置",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default",
"shell:allow-open",
"dialog:allow-open",
"fs:default"
]
}
每个权限对应一个具体操作。没有在 permissions 里声明的操作,前端就是调不了——不是报错,而是根本不可用。
案例说明:你的应用只需要"打开文件对话框"和"读写用户指定文件"两个文件系统权限。在 Electron 里,nodeIntegration: true 就给了渲染进程完整 Node.js 能力——读任意文件、执行命令、访问网络,全开。在 Tauri 里,你必须显式写 fs:allow-read-file 和 fs:allow-write-file,并且只能操作用户主动选择的文件。
6.2 CSP(内容安全策略)
Tauri 默认对所有 WebView 页面注入严格的 CSP:
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
这意味着: - 不能加载外部脚本(防 XSS) - 不能内联脚本(防注入) - 样式只允许 self 和 inline
案例说明:一个 Tauri 应用不小心在渲染层加载了恶意 CDN 脚本。由于 CSP 阻止了外部脚本执行,攻击者的代码根本跑不起来。同样的场景在 Electron 里,如果 webSecurity: false(很多应用为了方便跨域就这么设了),恶意脚本可以直接访问 Node.js API。
6.3 安全审计
Tauri 每个大版本和小版本都会接受独立安全审计,审计范围包括 Tauri 自身代码和上游依赖。审计报告公开可查。
| 版本 | 审计机构 |
|---|---|
| Tauri 1.0 | Radically Open Security |
| Tauri 2.0 | Radically Open Security |
七、插件系统:Tauri 2 的模块化架构
Tauri 1 的很多功能是内置的,Tauri 2 把它们全部抽成了插件——包括自动更新、CLI、文件系统、对话框、HTTP 请求等。
7.1 官方插件一览
| 插件 | 功能 | 典型场景 |
|---|---|---|
tauri-plugin-shell |
执行系统命令、打开外部程序 | 在默认浏览器打开链接 |
tauri-plugin-fs |
文件系统读写 | 读写配置文件、导出数据 |
tauri-plugin-dialog |
原生对话框 | 打开/保存文件选择器、消息弹窗 |
tauri-plugin-http |
HTTP 客户端 | 请求 API,无跨域问题 |
tauri-plugin-store |
持久化 KV 存储 | 保存用户偏好设置 |
tauri-plugin-updater |
自动更新 | 检查新版本、下载并安装 |
tauri-plugin-process |
进程管理 | 退出应用、重启 |
tauri-plugin-clipboard |
剪贴板读写 | 复制/粘贴内容 |
tauri-plugin-notification |
系统通知 | 推送桌面通知 |
tauri-plugin-os |
操作系统信息 | 获取平台、版本、架构 |
tauri-plugin-log |
日志记录 | 写日志到文件 |
tauri-plugin-sql |
数据库访问 | SQLite / MySQL / PostgreSQL |
tauri-plugin-autostart |
开机自启 | 系统启动时自动运行 |
7.2 使用插件的三步
第一步:添加依赖
# Rust 端
cargo add tauri-plugin-shell
# 前端
npm add @tauri-apps/plugin-shell
第二步:注册插件
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
第三步:配置权限 + 调用
// capabilities/default.json
{ "permissions": ["shell:allow-open"] }
import { open } from '@tauri-apps/plugin-shell';
await open('https://tauri.app');
案例说明:DBX 使用了 tauri-plugin-sql 直接在 Rust 端操作数据库,tauri-plugin-dialog 让用户选择数据库文件,tauri-plugin-fs 读写导出文件,tauri-plugin-http 做 API 请求。每个插件的权限都是按需声明,不会因为用了 SQL 插件就自动获得文件系统的写权限。
7.3 HTTP 插件:解决跨域的终极方案
这是前端开发者最关心的一个点。Electron 里跨域靠关 webSecurity 或配代理,Tauri 的 tauri-plugin-http 直接在 Rust 进程里发请求——根本不存在跨域问题,因为请求不是从浏览器发出的。
import { fetch } from '@tauri-apps/plugin-http';
// 直接请求任意 API,没有 CORS 限制
const response = await fetch('https://api.example.com/data', {
method: 'GET',
headers: { 'Authorization': 'Bearer xxx' },
});
const data = await response.json();
案例说明:一个股票监控工具,需要请求多个第三方行情 API。在浏览器里这些 API 大多不支持 CORS,在 Electron 里需要配代理。用 Tauri 的 HTTP 插件,Rust 进程直接发请求,跟 curl 一样自由,同时支持 Windows、macOS、Android 全平台。
八、移动端支持:Tauri 2 的新战场
Tauri 1 只支持桌面,Tauri 2 正式支持 Android 和 iOS。
8.1 移动端架构
移动端的架构和桌面端一脉相承:
┌──────────────────────────────────┐
│ 前端 (WebView) │ ← 和桌面一样
├──────────────────────────────────┤
│ IPC 层 │ ← 和桌面一样
├──────────┬───────────────────────┤
│ Kotlin 层 │ Rust 核心层 │ ← Android 用 Kotlin 桥接
│ (Android) │ │
├──────────┤ │
│ Swift 层 │ │ ← iOS 用 Swift 桥接
│ (iOS) │ │
└──────────┴───────────────────────┘
- Android:Rust 编译为
.so,通过 JNI 被 Kotlin 调用 - iOS:Rust 编译为静态库,通过 C FFI 被 Swift 调用
8.2 移动端特有的插件
| 插件 | 功能 |
|---|---|
tauri-plugin-barcode-scanner |
扫码 |
tauri-plugin-biometric |
指纹/面容认证 |
tauri-plugin-nfc |
NFC 读写 |
tauri-plugin-geolocation |
GPS 定位 |
tauri-plugin-local-notification |
本地推送 |
案例说明:一个密码管理器,桌面端用 Tauri 做,移动端也用 Tauri 2 做。核心加密逻辑写在 Rust 里,桌面和移动端共享同一套代码。移动端额外用 tauri-plugin-biometric 做指纹解锁——这个插件在桌面端不生效,但不影响编译。
九、性能对比:用数据说话
9.1 包体大小
| 应用类型 | Electron | Tauri |
|---|---|---|
| 空项目(压缩后) | ~150MB | ~3-10MB |
| DBX 数据库管理工具 | N/A | ~15MB |
| VS Code | ~350MB | N/A |
| 简单记事本 | ~160MB | ~4MB |
为什么差距这么大? Electron 打包了完整 Chromium(~150MB),Tauri 只打包了你的代码和资源。
9.2 内存占用
| 指标 | Electron | Tauri |
|---|---|---|
| 空项目 | ~80-150MB | ~15-30MB |
| 真实应用基准测试 | ~409MB | ~172MB |
案例说明:Authme(一款 2FA 验证器)同时用 Electron 和 Tauri 做了版本。Tauri 版内存占用约为 Electron 版的 42%。
9.3 启动速度
| 指标 | Electron | Tauri |
|---|---|---|
| 冷启动 | ~4s | ~2s |
| 热启动 | ~2s | ~1s |
案例说明:DBX 的启动时间约 3 秒(含数据库驱动初始化),同等功能的 Electron 应用启动至少 5-7 秒——差距主要来自 Chromium 引擎的初始化时间。
十、Tauri 1 vs Tauri 2:升级了什么
| 维度 | Tauri 1 | Tauri 2 |
|---|---|---|
| 平台 | 桌面(Win/Mac/Linux) | 桌面 + 移动端(Android/iOS) |
| 配置 | tauri.conf.json + allowlist |
tauri.conf.json 重构 + 能力系统(Capability) |
| 权限 | allowlist(白名单) | 细粒度权限(每个插件独立权限) |
| IPC | 基于字符串的 IPC | 重写的 IPC 层(类型安全、更高性能) |
| 插件 | 部分内置 | 全部插件化(updater/cli/sql 全部独立) |
| 移动端桥接 | 无 | Kotlin(Android)/ Swift(iOS)绑定 |
| 安全审计 | 1.0 审计 | 2.0 重新审计 |
十一、开发实战:从零创建一个 Tauri 应用
11.1 环境准备
| 依赖 | 安装方式 |
|---|---|
| Rust | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \| sh |
| Node.js | 18+ |
| Windows | Visual Studio Build Tools + WebView2 |
| macOS | Xcode Command Line Tools |
| Linux | libwebkit2gtk-4.1-dev 等依赖 |
11.2 创建项目
# 使用官方脚手架
npm create tauri-app@latest
# 选择框架(以 React + TypeScript 为例)
# → React
# → TypeScript
# → 项目名: my-tauri-app
11.3 项目结构
my-tauri-app/
├── src/ # 前端代码(React/Vue/etc)
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/ # Rust 后端代码
│ ├── src/
│ │ └── lib.rs # 命令定义 + 应用入口
│ ├── Cargo.toml # Rust 依赖
│ ├── tauri.conf.json # Tauri 配置
│ ├── capabilities/ # 权限配置
│ │ └── default.json
│ └── icons/ # 应用图标
├── package.json
└── vite.config.ts
11.4 开发与调试
# 启动开发模式(前端热更新 + Rust 自动重编译)
npm run tauri dev
# 构建生产版本
npm run tauri build
开发模式下按 F12 或 Ctrl+Shift+I 打开 WebView 的 DevTools——和 Chrome DevTools 一模一样。
十二、Tauri 适合什么,不适合什么
适合
| 场景 | 原因 |
|---|---|
| 开发者工具(数据库客户端、SSH 工具、API 调试器) | 轻量 + Rust 性能 + 系统集成 |
| 效率工具(Markdown 编辑器、笔记、日历) | 安装包小,用户下载快 |
| 数据密集型应用(日志分析、数据可视化) | Rust 处理数据远快于 JS |
| 需要安全性的应用(密码管理器、加密工具) | 默认最小权限 + Rust 内存安全 |
| 桌面+移动跨平台 | 一套代码覆盖全平台 |
不适合
| 场景 | 原因 |
|---|---|
| 团队完全没有 Rust 经验且不愿学习 | 哪怕只写简单命令,也需要基本 Rust 语法能力 |
| 需要精确控制每个像素的渲染效果 | WebView 渲染能力有限,不如自绘方案(Flutter/GPUI) |
| 应用强依赖 Chrome 特有 API | 系统 WebView 不是 Chromium,某些 API 不可用 |
| 需要兼容老旧 Windows(Win7 及以下) | WebView2 要求 Win10+ |
十三、常见坑与解法
| 坑 | 原因 | 解法 |
|---|---|---|
| WebView2 找不到 | 老版本 Windows 没装 WebView2 | 打包时选择 webview2-patch 或 webview2-offline-installer |
| Rust 编译慢 | 首次编译 Tauri 依赖需 5-10 分钟 | 只需一次,增量编译很快;可用 sccache 加速 |
| 系统 WebView 行为不一致 | 各平台 WebView 引擎不同 | 测试时覆盖所有目标平台;避免使用 Chrome-only API |
| 权限报错 | 忘记在 capabilities 里声明 | 检查 capabilities/default.json,添加所需权限 |
| 热更新偶尔失效 | Rust 代码修改需重编译 | 前端改动秒级生效;Rust 改动需等几秒重编译 |
总结
Tauri 的核心价值可以用三个关键词概括:
| 关键词 | 内涵 | 代价 |
|---|---|---|
| 轻量 | 系统 WebView + Rust,安装包 5-10MB | 依赖系统 WebView 版本,行为有差异 |
| 安全 | 默认最小权限 + CSP + 安全审计 | 权限配置更繁琐,开发时需显式声明 |
| 高性能 | Rust 后端处理计算密集型任务 | 需要基础 Rust 能力,学习曲线比纯 JS 陡 |
一句话建议:如果你的应用是"前端展示 + 后端干活"的架构(大部分工具类应用都是),Tauri 是当前最好的选择之一。前端该怎么写还怎么写,后端换成 Rust,立刻获得更小的包、更少的内存、更强的安全和更快的启动。
如果你完全不想碰 Rust,Electron 仍然是最安全的选择——但请接受 150MB 的安装包和数百 MB 的内存占用作为代价。
评论
0