Tauri 2.0正式版发布与实战

Tauri 2.0 正式版终于发布了。从 beta 一路跟过来,2.0 的变化相当大:新的权限系统、重写的 IPC 安全模型、移动端支持。这篇文章梳理核心变更,并从零搭建一个系统信息查看工具来感受 2.0 的开发体验。

2.0 核心变更

权限系统(Capabilities)

这是 2.0 最大的架构级变更。1.x 时代的 allowlist 被全新的 capabilities 系统取代。

1.x 的做法是在 tauri.conf.json 里配一个白名单:

{
  "tauri": {
    "allowlist": {
      "fs": { "readFile": true, "writeFile": false },
      "shell": { "open": true }
    }
  }
}

2.0 改为基于 capability 文件的声明式权限。每个窗口(或 webview)可以分配不同的能力:

// src-tauri/capabilities/main-window.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-window-capability",
  "description": "主窗口的权限",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:allow-read-text-file",
    "fs:deny-write-file",
    "shell:allow-open",
    "os:allow-info"
  ]
}

这意味着不同窗口可以有不同的权限边界。比如设置窗口只需要读配置的权限,而主窗口可以访问更多 API。

IPC 安全模型

2.0 重新设计了前端到后端的 IPC 通信。核心变化:

  • 命令默认需要权限声明才能被前端调用
  • origin 校验:可以限制只有特定 origin 的 webview 才能调用某个命令
  • scope 细化:文件系统操作可以限制到具体路径
// 定义命令时声明权限
#[tauri::command]
async fn get_system_info() -> Result<SystemInfo, String> {
    // ...
}

// plugin 权限定义
// permissions/default.toml
// [[permission]]
// identifier = "allow-get-system-info"
// description = "允许获取系统信息"
// commands.allow = ["get_system_info"]

移动端支持

2.0 原生支持 iOS 和 Android。不是套壳 —— 它通过 WKWebView (iOS) 和 Android WebView 实现,Rust 代码通过 JNI (Android) 和 C FFI (iOS) 桥接。

项目初始化时可以选择目标平台:

# 创建项目时选择移动端支持
cargo tauri init
# 或
npm create tauri-app@latest

# 添加移动端
cargo tauri android init
cargo tauri ios init

# 开发
cargo tauri android dev
cargo tauri ios dev

移动端有些限制:不支持多窗口、系统托盘,部分桌面端 API 不可用。但文件系统、HTTP、通知等核心能力都可以跨平台使用。

其他重要变更

  • 插件系统重构:官方功能拆成独立插件(fs、shell、dialog 等),按需引入
  • 多 webview 支持:一个窗口里可以有多个 webview
  • 事件系统增强:支持 webview 级别的事件隔离
  • 构建产物优化:更小的二进制体积

从 1.x 迁移

Tauri 提供了迁移工具:

# 自动迁移
npx @tauri-apps/cli migrate

不过自动迁移只能处理配置层面的变更。代码层面需要手动调整的地方:

  1. allowlist → capabilities:最大的改动,需要重写权限配置
  2. API 导入路径变更@tauri-apps/api 的模块结构变了
  3. 插件化:之前内置的功能现在要单独安装插件
# 安装需要的官方插件
cargo add tauri-plugin-fs tauri-plugin-shell tauri-plugin-os

前端 API 变更示例:

// 1.x
import { readTextFile } from '@tauri-apps/api/fs';

// 2.0
import { readTextFile } from '@tauri-apps/plugin-fs';

实战:系统信息工具

做一个简单但完整的工具,展示 CPU、内存、磁盘、网络等系统信息。

项目初始化

npm create tauri-app@latest sysinfo-tool -- --template react-ts
cd sysinfo-tool
cargo add serde serde_json sysinfo --manifest-path src-tauri/Cargo.toml
cargo add tauri-plugin-os --manifest-path src-tauri/Cargo.toml

Rust 后端:采集系统信息

// src-tauri/src/lib.rs
use serde::Serialize;
use sysinfo::{System, Disks, Networks};

#[derive(Serialize)]
struct CpuInfo {
    name: String,
    cores: usize,
    usage: f32,
    frequency: u64,
}

#[derive(Serialize)]
struct MemoryInfo {
    total: u64,
    used: u64,
    available: u64,
    usage_percent: f64,
}

#[derive(Serialize)]
struct DiskInfo {
    name: String,
    mount_point: String,
    total: u64,
    available: u64,
    fs_type: String,
}

#[derive(Serialize)]
struct SystemInfo {
    hostname: String,
    os_name: String,
    os_version: String,
    kernel_version: String,
    cpu: CpuInfo,
    memory: MemoryInfo,
    disks: Vec<DiskInfo>,
    uptime: u64,
}

#[tauri::command]
fn get_system_info() -> SystemInfo {
    let mut sys = System::new_all();
    sys.refresh_all();
    std::thread::sleep(std::time::Duration::from_millis(200));
    sys.refresh_cpu_usage();

    let cpu = sys.cpus().first().map(|c| CpuInfo {
        name: c.brand().to_string(),
        cores: sys.cpus().len(),
        usage: sys.global_cpu_usage(),
        frequency: c.frequency(),
    }).unwrap_or(CpuInfo {
        name: "Unknown".into(), cores: 0, usage: 0.0, frequency: 0,
    });

    let memory = MemoryInfo {
        total: sys.total_memory(),
        used: sys.used_memory(),
        available: sys.available_memory(),
        usage_percent: sys.used_memory() as f64 / sys.total_memory() as f64 * 100.0,
    };

    let disk_list = Disks::new_with_refreshed_list();
    let disks = disk_list.iter().map(|d| DiskInfo {
        name: d.name().to_string_lossy().to_string(),
        mount_point: d.mount_point().to_string_lossy().to_string(),
        total: d.total_space(),
        available: d.available_space(),
        fs_type: String::from_utf8_lossy(d.file_system()).to_string(),
    }).collect();

    SystemInfo {
        hostname: System::host_name().unwrap_or_default(),
        os_name: System::name().unwrap_or_default(),
        os_version: System::os_version().unwrap_or_default(),
        kernel_version: System::kernel_version().unwrap_or_default(),
        cpu,
        memory,
        disks,
        uptime: System::uptime(),
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_os::init())
        .invoke_handler(tauri::generate_handler![get_system_info])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

权限配置

// src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "默认权限",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "os:default"
  ]
}

自定义命令的权限需要在插件配置中声明。对于应用内命令,2.0 默认允许调用(除非你显式限制)。

前端展示

// src/App.tsx
import { useState, useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";

interface SystemInfo {
  hostname: string;
  os_name: string;
  os_version: string;
  kernel_version: string;
  cpu: { name: string; cores: number; usage: number; frequency: number };
  memory: { total: number; used: number; available: number; usage_percent: number };
  disks: { name: string; mount_point: string; total: number; available: number; fs_type: string }[];
  uptime: number;
}

function formatBytes(bytes: number): string {
  const gb = bytes / (1024 * 1024 * 1024);
  return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
}

function formatUptime(seconds: number): string {
  const days = Math.floor(seconds / 86400);
  const hours = Math.floor((seconds % 86400) / 3600);
  const mins = Math.floor((seconds % 3600) / 60);
  return `${days}天 ${hours}小时 ${mins}分钟`;
}

export default function App() {
  const [info, setInfo] = useState<SystemInfo | null>(null);

  const refresh = async () => {
    const data = await invoke<SystemInfo>("get_system_info");
    setInfo(data);
  };

  useEffect(() => { refresh(); }, []);

  if (!info) return <div>加载中...</div>;

  return (
    <div className="container">
      <h1>系统信息</h1>

      <section>
        <h2>操作系统</h2>
        <p>{info.os_name} {info.os_version} ({info.kernel_version})</p>
        <p>主机名: {info.hostname} | 运行时间: {formatUptime(info.uptime)}</p>
      </section>

      <section>
        <h2>CPU</h2>
        <p>{info.cpu.name}</p>
        <p>{info.cpu.cores} 核 | {info.cpu.frequency} MHz | 使用率 {info.cpu.usage.toFixed(1)}%</p>
        <div className="progress-bar">
          <div style={{ width: `${info.cpu.usage}%` }} />
        </div>
      </section>

      <section>
        <h2>内存</h2>
        <p>{formatBytes(info.memory.used)} / {formatBytes(info.memory.total)}</p>
        <div className="progress-bar">
          <div style={{ width: `${info.memory.usage_percent}%` }} />
        </div>
      </section>

      <section>
        <h2>磁盘</h2>
        {info.disks.map((d, i) => (
          <div key={i} className="disk-item">
            <p>{d.mount_point} ({d.fs_type})</p>
            <p>{formatBytes(d.total - d.available)} / {formatBytes(d.total)}</p>
          </div>
        ))}
      </section>

      <button onClick={refresh}>刷新</button>
    </div>
  );
}

构建

# 开发
cargo tauri dev

# 构建
cargo tauri build

2.0 的构建产物依然很小。这个系统信息工具在 Linux 下约 3MB,Windows 下约 5MB(包含 WebView2 引导程序)。

2.0 开发体验总结

优势:

  • 权限系统更安全,细粒度控制好
  • 插件系统干净,不用的功能不打包
  • 移动端支持是杀手级特性
  • Rust 后端的性能和安全性依然是最大卖点

需要注意的:

  • 迁移成本不低,权限系统的概念需要时间理解
  • 移动端支持还在完善,部分插件尚未适配
  • 生态规模比 Electron 小,第三方库少

总的来说,如果你在意应用体积和资源占用,Tauri 2.0 值得认真考虑。