Featured image of post Tauri 2.x 跨平台桌面应用:Rust + WebView2 实战

Tauri 2.x 跨平台桌面应用:Rust + WebView2 实战

Tauri 2.x 桌面应用开发完整指南:环境搭建、Rust + WebView 工作区、pnpm + Vite 前端集成、tauri.conf.json 配置、Rust 后端命令、Cargo 工作区、跨平台打包。

什么是 Tauri 2.x

Tauri 是 2020 年起 Rust 生态的跨平台桌面应用框架,2.0 版本(2024-10 GA)带来更完善的插件体系和移动端支持。

核心架构

  • 前端:任意 Web 框架(React / Vue / Svelte / Solid)
  • 后端:Rust 二进制,编译成原生代码
  • WebView:复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK)
  • 进程间通信:Rust 命令 + JS 桥

优势

  • 包大小 2~10MB(vs Electron 80MB+)
  • 内存低:复用系统 WebView,不开新 Chromium
  • Rust 内存安全:无 GC 抖动
  • 细粒度权限:声明式 ACL
  • 移动端支持(2.0+):iOS / Android

前置环境

通用

  • Rust 工具链curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  • Node.js 18+(推荐 22 LTS)
  • 包管理:pnpm

Windows

macOS

1
xcode-select --install

Linux (Debian/Ubuntu)

1
2
3
4
5
6
7
8
sudo apt install -y \
  libwebkit2gtk-4.1-dev \
  libgtk-3-dev \
  libayatana-appindicator3-dev \
  librsvg2-dev \
  libssl-dev \
  libjavascriptcoregtk-4.1-dev \
  libsoup-3.0-dev

创建项目

1. 创建工作区

1
2
3
4
mkdir -p tauri-app && cd tauri-app

# pnpm-workspace.yaml
nvim pnpm-workspace.yaml
1
2
3
packages:
  - 'frontend'
  - 'src-tauri'

2. 创建 Vite + React 前端

1
pnpm create vite frontend --template react-ts

3. 添加 Tauri CLI

1
pnpm add -D @tauri-apps/cli@latest -w

package.json 会加:

1
"@tauri-apps/cli": "^2.11.1"

4. 初始化 Tauri

1
pnpm tauri init

按提示填写:

1
2
3
4
5
6
✔ What is your app name? · tauri-app
✔ What should the window title be? · tauri-app
✔ Where are your web assets (HTML/CSS/JS) located, relative to the "<current dir>/src-tauri/tauri.conf.json" file that will be created? · ../frontend/dist
✔ What is the url of your dev server? http://localhost:1420
✔ What is your frontend dev command? · pnpm -F frontend dev
✔ What is your frontend build command? · pnpm -F frontend build

5. 安装依赖

1
2
pnpm install
mkdir -p frontend/dist  # dist 必须存在

6. 修改 tauri.conf.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "productName": "tauri-app",
  "version": "0.1.0",
  "identifier": "com.example.tauri-app",
  "build": {
    "frontendDist": "../frontend/dist",
    "devUrl": "http://localhost:1420",
    "beforeDevCommand": "pnpm -F frontend dev",
    "beforeBuildCommand": "pnpm -F frontend build"
  },
  "app": {
    "windows": [
      {
        "title": "tauri-app",
        "width": 1200,
        "height": 800
      }
    ],
    "security": {
      "csp": null
    }
  }
}

开发与打包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 开发(自动打开 DevTools 调试)
pnpm tauri dev

# 打包
pnpm tauri build

# 结果
# Windows: src-tauri/target/release/tauri-app.exe
# macOS:   src-tauri/target/release/bundle/macos/tauri-app.app
# Linux:   src-tauri/target/release/bundle/deb/*.deb

Cargo 工作区改造

Tauri 2.x 推荐用 Cargo 工作区管理(多包):

1
2
3
4
# Cargo.toml (根目录)
[workspace]
members = ["src-tauri"]
resolver = "2"

迁移步骤:

  1. src-tauri/target(构建会在根目录的 target 重新生成)
  2. 迁移 src-tauri/.gitignore 到根目录并修改
  3. frontend/.gitignore(如果根目录有覆盖)

根目录 .gitignore

1
2
3
4
5
**/target/
/src-tauri/gen/schemas

**/node_modules
**/dist

Vite 配置

frontend/vite.config.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

const host = process.env.TAURI_DEV_HOST;

export default defineConfig({
  plugins: [react()],
  // 防止 Vite 清除 Rust 显示的错误
  clearScreen: false,
  server: {
    port: 5173,
    strictPort: true,    // Tauri 需要固定端口
    host: host || false,
    hmr: host
      ? {
          protocol: "ws",
          host,
          port: 5174,
        }
      : undefined,
    watch: {
      // 告诉 Vite 忽略 src-tauri 目录
      ignored: ["**/src-tauri/**"],
    },
  },
  envPrefix: ["VITE_", "TAURI_ENV_*"],
  build: {
    // Windows 用 chromium,其他用 safari13
    target:
      process.env.TAURI_ENV_PLATFORM == "windows" ? "chrome105" : "safari13",
    minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
    sourcemap: !!process.env.TAURI_ENV_DEBUG,
  },
});

Rust 后端命令

Tauri 2.x 用 #[tauri::command] 宏定义命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src-tauri/src/main.rs (或 src/lib.rs)
use tauri::Manager;

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! 你好, Tauri!", name)
}

#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?
        .text()
        .await
        .map_err(|e| e.to_string())
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet, fetch_data])
        .setup(|app| {
            #[cfg(debug_assertions)]
            {
                let window = app.get_webview_window("main").unwrap();
                window.open_devtools();
            }
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

前端调用 Rust 命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// frontend/src/App.tsx
import { invoke } from "@tauri-apps/api/core";
import { useState } from "react";

function App() {
  const [greeting, setGreeting] = useState("");

  async function handleClick() {
    const msg = await invoke<string>("greet", { name: "World" });
    setGreeting(msg);
  }

  return (
    <div>
      <button onClick={handleClick}>调用 Rust</button>
      <p>{greeting}</p>
    </div>
  );
}

export default App;

常用插件

1
2
3
4
5
6
7
8
pnpm tauri add fs
pnpm tauri add dialog
pnpm tauri add notification
pnpm tauri add shell
pnpm tauri add http
pnpm tauri add store
pnpm tauri add window
pnpm tauri add updater

权限配置

src-tauri/capabilities/default.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "默认权限",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:allow-read-text-file",
    "fs:allow-write-text-file",
    "dialog:default",
    "notification:default"
  ]
}

文件系统

1
2
3
4
5
6
7
8
// Rust
use tauri_plugin_fs::FsExt;

#[tauri::command]
fn read_config(app: tauri::AppHandle) -> Result<String, String> {
    let path = app.path().app_config_dir().unwrap().join("config.json");
    std::fs::read_to_string(path).map_err(|e| e.to_string())
}
1
2
3
4
// 前端
import { readTextFile } from '@tauri-apps/plugin-fs';

const content = await readTextFile('/path/to/file.txt');

状态管理

Tauri 用 tauri::State 在命令间共享状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::sync::Mutex;

struct AppState {
    counter: Mutex<i32>,
}

#[tauri::command]
fn increment(state: tauri::State<AppState>) -> i32 {
    let mut count = state.counter.lock().unwrap();
    *count += 1;
    *count
}

fn main() {
    tauri::Builder::default()
        .manage(AppState { counter: Mutex::new(0) })
        .invoke_handler(tauri::generate_handler![increment])
        .run(tauri::generate_context!())
        .expect("failed to run app");
}

事件系统

1
2
3
4
5
6
7
// Rust 发送事件
use tauri::Emitter;

#[tauri::command]
fn start_task(app: tauri::AppHandle) {
    app.emit("task-progress", "started").unwrap();
}
1
2
3
4
5
6
7
8
9
// 前端监听
import { listen } from '@tauri-apps/api/event';

const unlisten = await listen<string>('task-progress', (event) => {
  console.log('收到:', event.payload);
});

// 取消监听
unlisten();

跨平台打包

1
2
3
4
5
6
7
8
# 当前平台
pnpm tauri build

# 指定平台(需要对应工具链)
pnpm tauri build --target x86_64-pc-windows-msvc
pnpm tauri build --target x86_64-apple-darwin
pnpm tauri build --target aarch64-apple-darwin
pnpm tauri build --target x86_64-unknown-linux-gnu

性能对比

指标Tauri 2.xElectron
包大小(Hello World)3~5MB80MB
内存空闲30~80MB100~200MB
启动时间0.3~1s1.5~3s
CPU 占用中等

下一步

  • Electron 入门,看 2018-12-15《Electron 跨平台桌面应用》
  • 桌面开发技术栈对比,看 2014-08-15《桌面开发技术栈对比》

参考资料

  • Tauri 官方:https://tauri.app/
  • Tauri 2.x 文档:https://v2.tauri.app/
  • Awesome Tauri:https://github.com/tauri-apps/awesome-tauri
  • tauri-plugin-* 插件列表:https://v2.tauri.app/plugin/
使用 Hugo 构建
主题 StackJimmy 设计