用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱

前言

最近在整理电脑文件时,发现了大量重复的照片和视频,占用了几十 GB 的空间。市面上的去重工具要么收费,要么功能臃肿,于是萌生了自己造一个的想法。

正好 Tauri 2.0 正式发布,相比 Electron,它有着更小的包体积和更好的性能。于是决定用 Tauri 2.0 + React + Rust 来实现这个工具。

最终成品:File Toolkit ------ 一个跨平台的文件工具箱,支持文件统计、文件去重、视频截取。

GitHub: github.com/220529/file...

功能展示

📊 文件统计

递归扫描文件夹,按类型统计文件数量和大小:

  • 支持拖拽选择文件夹
  • 按文件类型分组
  • 显示占比和总大小

🔍 文件去重

这是核心功能,支持:

  • 两阶段扫描:先按文件大小筛选,再计算哈希
  • xxHash3 快速哈希:比 MD5 快 5-10 倍
  • 并行计算:充分利用多核 CPU
  • 大文件采样:只读头部 + 中间 + 尾部,避免全量读取
  • 缩略图预览:图片直接显示,视频用 FFmpeg 截帧
  • 智能选择:自动选中较新的文件,保留最早的

✂️ 视频截取

  • 快速模式:无损截取(-c copy),秒级完成
  • 精确模式:重新编码,时间精确到毫秒
  • 时间轴预览:8 帧缩略图,快速定位
  • 实时进度:精确模式显示编码进度

技术选型

为什么选 Tauri 而不是 Electron?

对比项 Electron Tauri
包体积 150MB+ 10MB+
内存占用 高(Chromium) 低(系统 WebView)
后端语言 Node.js Rust
性能 一般 优秀

对于文件处理这种 CPU 密集型任务,Rust 的性能优势非常明显。

技术栈

css 复制代码
┌─────────────────────────────────────────────────────┐
│                    Frontend                          │
│         React 19 + TypeScript + Tailwind CSS        │
├─────────────────────────────────────────────────────┤
│                   Tauri IPC                          │
├─────────────────────────────────────────────────────┤
│                    Backend                           │
│                  Rust + Tauri 2.0                   │
│  ┌─────────────┬─────────────┬─────────────────┐   │
│  │ file_stats  │    dedup    │     video       │   │
│  │  walkdir    │  xxHash3    │    FFmpeg       │   │
│  │             │  rayon      │                 │   │
│  │             │  memmap2    │                 │   │
│  └─────────────┴─────────────┴─────────────────┘   │
└─────────────────────────────────────────────────────┘

核心实现

1. 文件去重算法

去重的核心是计算文件哈希,但如果对每个文件都完整计算哈希,效率会很低。我采用了两阶段策略:

第一阶段:按文件大小筛选

rust 复制代码
let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();

for entry in WalkDir::new(&path)
    .into_iter()
    .filter_map(|e| e.ok())
    .filter(|e| e.file_type().is_file())
{
    if let Ok(meta) = entry.metadata() {
        let size = meta.len();
        if size > 0 {
            size_map.entry(size).or_default().push(entry.path().to_string_lossy().to_string());
        }
    }
}

// 只对大小相同的文件计算哈希
let files_to_hash: Vec<_> = size_map
    .iter()
    .filter(|(_, files)| files.len() >= 2)
    .flat_map(|(size, files)| files.iter().map(move |f| (*size, f.clone())))
    .collect();

这一步可以过滤掉大部分文件,因为大小不同的文件肯定不重复。

第二阶段:并行计算哈希

rust 复制代码
use rayon::prelude::*;

let results: Vec<(String, FileInfo)> = files_to_hash
    .par_iter()  // 并行迭代
    .filter_map(|(size, file_path)| {
        let hash = calculate_fast_hash(Path::new(file_path), *size).ok()?;
        // ...
        Some((hash, file_info))
    })
    .collect();

使用 rayon 实现并行计算,充分利用多核 CPU。

2. 快速哈希算法

传统的 MD5 哈希速度较慢,我选择了 xxHash3,它是目前最快的非加密哈希算法之一:

rust 复制代码
fn calculate_fast_hash(path: &Path, size: u64) -> Result<String, String> {
    use memmap2::Mmap;
    use xxhash_rust::xxh3::Xxh3;

    const SMALL_FILE: u64 = 1024 * 1024;      // 1MB
    const THRESHOLD: u64 = 10 * 1024 * 1024;  // 10MB
    const SAMPLE_SIZE: usize = 1024 * 1024;   // 采样 1MB

    let file = File::open(path).map_err(|e| e.to_string())?;

    if size <= SMALL_FILE {
        // 小文件:内存映射,零拷贝
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let hash = xxhash_rust::xxh3::xxh3_64(&mmap);
        return Ok(format!("{:016x}", hash));
    }

    let mut hasher = Xxh3::new();

    if size <= THRESHOLD {
        // 中等文件:完整读取
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        hasher.update(&mmap);
    } else {
        // 大文件:只读头部 + 中间 + 尾部
        let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
        let len = mmap.len();

        hasher.update(&mmap[..SAMPLE_SIZE]);                    // 头部
        hasher.update(&mmap[len/2 - SAMPLE_SIZE/2..][..SAMPLE_SIZE]); // 中间
        hasher.update(&mmap[len - SAMPLE_SIZE..]);              // 尾部
        hasher.update(&size.to_le_bytes());                     // 文件大小
    }

    Ok(format!("{:016x}", hasher.digest()))
}

优化点

  • xxHash3:比 MD5 快 5-10 倍
  • memmap2:内存映射,零拷贝读取
  • 大文件采样:只读头中尾各 1MB,避免全量读取

3. 前后端通信

Tauri 使用 IPC 进行前后端通信。后端定义命令:

rust 复制代码
#[tauri::command]
pub async fn find_duplicates(app: AppHandle, path: String) -> Result<DedupResult, String> {
    // ...
}

前端调用:

typescript 复制代码
import { invoke } from "@tauri-apps/api/core";

const result = await invoke<DedupResult>("find_duplicates", { path });

4. 进度反馈

长时间任务需要显示进度,Tauri 支持事件机制:

后端发送进度

rust 复制代码
use tauri::{AppHandle, Emitter};

let _ = app.emit("dedup-progress", DedupProgress {
    stage: "计算文件指纹".into(),
    current,
    total: total_to_hash,
    percent,
});

前端监听

typescript 复制代码
import { listen } from "@tauri-apps/api/event";

useEffect(() => {
  const unlisten = listen<DedupProgress>("dedup-progress", (event) => {
    setProgress(event.payload);
  });
  return () => { unlisten.then((fn) => fn()); };
}, []);

5. 内嵌 FFmpeg

视频功能依赖 FFmpeg,为了让用户开箱即用,我把 FFmpeg 打包进了应用:

配置 tauri.conf.json

json 复制代码
{
  "bundle": {
    "externalBin": [
      "binaries/ffmpeg",
      "binaries/ffprobe"
    ]
  }
}

Rust 中获取路径

rust 复制代码
fn get_ffmpeg_path(app: &AppHandle) -> PathBuf {
    app.path()
        .resource_dir()
        .ok()
        .map(|p| p.join("binaries").join("ffmpeg"))
        .filter(|p| p.exists())
        .unwrap_or_else(|| PathBuf::from("ffmpeg"))  // 回退到系统 PATH
}

踩坑记录

1. Tauri 拖拽事件是全局的

最初使用 CSS hidden 隐藏非活动 Tab 来保持状态,但发现拖拽文件时所有 Tab 都会响应。

解决方案 :给每个组件传递 active 属性,只有激活的组件才监听拖拽事件。

typescript 复制代码
useEffect(() => {
  if (!active) return;  // 非激活状态不监听
  
  const unlisten = listen("tauri://drag-drop", (event) => {
    if (!active) return;
    // 处理拖拽
  });
  // ...
}, [active]);

2. 并行计算进度跳动

使用 rayon 并行计算时,多线程同时更新计数器,导致进度显示不连续。

解决方案:使用原子变量 + compare_exchange 确保进度单调递增:

rust 复制代码
let progress_counter = Arc::new(AtomicUsize::new(0));
let last_reported = Arc::new(AtomicUsize::new(0));

// 在并行迭代中
let current = progress_counter.fetch_add(1, Ordering::Relaxed) + 1;
let last = last_reported.load(Ordering::Relaxed);

if current > last && (current - last >= 20 || current == total) {
    if last_reported.compare_exchange(last, current, Ordering::SeqCst, Ordering::Relaxed).is_ok() {
        // 发送进度
    }
}

3. Release 比 Debug 快很多

开发时觉得去重速度一般,打包后发现快了好几倍。

模式 说明
Debug 无优化,保留调试信息
Release LTO、内联、循环展开等优化

对于 CPU 密集型任务,Release 版本可能快 3-5 倍。

打包发布

本地打包

bash 复制代码
# 1. 下载 FFmpeg 静态版本(macOS 示例)
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
unzip /tmp/ffmpeg.zip -d src-tauri/binaries/
mv src-tauri/binaries/ffmpeg src-tauri/binaries/ffmpeg-x86_64-apple-darwin

# 2. 执行打包
pnpm tauri build

产物:src-tauri/target/release/bundle/macos/File Toolkit.app(约 62MB,含 FFmpeg)

GitHub Actions 多平台自动打包

本地打包只能生成当前平台的安装包。要支持 Windows、Linux,需要在对应平台编译。

解决方案:用 GitHub Actions,它提供 macOS、Windows、Linux 虚拟机,可以并行打包。

scss 复制代码
git push tag v0.2.0
        ↓
GitHub Actions 触发
        ↓
┌─────────────────────────────────────────────────────┐
│  同时启动 4 台虚拟机(并行执行)                       │
├─────────────┬─────────────┬────────────┬────────────┤
│  macOS VM   │  macOS VM   │ Windows VM │  Linux VM  │
│  (Intel)    │  (ARM)      │            │            │
├─────────────┼─────────────┼────────────┼────────────┤
│ 装环境      │ 装环境       │ 装环境     │ 装环境      │
│ 下载 FFmpeg │ 下载 FFmpeg │ 下载FFmpeg │ 下载FFmpeg │
│ tauri build │ tauri build │ tauri build│ tauri build│
├─────────────┼─────────────┼────────────┼────────────┤
│   .dmg      │   .dmg      │   .msi     │   .deb     │
│  (x86_64)   │  (aarch64)  │   .exe     │  .AppImage │
└─────────────┴─────────────┴────────────┴────────────┘
        ↓
   全部上传到 GitHub Release

核心配置 .github/workflows/release.yml

yaml 复制代码
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: macos-latest
            target: x86_64-apple-darwin
          - platform: macos-latest
            target: aarch64-apple-darwin
          - platform: windows-latest
            target: x86_64-pc-windows-msvc
          - platform: ubuntu-22.04
            target: x86_64-unknown-linux-gnu

    runs-on: ${{ matrix.platform }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - uses: pnpm/action-setup@v4
      - uses: dtolnay/rust-toolchain@stable

      # 各平台下载对应的 FFmpeg
      - name: Download FFmpeg (macOS)
        if: matrix.platform == 'macos-latest'
        run: |
          curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
          # ...

      - name: Download FFmpeg (Windows)
        if: matrix.platform == 'windows-latest'
        shell: pwsh
        run: |
          Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/..." -OutFile ...
          # ...

      # 打包并上传到 Release
      - uses: tauri-apps/tauri-action@v0
        with:
          tagName: ${{ github.ref_name }}
          releaseDraft: true
          args: --target ${{ matrix.target }}

使用方式

bash 复制代码
git tag v0.2.0
git push origin v0.2.0
# 自动触发,完成后去 Releases 页面发布

为什么能跨平台?

  • 前端:React 编译成 HTML/CSS/JS,哪都能跑
  • 后端:Rust 在目标平台的虚拟机上编译,生成原生二进制
  • FFmpeg:每个平台下载对应的静态版本

本质是在真实目标平台上编译,GitHub 免费提供这些虚拟机。

总结

这个项目让我对 Tauri 2.0 有了更深的理解:

  1. Rust 性能确实强:文件哈希、并行计算等场景优势明显
  2. Tauri 开发体验不错:前后端分离,IPC 通信简单
  3. 包体积小:不含 FFmpeg 只有 10MB 左右
  4. 跨平台:一套代码,多端运行

如果你也想尝试 Tauri,这个项目可以作为参考。

GitHub: github.com/220529/file...

欢迎 Star ⭐️


相关文章

相关推荐
码界奇点2 小时前
基于React与TypeScript的后台管理系统设计与实现
前端·c++·react.js·typescript·毕业设计·源代码管理
前端小咸鱼一条3 小时前
Redux
react.js·前端框架
zhenryx3 小时前
React Native 横向滚动指示器组件库(淘宝|京东...&旧版|新版)
javascript·react native·react.js
superman超哥3 小时前
Rust Link-Time Optimization (LTO):跨边界的全局优化艺术
开发语言·后端·rust·lto·link-time·跨边界·优化艺术
superman超哥3 小时前
Rust 编译优化选项配置:释放性能潜力的精细调控
开发语言·后端·rust·rust编译优化·精细调控·编译优化选项
黎明初时3 小时前
react基础框架搭建4-tailwindcss配置:react+router+redux+axios+Tailwind+webpack
前端·react.js·webpack·前端框架
superman超哥4 小时前
Rust 日志级别与结构化日志:生产级可观测性实践
开发语言·后端·rust·可观测性·rust日志级别·rust结构化日志
Mintopia5 小时前
🧠 从零开始:纯手写一个支持流式 JSON 解析的 React Renderer
前端·数据结构·react.js
superman超哥5 小时前
Rust 减少内存分配策略:性能优化的内存管理艺术
开发语言·后端·性能优化·rust·内存管理·内存分配策略