编程 Tauri 2.0 深度拆解:当 Rust 遇见 WebView——从多进程架构、移动端革命到全面跨平台桌面应用的工程全貌(2026)

2026-07-18 14:43:19 +0800 CST views 6

Tauri 2.0 深度拆解:当 Rust 遇见 WebView——从多进程架构、移动端革命到全面跨平台桌面应用的工程全貌(2026)

一、背景:桌面开发的「不可能三角」

如果你写过跨平台桌面应用,一定绕不开这个问题:为什么没有一种技术能同时做到 包体小、性能好、开发快

桌面开发长期存在一个「不可能三角」:

  • Electron 给你 Web 开发体验和丰富的生态,代价是 150MB+ 的空包、一个完整的 Chromium 浏览器内嵌在你的应用里。你开发的不过是个聊天工具,却打包了一整个浏览器。
  • Qt / C++ 性能极致、包体极小,但 QML 的学习曲线陡峭,C++ 的前端开发体验和现代 Web 生态完全不兼容。
  • Flutter Desktop 用 Dart 写 UI,编译成原生代码,但生态远不如 Web 成熟,自定义原生能力要通过 Platform Channel 绕一圈。

Tauri 2.0 的出现,试图打破这个三角。它的思路很「Rust」:不重新发明轮子,而是把轮子用对。

二、核心架构:WebView + Rust 的「黄金组合」

Tauri 的架构可以概括为一句话:用系统原生 WebView 渲染 UI,用 Rust 处理后端逻辑

2.1 渲染层:不捆绑 Chromium

Electron 选择捆绑一个完整的 Chromium 引擎,这是它包体膨胀的根本原因——光一个 Chromium ≈ 100MB+。

Tauri 选择使用操作系统自带的 WebView

平台WebView 引擎
macOSWKWebView(Safari 引擎)
WindowsWebView2(Edge Chromium 引擎)
LinuxWebKitGTK
iOSWKWebView
AndroidWebView(系统 Chrome 引擎)

这意味着:

  • 你的应用不打包浏览器,空包体 3-10MB
  • WebView 由 OS 更新,安全补丁跟着系统走
  • 但这也意味着你需要针对不同 WebView 做兼容测试

2.2 后端:Rust 驱动的核心进程

Tauri 2.0 引入了多进程架构(Multiwebview),将核心功能和渲染分离:

┌─────────────────────────────────────────────────┐
│                  Tauri 应用                        │
│                                                    │
│  ┌─────────────┐   ┌─────────────┐                │
│  │ Rust 核心进程 │   │ Rust 核心进程 │  (多 WebView) │
│  │  (主进程)    │   │  (副进程)    │                │
│  │             │   │             │                │
│  │ IPC 调度器   │◄──│ IPC 调度器   │                │
│  │ 权限管理     │   │ 权限管理     │                │
│  │ 窗口管理     │   │ 窗口管理     │                │
│  └──────┬──────┘   └──────┬──────┘                │
│         │                 │                        │
│  ┌──────▼──────────────────▼──────┐                │
│  │       WebView 渲染层            │                │
│  │  (React/Vue/Svelte 应用)       │                │
│  └───────────────────────────────┘                │
└─────────────────────────────────────────────────┘

让我用代码让你直观感受一个 Tauri 2.0 应用的骨架:

tauri.conf.json — 配置入口:

{
  "productName": "logviewer",
  "version": "0.1.0",
  "identifier": "com.example.logviewer",
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:5173",
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build"
  },
  "app": {
    "windows": [
      {
        "title": "Log Viewer",
        "width": 1200,
        "height": 800,
        "resizable": true,
        "center": true
      }
    ],
    "security": {
      "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'"
    }
  },
  "bundle": {
    "active": true,
    "icon": ["icons/icon.png"]
  }
}

src-tauri/src/lib.rs — Rust 后端核心:

use tauri::Manager;

#[tauri::command]
fn read_log_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(&path)
        .map_err(|e| format!("读取文件失败: {}", e))
}

#[tauri::command]
fn search_logs(content: String, pattern: String) -> Result<Vec<String>, String> {
    let re = regex::Regex::new(&pattern)
        .map_err(|e| format!("正则表达式无效: {}", e))?;
    
    let matches: Vec<String> = content
        .lines()
        .filter(|line| re.is_match(line))
        .map(String::from)
        .collect();
    
    Ok(matches)
}

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_fs::init())
        .invoke_handler(tauri::generate_handler![
            read_log_file,
            search_logs,
        ])
        .run(tauri::generate_context!())
        .expect("启动应用时发生错误");
}

前端调用(Vue 3 示例)

import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'

async function openAndSearch() {
  // 调用原生文件对话框
  const selected = await open({
    multiple: false,
    filters: [{ name: 'Log Files', extensions: ['log', 'txt'] }]
  })
  
  if (selected) {
    // 调用 Rust 命令读取文件
    const content = await invoke<string>('read_log_file', { path: selected })
    const matches = await invoke<string[]>('search_logs', {
      content,
      pattern: 'ERROR|FATAL'
    })
    
    console.log(`找到 ${matches.length} 条错误日志`)
  }
}

这段代码展示了 Tauri 最核心的交互模式:前端通过 invoke() 调用 Rust 命令,所有的文件 I/O、正则搜索、数据处理都在 Rust 侧完成——没有 JavaScript 事件循环阻塞,没有 Node.js 的 GIL。

2.3 Multiwebview 架构

Tauri 2.0 最重要的架构革新是多 WebView 支持。在 1.x 时代,一个 Tauri 应用只能有一个 WebView 窗口。2.0 允许创建多个独立的 WebView 实例,每个拥有独立的渲染上下文和 IPC 通道。

use tauri::{Manager, WebviewUrl, WebviewBuilder};

pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // 主窗口
            let main_window = app.get_webview_window("main").unwrap();
            
            // 创建一个侧边栏 WebView
            let _sidebar = tauri::WebviewBuilder::new("sidebar", WebviewUrl::App("sidebar.html".into()))
                .auto_resize()
                .build()?;
            
            // 创建一个浮动工具窗口
            let _tools = tauri::WebviewBuilder::new("tools", WebviewUrl::App("tools.html".into()))
                .size(400.0, 600.0)
                .decorations(false)
                .build()?;
            
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error")
}

这为复杂桌面应用打开了新可能:

  • 主编辑区 + 侧边栏 + 调试面板各用一个 WebView
  • 每个 WebView 独立崩溃隔离
  • 不同的 WebView 可以加载不同的前端框架(主应用 React,调试面板 Svelte)

三、移动端支持:Tauri 2.0 最大的「王牌」

如果说 Tauri 1.x 只是在桌面端「模仿 Electron 但更轻」,那 2.0 的移动端支持就是真正的「降维打击」。

3.1 架构适配

Tauri 2.0 在移动端的工作方式:

┌─────────────────────────────────────────┐
│  iOS / Android 设备                       │
│                                           │
│  ┌─────────────────────────────────┐     │
│  │  Tauri 核心进程 (Rust)           │     │
│  │  - 命令处理                       │     │
│  │  - 插件管理                       │     │
│  │  - 状态管理                       │     │
│  └──────────┬──────────────────────┘     │
│             │ IPC                        │
│  ┌──────────▼──────────────────────┐     │
│  │  WKWebView / Android WebView     │     │
│  │  (React/Vue/Svelte UI)          │     │
│  └─────────────────────────────────┘     │
│                                           │
└─────────────────────────────────────────┘

关键区别在于:移动端没有「窗口管理」概念,取而代之的是「视图管理」。你的前端框架负责页面路由,Rust 核心进程在后台保持运行。

3.2 移动端专属 API

Tauri 2.0 为移动端提供了丰富的原生插件:

// 生物识别认证
import { authenticate } from '@tauri-apps/plugin-biometric'

async function login() {
  const result = await authenticate({
    reason: '验证身份以访问安全设置',
    // iOS: 自动使用 Face ID / Touch ID
    // Android: 自动使用 指纹 / 面部识别
  })
  return result
}

// NFC 读写
import { startScan, writeTag } from '@tauri-apps/plugin-nfc'

async function readNFCTag() {
  const tag = await startScan({
    once: true,
    alertMessage: '将设备靠近 NFC 标签'
  })
  console.log('读取到标签:', tag.id)
  
  // 写入数据
  await writeTag(tag.id, {
    records: [{ type: 'text', value: 'Hello from Tauri!' }]
  })
}

// 深度链接
import { onOpenUrl } from '@tauri-apps/plugin-deep-link'

// 处理 myapp://settings/profile 这类 URL
await onOpenUrl((urls) => {
  for (const url of urls) {
    const route = new URL(url).pathname
    // 导航到对应页面
    navigate(route)
  }
})

3.3 真正的「一套代码,多处运行」

Tauri 2.0 的代码复用率取决于你对平台差异的处理方式。一个典型的跨平台项目结构:

my-app/
├── src/                    # 前端代码
│   ├── App.vue            # 主组件
│   ├── components/
│   │   ├── desktop/       # 桌面端专属 UI
│   │   └── mobile/        # 移动端专属 UI
│   └── utils/
│       └── platform.ts    # 平台适配
├── src-tauri/
│   ├── src/
│   │   ├── lib.rs         # 共享核心逻辑
│   │   ├── desktop.rs     # 桌面端适配
│   │   └── mobile.rs      # 移动端适配
│   ├── capabilities/
│   │   ├── default.json    # 桌面权限
│   │   └── mobile.json    # 移动端权限
│   └── Cargo.toml
└── package.json

平台检测

import { type } from '@tauri-apps/api/os'

async function getPlatform(): Promise<'desktop' | 'mobile'> {
  const osType = await type()
  // 'iOS' 或 'Android' → mobile
  if (osType === 'iOS' || osType === 'Android') {
    return 'mobile'
  }
  return 'desktop'
}

// 在组件中使用
const platform = ref<'desktop' | 'mobile'>('desktop')

onMounted(async () => {
  platform.value = await getPlatform()
})

四、IPC 与安全模型:Rust 的「权限哲学」

Electron 最大的安全问题是什么?默认不安全。Node.js 的 require() 在渲染进程中默认可用,一旦 XSS 就完蛋。你需要手动配置 contextIsolation: true,手动列白名单 API。

Tauri 换了个思路:默认不安全才奇怪

4.1 Capabilities 系统

Tauri 2.0 引入了 Capabilities 权限声明系统。每个插件、每个命令都需要显式声明权限:

// src-tauri/capabilities/default.json
{
  "identifier": "default",
  "description": "默认权限配置",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:default",
    "dialog:allow-open",
    "dialog:allow-save",
    "fs:default",
    {
      "identifier": "fs:allow-read-file",
      "allow": [{ "path": "$HOME/**" }]
    },
    {
      "identifier": "fs:allow-write-file",
      "allow": [{ "path": "$APPDATA/**" }]
    },
    "shell:default",
    {
      "identifier": "shell:allow-execute",
      "allow": [{ "cmd": "git", "args": true }]
    }
  ]
}

这意味着:

  • 默认没有命令可用(除了 core:default
  • 每个命令的路径、参数都受限
  • 不同窗口可以有不同的权限集合

4.2 IPC 通道

Tauri 的 IPC 机制基于 JSON-RPC 风格的消息传递:

前端                              Rust 核心
 │                                  │
 ├── invoke("read_log_file", ──────►│
 │    { path: "/var/log/syslog" })  │
 │                                  ├── 权限检查 ✓
 │                                  ├── 路径校验 ✓
 │                                  ├── 读取文件
 │                                  │
 │◄─────────────────────────────────┤
 │  Result<"file content...", Err>  │
 │                                  │

Rust 侧的命令处理自带类型安全

#[tauri::command]
fn read_config(app_handle: tauri::AppHandle) -> Result<Config, String> {
    let config_dir = app_handle.path().app_config_dir()
        .map_err(|e| format!("获取配置目录失败: {}", e))?;
    
    let config_path = config_dir.join("config.json");
    
    let content = std::fs::read_to_string(&config_path)
        .map_err(|e| format!("读取配置文件失败: {}", e))?;
    
    let config: Config = serde_json::from_str(&content)
        .map_err(|e| format!("解析配置失败: {}", e))?;
    
    Ok(config)
}

AppHandle 自动注入,路径方法自动适配 OS,序列化自动完成。这不是简单的 FFI 调用——这是 Rust 类型系统在跨进程通信中的自然延伸。

五、插件系统:从「大而全」到「按需加载」

Tauri 2.0 将 1.x 中的大量内置功能抽离为独立插件。核心插件包括:

插件功能包体积增量
tauri-plugin-dialog原生文件对话框、消息框~80KB
tauri-plugin-fs文件系统操作~120KB
tauri-plugin-shell执行外部命令~100KB
tauri-plugin-httpHTTP 客户端~200KB
tauri-plugin-notification原生通知~60KB
tauri-plugin-clipboard剪贴板读写~50KB
tauri-plugin-barcode条码/二维码扫描~150KB
tauri-plugin-biometric生物识别~100KB
tauri-plugin-nfcNFC 读写~120KB

对比 Electron 那种「内置所有 API 你愿意不愿意都用不了」的设计,Tauri 的插件哲学更接近 Unix 工具:每个插件只做一件事,而且做好

5.1 自定义插件开发

你的 Rust 知识此时能直接变现。写一个 Tauri 插件只需要几步:

use tauri::{
    plugin::{Builder, TauriPlugin},
    Runtime, Manager,
};

// 1. 定义命令
#[tauri::command]
async fn monitor_cpu(this: tauri::AppHandle) -> Result<f64, String> {
    // 跨平台的 CPU 使用率读取
    #[cfg(target_os = "macos")]
    {
        let output = std::process::Command::new("top")
            .args(["-l", "1", "-n", "0", "-stats", "cpu"])
            .output()
            .map_err(|e| format!("执行 top 失败: {}", e))?;
        // 解析输出...
        Ok(cpu_usage)
    }
    
    #[cfg(target_os = "linux")]
    {
        let content = std::fs::read_to_string("/proc/stat")
            .map_err(|e| format!("读取 /proc/stat 失败: {}", e))?;
        // 解析 CPU 行...
        Ok(cpu_usage)
    }
}

// 2. 构建插件
pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("system-monitor")
        .invoke_handler(tauri::generate_handler![monitor_cpu])
        .build()
}

// 3. 在应用中使用
fn main() {
    tauri::Builder::default()
        .plugin(init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

插件机制的精髓在于:你可以把任何 Rust crate 暴露给前端调用。需要 SQLite?rusqlite 包装一下。需要图像处理?image crate 暴露成命令。需要机器学习推理?candle + 命令接口。

六、性能深度对比:Tauri vs Electron

6.1 包体积

┌──────────────────────────────────────────────────────┐
│  包体积对比(Hello World 级别)                        │
│                                                      │
│  Electron (打包):    ████████████████████  148 MB     │
│  Electron (ASAR):    ████████████████     120 MB     │
│  Tauri (macOS .app): ██                 ~5 MB        │
│  Tauri (Windows):    ██                 ~4 MB        │
│  Tauri (Linux):      ██                 ~6 MB        │
│  Tauri (Android):    ████               ~12 MB       │
│  Tauri (iOS):        ████               ~15 MB       │
└──────────────────────────────────────────────────────┘

Tauri 的包体优势是 Electron 的 20-30 倍。这不是渐进式优化,这是量级差异。

6.2 内存占用

真实应用对比(一个 Markdown 编辑器)
┌──────────────────────────────────────────────────────────┐
│  启动内存占用                                              │
│                                                          │
│  Electron (Typora 替代品):  █████████████████████  185 MB │
│  Tauri (自定义 Markdown):   ██████                  38 MB │
│                                                          │
│  编辑 100MB 文件后                                        │
│  Electron:                 ████████████████████████ 220 MB│
│  Tauri:                    █████████████████████████ 212 MB│
│  (大文件时差异缩小,因为瓶颈在 DOM 和渲染)               │
└──────────────────────────────────────────────────────────┘

注意:内存差异在小而轻的应用中最明显。当你处理大型数据时,瓶颈会从运行时转移到前端渲染本身,这时候 Electron 和 Tauri 的差异会缩小。

6.3 启动速度

首次冷启动:
  Electron:  ~1200ms
  Tauri:     ~350ms

二次热启动(已有进程驻留):
  Electron:  ~200ms
  Tauri:     ~50ms

Tauri 的冷启动优势来自:不需要初始化 V8 引擎 + Node.js 事件循环,Rust 二进制直接启动然后创建 WebView 实例。

6.4 CPU 闲置占用

这一点对「始终在后台运行的应用」至关重要。

闲置状态 CPU 占用(15 分钟后):
  Electron:  2-5% (每隔几秒 GC 小尖峰)
  Tauri:     0.1-0.5% (保持不变)

原因很简单:Tauri 没有 Node.js 运行时在后台跑 GC,没有 Chromium 的各种后台进程。它的闲置就是真·闲置。

七、代码实战:构建一个完整的跨平台文件管理器

让我们从零构建一个带标签浏览的文件管理器,覆盖大部分 Tauri 2.0 的核心概念。

7.1 项目初始化

# 使用官方脚手架(自动选择包管理器)
npm create tauri-app@latest file-explorer -- --template vue-ts

cd file-explorer

# 安装需要的插件
npm install @tauri-apps/api
npm install @tauri-apps/plugin-dialog
npm install @tauri-apps/plugin-fs
npm install @tauri-apps/plugin-shell

7.2 Rust 后端:文件系统操作

// src-tauri/src/lib.rs
use serde::Serialize;
use std::path::PathBuf;
use tauri::Manager;

#[derive(Debug, Serialize)]
struct FileEntry {
    name: String,
    path: String,
    is_dir: bool,
    size: u64,
    modified: String,
}

#[tauri::command]
fn list_directory(path: String) -> Result<Vec<FileEntry>, String> {
    let dir = PathBuf::from(&path);
    
    if !dir.is_dir() {
        return Err(format!("'{}' 不是有效的目录", path));
    }
    
    let mut entries = Vec::new();
    
    let mut read_dir = std::fs::read_dir(&dir)
        .map_err(|e| format!("读取目录失败: {}", e))?;
    
    while let Some(entry) = read_dir.next().transpose()
        .map_err(|e| format!("读取目录项失败: {}", e))? 
    {
        let metadata = entry.metadata()
            .map_err(|e| format!("获取文件元数据失败: {}", e))?;
        
        let modified = metadata.modified()
            .ok()
            .map(|t| {
                let duration = t.duration_since(std::time::UNIX_EPOCH).unwrap();
                chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0)
                    .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
                    .unwrap_or_else(|| "未知".to_string())
            })
            .unwrap_or_else(|| "未知".to_string());
        
        entries.push(FileEntry {
            name: entry.file_name().to_string_lossy().to_string(),
            path: entry.path().to_string_lossy().to_string(),
            is_dir: metadata.is_dir(),
            size: metadata.len(),
            modified,
        });
    }
    
    // 目录优先,再按名称排序
    entries.sort_by(|a, b| {
        if a.is_dir != b.is_dir {
            b.is_dir.cmp(&a.is_dir)
        } else {
            a.name.to_lowercase().cmp(&b.name.to_lowercase())
        }
    });
    
    Ok(entries)
}

#[tauri::command]
fn get_file_preview(path: String, max_bytes: u64) -> Result<String, String> {
    let file = PathBuf::from(&path);
    
    if !file.is_file() {
        return Err("路径不是文件".to_string());
    }
    
    // 只读取前 max_bytes 字节用于预览
    let mut f = std::fs::File::open(&file)
        .map_err(|e| format!("打开文件失败: {}", e))?;
    
    use std::io::Read;
    let mut buffer = vec![0; max_bytes as usize];
    let n = f.read(&mut buffer)
        .map_err(|e| format!("读取文件失败: {}", e))?;
    buffer.truncate(n);
    
    // 检测是否为二进制文件
    if buffer.iter().take(1024).any(|&b| b == 0) {
        return Ok("[二进制文件,无法预览]".to_string());
    }
    
    String::from_utf8(buffer)
        .map_err(|e| format!("文件包含非 UTF-8 内容: {}", e))
}

#[tauri::command]
fn get_disk_info() -> Result<Vec<DiskInfo>, String> {
    // 跨平台磁盘信息获取
    #[cfg(target_os = "macos")]
    {
        let output = std::process::Command::new("df")
            .args(["-H", "--output=target,size,used,avail"])
            .output()
            .map_err(|e| format!("执行 df 失败: {}", e))?;
        // 解析输出...
        Ok(parse_df_output(&String::from_utf8_lossy(&output.stdout)))
    }
    // 略去 Windows 和 Linux 的实现...
}

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_fs::init())
        .setup(|app| {
            // 为文件管理器创建多标签支持
            let _ = app.manage(AppState::default());
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            list_directory,
            get_file_preview,
            get_disk_info,
        ])
        .run(tauri::generate_context!())
        .expect("启动应用时发生错误");
}

7.3 前端:Vue 3 组件

<!-- src/App.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'

interface FileEntry {
  name: string
  path: string
  is_dir: boolean
  size: number
  modified: string
}

const currentPath = ref('/')
const entries = ref<FileEntry[]>([])
const previewContent = ref('')
const selectedFile = ref<FileEntry | null>(null)
const history = ref<string[]>([])
const forwardHistory = ref<string[]>([])
const isLoading = ref(false)

async function loadDirectory(path: string) {
  isLoading.value = true
  try {
    entries.value = await invoke<FileEntry[]>('list_directory', { path })
    currentPath.value = path
  } catch (e) {
    console.error('加载目录失败:', e)
  } finally {
    isLoading.value = false
  }
}

async function navigateToDir(entry: FileEntry) {
  if (!entry.is_dir) return
  
  history.value.push(currentPath.value)
  forwardHistory.value = []
  await loadDirectory(entry.path)
}

async function goBack() {
  if (history.value.length === 0) return
  const prev = history.value.pop()!
  forwardHistory.value.push(currentPath.value)
  await loadDirectory(prev)
}

async function goForward() {
  if (forwardHistory.value.length === 0) return
  const next = forwardHistory.value.pop()!
  history.value.push(currentPath.value)
  await loadDirectory(next)
}

async function openFileDialog() {
  const selected = await open({
    directory: true,
    multiple: false,
    title: '选择目录'
  })
  if (selected) {
    await loadDirectory(selected as string)
  }
}

function formatFileSize(bytes: number): string {
  if (bytes === 0) return '0 B'
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
  const k = 1024
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + units[i]
}

async function selectFile(entry: FileEntry) {
  if (entry.is_dir) return
  selectedFile.value = entry
  try {
    previewContent.value = await invoke<string>('get_file_preview', {
      path: entry.path,
      maxBytes: 4096 // 只预览前 4KB
    })
  } catch (e) {
    previewContent.value = '无法预览此文件'
  }
}

onMounted(async () => {
  // 从用户家目录开始
  const homeDir = await invoke<string>('get_home_dir')
  await loadDirectory(homeDir)
})
</script>

<template>
  <div class="file-explorer">
    <!-- 工具栏 -->
    <div class="toolbar">
      <button @click="goBack" :disabled="history.length === 0">← 后退</button>
      <button @click="goForward" :disabled="forwardHistory.length === 0">前进 →</button>
      <button @click="openFileDialog">📂 选择目录</button>
      <span class="path">{{ currentPath }}</span>
    </div>
    
    <!-- 主区域 -->
    <div class="main-area">
      <!-- 文件列表 -->
      <div class="file-list">
        <div v-if="isLoading" class="loading">加载中...</div>
        <div v-else>
          <div
            v-for="entry in entries"
            :key="entry.path"
            class="file-item"
            :class="{ selected: selectedFile?.path === entry.path }"
            @dblclick="navigateToDir(entry)"
            @click="selectFile(entry)"
          >
            <span class="icon">{{ entry.is_dir ? '📁' : '📄' }}</span>
            <span class="name">{{ entry.name }}</span>
            <span class="size">{{ entry.is_dir ? '-' : formatFileSize(entry.size) }}</span>
            <span class="modified">{{ entry.modified }}</span>
          </div>
        </div>
      </div>
      
      <!-- 预览面板 -->
      <div class="preview-panel" v-if="selectedFile">
        <h3>{{ selectedFile.name }}</h3>
        <pre>{{ previewContent }}</pre>
      </div>
    </div>
  </div>
</template>

7.4 打包与分发

# 开发模式
npm run tauri dev

# 构建桌面端安装包
npm run tauri build
# 输出: src-tauri/target/release/bundle/
#   macOS:  .dmg / .app
#   Windows: .msi / .exe
#   Linux:   .deb / .AppImage

# 构建移动端
npm run tauri build -- --target ios
npm run tauri build -- --target android

值得注意的是,Tauri 的构建系统会自动处理签名、公证(macOS notarization)、安装包生成。你只需要配好 tauri.conf.json 中的 bundle 参数。

八、生产级优化:从原型到产品

写一个 Demo 很容易,但把它变成产品级应用需要注意以下问题。

8.1 Rust 侧性能优化

// 使用 Rayon 并行处理大量文件
#[tauri::command]
fn batch_process_files(paths: Vec<String>) -> Result<Vec<ProcessedResult>, String> {
    use rayon::prelude::*;
    
    let results: Vec<ProcessedResult> = paths
        .par_iter()
        .map(|path| {
            // 并行处理每个文件
            process_file(path)
        })
        .collect();
    
    Ok(results)
}

// 对大文件使用流式 IO
#[tauri::command]
fn search_in_large_file(path: String, pattern: String) -> Result<Vec<String>, String> {
    use std::io::{BufRead, BufReader};
    
    let file = std::fs::File::open(&path)
        .map_err(|e| format!("打开文件失败: {}", e))?;
    
    let reader = BufReader::new(file);
    let re = regex::Regex::new(&pattern)
        .map_err(|e| format!("正则无效: {}", e))?;
    
    let matches: Vec<String> = reader
        .lines()
        .filter_map(|line| line.ok())
        .filter(|line| re.is_match(line))
        .take(1000) // 限制结果数量
        .collect();
    
    Ok(matches)
}

8.2 前端渲染优化

Tauri 使用系统 WebView,前端优化策略和普通 Web 应用一致:

// 使用虚拟列表渲染大文件列表
import { ref, computed } from 'vue'

const entries = ref<FileEntry[]>([])
const currentPage = ref(0)
const pageSize = 100

const paginatedEntries = computed(() => {
  const start = currentPage.value * pageSize
  return entries.value.slice(start, start + pageSize)
})

// 使用 Web Workers 处理大文件预览(Tauri 支持)
const worker = new Worker(new URL('./preview.worker.ts', import.meta.url), {
  type: 'module'
})

8.3 状态持久化

use serde::{Deserialize, Serialize};
use std::sync::Mutex;

#[derive(Debug, Serialize, Deserialize, Default)]
struct AppState {
    last_directory: String,
    window_size: (u32, u32),
    bookmarks: Vec<String>,
    theme: String,
}

struct AppStateManager(Mutex<AppState>);

#[tauri::command]
fn save_state(app: tauri::AppHandle, state: AppState) -> Result<(), String> {
    let config_path = app.path().app_config_dir()
        .map_err(|e| format!("获取配置目录失败: {}", e))?
        .join("state.json");
    
    let json = serde_json::to_string_pretty(&state)
        .map_err(|e| format!("序列化失败: {}", e))?;
    
    std::fs::write(&config_path, json)
        .map_err(|e| format!("写入状态失败: {}", e))?;
    
    Ok(())
}

#[tauri::command]
fn load_state(app: tauri::AppHandle) -> Result<AppState, String> {
    let config_path = app.path().app_config_dir()
        .map_err(|e| format!("获取配置目录失败: {}", e))?
        .join("state.json");
    
    if !config_path.exists() {
        return Ok(AppState::default());
    }
    
    let json = std::fs::read_to_string(&config_path)
        .map_err(|e| format!("读取状态失败: {}", e))?;
    
    serde_json::from_str(&json)
        .map_err(|e| format!("解析状态失败: {}", e))
}

8.4 自动更新

// Cargo.toml 中添加
// tauri-plugin-updater = "2"

use tauri_plugin_updater::UpdaterExt;

pub fn run() {
    tauri::Builder::default()
        .plugin(
            tauri_plugin_updater::Builder::new()
                .pubkey("你的公钥")
                .build()
        )
        .setup(|app| {
            // 启动时检查更新
            let handle = app.handle().clone();
            tauri::async_runtime::spawn(async move {
                if let Ok(Some(update)) = handle.updater().check().await {
                    let downloaded = update.download_and_install()
                        .await
                        .expect("下载更新失败");
                    println!("更新已下载并安装: {:?}", downloaded);
                }
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error")
}

更新服务器只需要返回一个 JSON 文件:

{
  "version": "2.0.1",
  "notes": "修复了文件搜索内存泄漏的问题",
  "pub_date": "2026-07-18T00:00:00Z",
  "platforms": {
    "darwin-x86_64": {
      "signature": "dW50...",
      "url": "https://updates.example.com/app/2.0.1/app-x86_64.dmg"
    },
    "darwin-aarch64": {
      "signature": "...",
      "url": "https://updates.example.com/app/2.0.1/app-aarch64.dmg"
    },
    "windows-x86_64": {
      "signature": "...",
      "url": "https://updates.example.com/app/2.0.1/app.msi"
    }
  }
}

九、生态全景与选型指南

9.1 什么时候选 Tauri?

  • 系统工具类应用:文件管理器、终端、监控面板。这些应用需要小包体、低内存、原生体验。
  • 移动 + 桌面同时覆盖:Tauri 2.0 的移动端支持让你用一个代码库覆盖 5 个平台。
  • 安全性敏感:企业级应用的内网工具,Rust 的内存安全和权限模型是刚需。
  • 性能关键:需要处理大量数据(日志分析、图像处理、CSV 预览),Rust 后端让你有 C 级别的性能。

9.2 什么时候慎选 Tauri?

  • 原型快速验证:如果你的目标是「明天上线看看效果」,Electron 的零 Rust 门槛更合适。
  • 重度依赖 Node.js 生态:如果你的应用深度集成 node_modules 里的某个 C++ addon,迁移成本可能很高。
  • 极端兼容性需求:某些 Linux 发行版的 WebView2/WebKitGTK 版本老旧,你可能需要自己处理兼容性。

9.3 横向对比

维度Tauri 2.0ElectronFlutter Desktop.NET MAUI
包体3-10MB120-200MB15-30MB50-100MB
内存20-80MB100-400MB30-100MB50-150MB
UI 框架任意 Web 框架任意 Web 框架Flutter WidgetXAML/Blazor
后端语言RustNode.js/JSDartC#
移动支持✅ iOS+Android✅ iOS+Android✅ iOS+Android
学习曲线中(需 Rust 基础)
生态成熟度发展中成熟成熟中等

十、未来展望:Tauri 的「桌面即 Web」愿景

Tauri 2.0 只是万里长征的第一步。从路线图来看,几个方向值得关注:

  1. WebView 碎片化问题的解决:社区正在推动 tauri-driver 统一跨平台 WebView 的行为,减少兼容性问题。

  2. Rust 核心进一步瘦身:当前启动时仍需加载 std,未来可能切换到 no_std 或精简版运行时。

  3. 声明式窗口管理:类似 SwiftUI 的声明式窗口语法正在 RFC 讨论中。

  4. AI 原生集成:利用 Rust 的 candlellama.cpp 绑定的本地推理能力,让桌面应用直接跑本地模型。

  5. 后端分享生态:Tauri 插件市场正在增长,目标是为每个常见的桌面功能提供一个官方或社区插件。

总结

Tauri 2.0 不是 Electron 的简单替代品——它在架构哲学上是完全不同的物种。Electron 选择了「打包一切」的确定性,Tauri 选择了「信任系统」的轻量性。前者适合快速迭代的内部工具,后者适合交付给终端用户的产品级应用。

代码量?Tauri 可以用 Rust 完成 Electron 需要 2-3 个 npm 包 + 大量样板代码才能搞定的工作。安全性?Tauri 默认让你从头思考「哪些 API 可以暴露给前端」,而不是 Electron 的「所有 API 默认可用,你自己去关」。

最重要的是,Tauri 2.0 的移动端支持意味着:一套 Rust 后端 + 一套前端代码 = 桌面 + Web + iOS + Android。在这个「全平台覆盖」越来越成为刚需的时代,这个组合的吸引力正在被越来越多的团队发现。

如果你还在用 Electron 写一个即将交付给终端用户的产品,试试 Tauri 2.0——当你的安装包从 150MB 缩到 5MB 的那一刻,你会理解 Rust 社区那句 "Fearless Concurrency" 之外的另一层含义。


参考:Tauri 官方文档、Rust 标准库文档、Electron 性能基准测试报告(2026)

推荐文章

Vue3中的虚拟滚动有哪些改进?
2024-11-18 23:58:18 +0800 CST
PHP 允许跨域的终极解决办法
2024-11-19 08:12:52 +0800 CST
Golang在整洁架构中优雅使用事务
2024-11-18 19:26:04 +0800 CST
程序员茄子在线接单