一、Tauri 是什么
Tauri 是一个用 Rust 编写应用逻辑、用 Web 技术编写界面的跨平台桌面(与移动端)应用框架。它的定位很明确:让你已经写好的前端代码直接变成原生应用,而不是重新用另一套语言重写一遍界面。
对「有 Next.js / Nuxt.js / SvelteKit 项目,但想做桌面版」这个场景来说,这正是 Tauri 最被低估的价值——你不需要重构任何前端业务逻辑。
Tauri 与 Electron 的根本差异在于渲染层:Electron 每个应用都打包一份完整的 Chromium 运行时,Tauri 则直接调用操作系统的 WebView。
| 平台 | Tauri 使用的渲染器 |
|---|---|
| Windows | WebView2(Edge/Chromium 内核,系统预装或安装包内置) |
| macOS | WKWebView(Safari 内核,系统自带) |
| Linux | WebKitGTK |
| Android | AndroidX WebView |
| iOS | WKWebView |
这个差异带来两个直接结果:安装包体积小一个数量级,运行时内存占用也低得多。代价是渲染行为依赖系统 WebView 版本——同一份代码在老旧 Linux(WebKitGTK 较旧)上可能出现渲染差异,这是选型时需要接受的约束。
版本说明:本文基于 Tauri 2.x 编写(当前稳定线为 2.12.x,Rust crate 最低要求 Rust 1.90)。Tauri 3 已有 alpha 版本,但仍在预览阶段,生产环境请使用 2.x。如果你的项目还在 Tauri 1.x,本文第六节给出了完整迁移对照。
Tauri 2 相比 Tauri 1 变了什么
Tauri 2 不是小版本升级,几处关键变化直接影响了写法:
- 权限模型重构:Tauri 1 的
allowlist白名单被移除,改为 capabilities(能力)+ permissions(权限) 两层模型,权限声明在src-tauri/capabilities/*.json里,不再塞在tauri.conf.json中。 - 配置结构重排:
tauri顶层键改名app,bundle提升到顶层,identifier从bundle里挪到顶层。 - 大部分原生 API 拆成插件:文件操作、对话框、通知、Shell、更新器等不再是内置 API,而是独立的插件(
tauri-plugin-fs、tauri-plugin-dialog等),按需安装。 - 正式支持移动端:iOS 与 Android 是一等目标,不再是实验特性。
二、Tauri 的分层架构
Tauri 的架构分三层,各层职责清晰、互不越界。这也是它能兼容任意 Web 栈的根本原因。
| 层级 | 组成 | 职责 |
|---|---|---|
| 前端层(Web) | HTML/CSS/JS/TS;任意 Web 框架;Tauri 前端 API 包 | 界面渲染与业务逻辑,通过受限的 API 调用系统能力 |
| Tauri 核心层(Rust) | IPC 通信;权限与能力校验;窗口管理;托盘;自动更新;Rust 原生能力封装 | 唯一有权触碰系统资源的一层,所有调用都要经过权限检查 |
| 原生系统层 | Windows:Win32 API / WebView2;macOS:AppKit / WebKit;Linux:X11/Wayland | 真正的系统调用 |
各层详解:
前端层就是你熟悉的 Web 项目(Next.js、Nuxt.js、SvelteKit 等),唯一的区别在于:开发时 Tauri 会代理你的 Web 开发服务器实现热重载,打包后加载构建产物。你无需修改任何前端业务逻辑,仅需通过 @tauri-apps/api 与插件包调用桌面端原生功能(如文件读写)。
Tauri 核心层(Rust)是应用的「大脑」,但如果你只用基础功能,无需深入掌握 Rust:
- IPC 通信:前端与 Rust 的唯一安全通道。所有原生功能必须通过显式声明的
Command或插件命令触发,杜绝前端随意访问系统资源。 - 权限管理:Tauri 2 用 capabilities 精确控制「哪个窗口可以调用哪些 API」。默认沙箱隔离,你可以限制前端只能读写某个目录而无法访问系统盘。
- 原生能力封装:窗口、托盘、文件系统等核心功能已封装好,你只需调用 API,无需自己写 Rust。
原生系统层由 Tauri 直接调用系统原生 API:Windows 上用 WebView2 渲染前端、调用 Win32 API 管理窗口;macOS 上用 WebKit 渲染前端、通过 AppKit 实现程序坞与菜单栏。
三、从零搭建一个 Tauri 2 项目
步骤 1:安装开发环境
Tauri 需要 Rust 环境加系统依赖。
# Windows(PowerShell,以管理员身份运行)
winget install Rustlang.Rustup
rustup default stable
1. macOS(终端)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env系统依赖:
| 系统 | 依赖项 | 安装命令 |
|---|---|---|
| Windows | WebView2 + C++ 构建工具 | 安装 Visual Studio 2022(勾选「桌面开发 C++」);WebView2 运行时通常已预装 |
| macOS | Xcode 命令行工具 | xcode-select --install |
| Linux | webkit2gtk、build-essential 等 | 依发行版安装 libwebkit2gtk-4.1-dev 等 |
步骤 2:创建项目
方式一:给已有 Web 项目接入(推荐)
cd my-next-app
npm install -D @tauri-apps/cli@latest
npx tauri init交互提问按你的项目回答。关键是这几项:
- 应用名与窗口标题
- 前端资源目录:Next.js 填
../out,Nuxt.js 填../.output/public,SvelteKit 填../build - 开发服务器地址:
http://localhost:3000(Next.js)、http://localhost:5173(SvelteKit) - 前端开发命令:
npm run dev - 前端构建命令:
npm run build
方式二:从零创建
npm create tauri-app@latest步骤 3:核心配置(tauri.conf.json)
Tauri 2 的配置结构与 1.x 差别很大。下面是一个可直接使用的最小配置:
{
"identifier": "com.yourname.myapp",
"productName": "MyApp",
"version": "0.1.0",
"app": {
"windows": [
{
"label": "main",
"title": "My App",
"width": 1200,
"height": 800,
"resizable": true
}
],
"security": {
"csp": null
}
},
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:3000",
"frontendDist": "../out"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}几个必须记住的字段位置:
identifier在顶层,不在bundle里。这是应用的唯一标识(Bundle ID、WebView 数据目录都用它),必须改成你自己的反向域名,com.tauri.dev默认值不能用于正式构建。productName与version也在顶层(1.x 藏在package对象里)。app取代了 1.x 的tauri键,窗口、托盘、安全策略都放这里。build.frontendDist对应 1.x 的distDir;build.devUrl对应 1.x 的devPath。bundle提升到顶层,平台相关配置下沉为bundle.windows、bundle.macOS、bundle.linux。
步骤 4:声明权限(capabilities)
这是 Tauri 2 与 1.x 差别最大、也最容易漏掉的一步。默认情况下前端能调用的系统 API 极少,你需要显式授权。
创建 src-tauri/capabilities/default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "主窗口的基础能力",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-set-title",
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"dialog:allow-open",
"dialog:allow-save"
]
}权限命名的规律是 <插件>:<动作>,核心 API 则是 core:<模块>:<动作>。写权限时遵循最小授权原则——只给真正需要的动作。core:default 是聚合权限,会一次性放开 app、event、image、menu、path、resources、tray、webview、window 九个核心模块的默认权限集;如果你对窗口或托盘没有需求,可以只列具体模块的 core:<模块>:default,甚至逐条列出 core:window:allow-xxx。
需要注意的是,<插件>:default 这类聚合权限通常会给出一整套能力(如 fs:default 覆盖全部文件操作),配置时应当用它替换掉宽泛的默认项,而不是与细粒度权限叠加。
src-tauri/capabilities/ 目录下的所有能力文件默认全部启用;一旦你在 tauri.conf.json 的 app.security.capabilities 里显式列出了能力标识,就只有被列出的那些生效。
关于 fs 插件的 scope:文件类权限分两层,命令授权与路径范围必须同时满足,只有一层是无效的。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "fs-capability",
"description": "应用数据目录读写",
"windows": ["main"],
"permissions": [
"fs:allow-read-text-file",
"fs:allow-write-text-file",
{
"identifier": "fs:allow-read-text-file",
"allow": [{ "path": "$APPDATA/*" }]
},
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$APPDATA/*" }]
}
]
}路径占位符 $APPDATA、$APPCONFIG、$APPLOCALDATA 指向应用自己的数据目录,$HOME、$DOCUMENT、$DESKTOP 等指向系统用户目录。* 匹配一层,** 匹配多层;deny 规则优先于 allow。
步骤 5:安装插件
用到的原生能力需要单独安装插件。Rust 侧加依赖,JS 侧加对应的 npm 包:
# Rust 侧(src-tauri/Cargo.toml)
cargo add tauri-plugin-fs tauri-plugin-dialog tauri-plugin-opener
1. JS 侧
npm install @tauri-apps/plugin-fs @tauri-apps/plugin-dialog @tauri-apps/plugin-opener插件在 src-tauri/src/lib.rs 中注册:
fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![get_system_info])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}步骤 6:开发与调试
npm run tauri devTauri 会先启动你的 Web 开发服务器,再拉起应用窗口。修改前端代码后窗口自动热更新,体验与 Web 开发基本一致。
步骤 7:调用原生功能
文件系统(插件方式)
import { useState } from 'react';
import { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
export default function Home() {
const [content, setContent] = useState('');
const writeFile = async () => {
await writeTextFile('test.txt', 'Hello Tauri!', { baseDir: BaseDirectory.Document });
};
const readFile = async () => {
setContent(await readTextFile('test.txt', { baseDir: BaseDirectory.Document }));
};
return (
<div style={{ padding: 20 }}>
<button onClick={writeFile}>写入文件</button>
<button onClick={readFile}>读取文件</button>
<div>文件内容:{content}</div>
</div>
);
}注意选项名是 baseDir(不是 dir)。BaseDirectory.Document 只是「用户文档目录」的语义化别名,真正决定前端能访问哪些路径的是 capabilities 里的权限与 scope 配置——命令授权与路径 scope 两者缺一不可:只声明 fs:allow-read-text-file 而没有对应 scope,调用依然会被拒绝。
自定义 Rust Command(进阶)
需要调用系统 API 或做复杂逻辑时,才需要写 Rust:
// src-tauri/src/lib.rs
#[tauri::command]
fn get_system_info() -> String {
let os = std::env::consts::OS;
format!("当前系统:{}", os)
}前端调用:
import { invoke } from '@tauri-apps/api/core';
const getSysInfo = async () => {
const res = await invoke<string>('get_system_info');
alert(res);
};步骤 8:打包
npm run tauri build产物在 src-tauri/target/release/bundle/:
- Windows:
msi/(安装包)与nsis/(安装程序),路径src-tauri/target/release/bundle/msi/ - macOS:
dmg/(磁盘镜像)与macos/(.app包),路径src-tauri/target/release/bundle/dmg/
四、Tauri 2 常用插件清单
Tauri 2 的系统能力基本都拆成了插件,按需安装。下面是桌面端最常用的一批:
| 能力 | Rust crate | npm 包 | 说明 |
|---|---|---|---|
| 文件系统 | tauri-plugin-fs | @tauri-apps/plugin-fs | 读写文件与目录,受 scope 限制 |
| 对话框 | tauri-plugin-dialog | @tauri-apps/plugin-dialog | 打开/保存文件、消息框 |
| 打开外部链接 | tauri-plugin-opener | @tauri-apps/plugin-opener | 用系统默认程序打开 URL 或文件 |
| 通知 | tauri-plugin-notification | @tauri-apps/plugin-notification | 系统通知 |
| 剪贴板 | tauri-plugin-clipboard-manager | @tauri-apps/plugin-clipboard-manager | 读写剪贴板 |
| 自动更新 | tauri-plugin-updater | @tauri-apps/plugin-updater | 应用内更新 |
| Shell | tauri-plugin-shell | @tauri-apps/plugin-shell | 执行外部命令(需谨慎授权) |
| 全局快捷键 | tauri-plugin-global-shortcut | @tauri-apps/plugin-global-shortcut | 注册系统级快捷键 |
| 窗口状态 | tauri-plugin-window-state | @tauri-apps/plugin-window-state | 记忆窗口大小与位置 |
| 单实例 | tauri-plugin-single-instance | @tauri-apps/plugin-single-instance | 保证应用只启动一次 |
| 存储 | tauri-plugin-store | @tauri-apps/plugin-store | 键值持久化 |
| 深度链接 | tauri-plugin-deep-link | @tauri-apps/plugin-deep-link | 自定义协议唤起应用 |
核心 API(窗口、事件、菜单、托盘等)仍在 @tauri-apps/api 里,不需要额外装插件:
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
await win.setTitle('新标题');
await win.minimize();五、Tauri 与 Electron 的对比
| 维度 | Tauri 2 | Electron |
|---|---|---|
| 包体积 | 几 MB 量级(不打包 Chromium) | 数十至上百 MB |
| 内存占用 | 低,无独立 Chromium 进程 | 高,Chromium 多进程模型 |
| 渲染一致性 | 依赖系统 WebView 版本,不同系统有差异 | 打包固定版本 Chromium,各系统一致 |
| 前后端语言 | Rust(后端)+ Web(前端) | Node.js(后端)+ Web(前端) |
| 安全模型 | capabilities 细粒度授权,默认最小权限 | Node.js 权限较宽,需自行约束 |
| 开发环境 | 需要装 Rust | 需要装 Node.js |
| 生态成熟度 | 插件生态持续发展中,官方 30+ 插件 | 生态极其成熟,npm 上大量现成方案 |
| 移动端 | 官方支持 iOS / Android | 官方不支持 |
| 最低系统要求 | 受系统 WebView 版本影响 | 由打包的 Chromium 版本决定 |
怎么选:如果你的用户群体是技术用户、工具类应用、对安装包体积敏感、且愿意接受跨系统渲染差异,Tauri 更合适。如果你的应用高度依赖特定 Chromium 行为、需要大量 npm 现成方案(如音视频编辑、复杂表格渲染),或者团队只有前端背景不想碰 Rust,Electron 的风险更低。
这不是「谁更先进」的问题,而是约束条件不同。
六、从 Tauri 1 迁移到 Tauri 2
如果你已有 Tauri 1.x 项目,官方提供了 tauri migrate 命令自动完成大部分机械替换:
npm install @tauri-apps/cli@latest
npm run tauri migrate它会重写 tauri.conf.json、把 v1 的 allowlist 转成默认的 capability 文件、升级 Cargo 依赖、添加对应的插件 crate。但它不修改你的 Rust 源码和 JS import——那部分需要手工处理。
关键字段对照:
| Tauri 1 | Tauri 2 |
|---|---|
package.productName / package.version | 顶层 productName / version |
package 整个对象 | 已移除 |
tauri | app |
tauri.allowlist | 已移除,改为 src-tauri/capabilities/*.json |
tauri.allowlist.protocol.assetScope | app.security.assetProtocol.scope |
tauri.bundle | 顶层 bundle |
tauri.bundle.identifier | 顶层 identifier |
tauri.bundle.dmg / deb / appimage | bundle.macOS.dmg / bundle.linux.deb / bundle.linux.appimage |
tauri.cli | plugins.cli |
tauri.systemTray | app.trayIcon |
tauri.pattern | app.security.pattern |
tauri.updater | plugins.updater,并需设 bundle.createUpdaterArtifacts |
tauri.windows[].fileDropEnabled | app.windows[].dragDropEnabled |
build.distDir | build.frontendDist |
build.devPath | build.devUrl |
build.withGlobalTauri | app.withGlobalTauri |
Rust crate 侧的拆分明细:
| Tauri 1 | Tauri 2 |
|---|---|
api::dialog | tauri-plugin-dialog |
api::http | tauri-plugin-http |
api::process / api::shell | tauri-plugin-shell |
api::fs | tauri-plugin-fs,或直接用 Rust 标准库 std::fs |
api::version | semver crate |
api::path | tauri::Manager::path |
App::clipboard_manager | tauri-plugin-clipboard-manager |
App::global_shortcut_manager | tauri-plugin-global-shortcut |
JS 侧的 import 路径同样要改:@tauri-apps/api/fs → @tauri-apps/plugin-fs,@tauri-apps/api/http → @tauri-apps/plugin-http,@tauri-apps/api/shell → @tauri-apps/plugin-shell。
另外 Rust 的 Window 类型在 2.x 中改名为 WebviewWindow(为多 WebView 让位),如果你在 Rust 侧直接操作窗口,需要同步调整。
七、适配常见 Web 框架
接入 Tauri 的核心是「配置静态导出 + 指定产物目录」:
| Web 框架 | 静态导出配置 | frontendDist | 开发服务器 |
|---|---|---|---|
| Next.js | next.config.js 中设 output: 'export' | ../out | http://localhost:3000 |
| Nuxt.js | nuxt.config.ts 中设 ssr: false + nitro: { preset: 'static' } | ../.output/public | http://localhost:3000 |
| SvelteKit | svelte.config.js 中设 kit: { adapter: adapter-static() } | ../build | http://localhost:5173 |
需要注意的几点:
- SSR 必须关闭。Tauri 打包的是静态文件,无法运行服务端代码。Next.js / Nuxt.js 若开启了 SSR,需先改为静态导出。
- 路由建议用哈希路由(如
/#/about),避免刷新时静态资源路径解析出错。Next.js 可设assetPrefix: './',SvelteKit 可设kit: { router: { mode: 'hash' } }。 - Next.js 需要注意:Next.js 静态导出不支持服务端 API Route、ISR、动态路由的
getStaticPaths需改为静态参数;如果你的项目重度依赖这些能力,Tauri 的适配成本会显著上升。
八、常见问题排查
- Windows 提示缺少 WebView2:多数 Windows 10/11 已预装。若目标用户是 Windows 7 或精简版系统,需要在
bundle.windows.webviewInstallMode中配置为离线安装模式,把运行时打进安装包。 - macOS 打包签名报错:正式分发需要配置代码签名与公证(notarization)。仅本地测试可用
npm run tauri build -- --no-bundle跳过打包环节。 - 前端调用插件 API 报权限错误:这是 Tauri 2 最常见的困惑来源。检查
src-tauri/capabilities/*.json里是否声明了对应权限,以及windows字段是否覆盖了你的窗口 label。 - 文件读写被拒绝:权限声明正确但路径不在允许范围内时,需要额外配置 scope。
fs插件的 scope 定义允许访问的目录。 - 前端路由 404:静态导出后确认路由模式与资源前缀配置,必要时改用哈希路由。
- Linux 上渲染异常:多为系统 WebKitGTK 版本过旧。Tauri 2.x 起要求 libwebkit2gtk-4.1(4.0 属 1.x 时代);发行版版本过低时需要自行编译或使用 Flatpak 分发。
九、总结
- Tauri 2 的核心价值是让现有 Web 项目直接变成原生应用,且安装包体积与内存占用都远优于 Electron。代价是渲染行为依赖系统 WebView,且需要引入 Rust 工具链。
- 与 Tauri 1 相比,最大的变化是权限模型从
allowlist改为 capabilities + permissions 插件化架构。迁移时用官方tauri migrate处理机械部分,Rust 源码与 JS import 需手工调整。 - 落地顺序建议:先用
tauri init接入已有 Web 项目跑通tauri dev,再按需加插件与 capabilities,最后才考虑自定义 Rust Command。这样能把「Rust 学习成本」推迟到你真正需要写 Rust 的时候。