Tauri 2.0 实战:打造「小文喵」文件工具箱
前言
最近在整理电脑文件时,发现了大量重复的照片和视频,占用了几十 GB 的空间。市面上的去重工具要么收费,要么功能臃肿,于是萌生了自己造一个的想法。
正好 Tauri 2.0 正式发布,相比 Electron 有着更小的包体积和更好的性能,于是决定用 Tauri 2.0 + React + Rust 来实现。
做着做着,需求就多了起来:
- 文件去重 —— 最初的需求,整理重复文件
- 视频截取 —— 有时候只想要视频的一小段,不想装 PR
- 格式转换 —— iPhone 录的 MOV 想转成 MP4,还有些视频想转 GIF
- 去水印 —— 想给应用换个好看的图标,用豆包 AI 生成了一张,结果右下角有水印。去网上搜"去水印",要么收费要么要注册,就为了去个小水印?算了,自己加一个
于是就有了「小文喵」—— 一个跨平台的文件工具箱。
GitHub: github.com/220529/file…
功能展示
📊 文件统计
递归扫描文件夹,按类型统计文件数量和大小。
🔍 文件去重
这是核心功能:
- 两阶段扫描:先按文件大小筛选,再计算哈希
- xxHash3 快速哈希:比 MD5 快 5-10 倍
- 并行计算:充分利用多核 CPU
- 大文件采样:只读头部 + 中间 + 尾部,避免全量读取
- 缩略图预览:图片直接显示,视频用 FFmpeg 截帧
✂️ 视频截取
- 快速模式:无损截取(-c copy),秒级完成
- 精确模式:重新编码,时间精确到毫秒
- 时间轴预览:8 帧缩略图,快速定位
🔄 格式转换
- 支持 MOV、MP4、GIF 互转
- 批量转换
- 画质可选(高/中/低)
- 实时进度显示
✨ 去水印
最后加的功能:
- 高斯模糊:让水印区域变糊
- 颜色覆盖:用颜色盖住水印(适合纯色背景)
- 取色器:点击打开系统调色板,或右键图片取色
- 选区操作:拖动移动、拖动边角缩放
技术选型
为什么选 Tauri 而不是 Electron?
| 对比项 | Electron | Tauri |
|---|---|---|
| 包体积 | 150MB+ | 10MB+ |
| 内存占用 | 高(Chromium) | 低(系统 WebView) |
| 后端语言 | Node.js | Rust |
| 性能 | 一般 | 优秀 |
对于文件处理这种 CPU 密集型任务,Rust 的性能优势非常明显。
技术栈
┌─────────────────────────────────────────────────────┐
│ Frontend │
│ React 19 + TypeScript + Tailwind CSS │
├─────────────────────────────────────────────────────┤
│ Tauri IPC │
├─────────────────────────────────────────────────────┤
│ Backend │
│ Rust + Tauri 2.0 │
│ ┌──────────┬────────┬────────┬─────────┬────────┐ │
│ │file_stats│ dedup │ video │ convert │watermark│ │
│ │ walkdir │xxHash3 │ FFmpeg │ FFmpeg │ FFmpeg │ │
│ │ │ rayon │ │ │ │ │
│ └──────────┴────────┴────────┴─────────┴────────┘ │
└─────────────────────────────────────────────────────┘
核心实现
1. 文件去重算法
去重的核心是计算文件哈希,但如果对每个文件都完整计算哈希,效率会很低。我采用了两阶段策略:
第一阶段:按文件大小筛选
let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();
for entry in WalkDir::new(&path).into_iter().filter_map(|e| e.ok()) {
if let Ok(meta) = entry.metadata() {
size_map.entry(meta.len()).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();
第二阶段:并行计算哈希
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();
2. 快速哈希算法
选择 xxHash3,目前最快的非加密哈希算法之一:
fn calculate_fast_hash(path: &Path, size: u64) -> Result<String, String> {
use memmap2::Mmap;
use xxhash_rust::xxh3::Xxh3;
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())?;
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
if size <= THRESHOLD {
// 小文件:完整读取
let hash = xxhash_rust::xxh3::xxh3_64(&mmap);
return Ok(format!("{:016x}", hash));
}
// 大文件:只读头部 + 中间 + 尾部
let mut hasher = Xxh3::new();
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. 去水印实现
去水印有两种模式:
高斯模糊:用 FFmpeg 的 boxblur 滤镜
let filter = format!(
"[0:v]crop={}:{}:{}:{}[crop];[crop]boxblur=15:3[blur];[0:v][blur]overlay={}:{}",
width, height, x, y, x, y
);
Command::new("ffmpeg")
.args(["-y", "-i", &input_path, "-filter_complex", &filter, "-q:v", "1", &output_path])
.output()
颜色覆盖:用 drawbox 滤镜
// 注意:颜色格式要从 #ffffff 转成 0xffffff
let ffmpeg_color = if color.starts_with('#') {
format!("0x{}", &color[1..])
} else {
color.clone()
};
let filter = format!(
"drawbox=x={}:y={}:w={}:h={}:color={}:t=fill",
x, y, width, height, ffmpeg_color
);
4. 格式转换
批量转换的关键是进度反馈:
// 后端发送进度
let _ = app.emit("convert-progress", ConvertProgress {
current_file: file_name,
current: index + 1,
total: files.len(),
percent,
});
// 前端监听
useEffect(() => {
const unlisten = listen<ConvertProgress>("convert-progress", (event) => {
setProgress(event.payload);
});
return () => { unlisten.then((fn) => fn()); };
}, []);
踩坑记录
开发过程中踩了不少坑,记录下来供参考。
1. macOS 图标白底问题
这个坑花了不少时间。用豆包生成的图标有透明背景,结果在 macOS 上显示时自动加了白底,很丑。
原因:macOS Big Sur 开始,所有 App 图标强制使用 squircle(圆角方形)形状。透明背景的图标会被系统自动加白底。
解决方案:图标设计时直接使用带背景色的 squircle 形状,不要用透明背景。让 AI 生成时就指定"squircle 形状,带背景色"。
2. DMG 打包失败 (macOS)
执行 pnpm tauri build 时报错:
failed to bundle project error running bundle_dmg.sh
原因:macOS 上打包 DMG 需要 create-dmg 工具。
解决方案:
brew install create-dmg
3. FFmpeg 滤镜语法
高斯模糊一开始用 -vf 参数,结果报错。
原因:复杂滤镜(涉及多个输入输出流)需要用 -filter_complex。
# 错误 ❌
ffmpeg -i input.jpg -vf "split[a][b];..." output.jpg
# 正确 ✅
ffmpeg -i input.jpg -filter_complex "[0:v]crop=...;..." output.jpg
4. 颜色格式不兼容
FFmpeg 的 drawbox 滤镜不认 #ffffff 格式。
解决方案:转成 0xffffff 格式:
let ffmpeg_color = if color.starts_with('#') {
format!("0x{}", &color[1..])
} else {
color.clone()
};
5. Tauri 拖拽事件是全局的
最初用 CSS hidden 隐藏非活动 Tab,但发现拖拽文件时所有 Tab 都会响应。
解决方案:给每个组件传递 active 属性,只有激活的组件才监听拖拽事件:
useEffect(() => {
if (!active) return; // 非激活状态不监听
const unlisten = listen("tauri://drag-drop", handler);
return () => { unlisten.then((fn) => fn()); };
}, [active]);
6. 并行计算进度跳动
使用 rayon 并行计算时,多线程同时更新计数器,导致进度显示不连续。
解决方案:使用原子变量 + compare_exchange:
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 {
if last_reported.compare_exchange(last, current, Ordering::SeqCst, Ordering::Relaxed).is_ok() {
// 发送进度
}
}
7. Dev 模式 Dock 显示英文名 (macOS)
开发模式下 macOS Dock 显示的是 Cargo 包名(英文),不是配置的中文名。
原因:这是正常的,Cargo 包名不支持中文。打包后会显示 tauri.conf.json 中配置的 productName。
8. Release 比 Debug 快很多
开发时觉得去重速度一般,打包后发现快了好几倍。
原因:Debug 模式无优化,Release 模式有 LTO、内联等优化。对于 CPU 密集型任务,Release 版本可能快 3-5 倍。
9. 打包后找不到 FFmpeg
开发模式正常,打包后报错找不到 ffmpeg。
原因:Tauri 的 externalBin 在开发模式和打包后的路径不同。开发时在 src-tauri/binaries/,打包后在可执行文件同目录,且文件名不带平台后缀。
解决方案:封装一个统一的路径查找函数,按优先级查找:
pub fn get_ffmpeg_path(app: &AppHandle) -> PathBuf {
let exe_name = if cfg!(windows) { "ffmpeg.exe" } else { "ffmpeg" };
// 1. 可执行文件同目录(打包后)
if let Ok(exe_path) = std::env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let path = exe_dir.join(exe_name);
if path.exists() { return path; }
}
}
// 2. Tauri resource_dir(开发模式)
if let Ok(resource_dir) = app.path().resource_dir() {
let path = resource_dir.join(exe_name);
if path.exists() { return path; }
}
// 3. 回退到系统 PATH
PathBuf::from(exe_name)
}
10. GitHub Release 显示旧版本
推送了新 tag,但 Release 页面还是显示旧版本为 "Latest"。
原因:workflow 配置了 releaseDraft: true,新版本都是草稿状态。
解决方案:改成 releaseDraft: false,或者手动去 Release 页面发布草稿。
11. Windows 开发环境配置
Windows 上开发 Tauri 需要额外安装 MSVC 编译工具链,否则 cargo build 会报错找不到链接器:
error: linker `link.exe` not found
解决方案:
# 安装 Visual Studio Build Tools(约 2-3GB)
winget install Microsoft.VisualStudio.2022.BuildTools --override "--wait --passive --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"
另外 FFmpeg 需要手动下载 Windows 版本(推荐 gyan.dev),解压后重命名放到 src-tauri/binaries/:
ffmpeg-x86_64-pc-windows-msvc.exeffprobe-x86_64-pc-windows-msvc.exe
打包发布
本地打包
# 1. 安装 DMG 打包工具(macOS)
brew install create-dmg
# 2. 下载 FFmpeg 静态版本
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
# 3. 执行打包
pnpm tauri build
GitHub Actions 多平台自动打包
本地只能打包当前平台。要支持 Windows、Linux,用 GitHub Actions:
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:
# ... 安装环境、下载 FFmpeg、打包
推送 tag 后自动触发,4 个平台并行打包,完成后上传到 Release。
版本管理脚本
写了个 tag.sh 脚本简化发版流程:
./tag.sh
# 📌 当前版本: v0.2.5
# 选择操作:
# 1) 补丁版本 v0.2.6 (bug修复)
# 2) 次版本 v0.3.0 (新功能)
# 3) 主版本 v1.0.0 (重大更新)
脚本会自动更新 tauri.conf.json 和 package.json 中的版本号,提交并推送 tag。
总结
从整理重复文件的小需求开始,一路加功能,最终做成了「小文喵」这个小工具。
这个项目让我对 Tauri 2.0 有了更深的理解:
- Rust 性能确实强:文件哈希、并行计算等场景优势明显
- Tauri 开发体验不错:前后端分离,IPC 通信简单
- 包体积小:不含 FFmpeg 只有 10MB 左右
- 跨平台:一套代码,多端运行
踩的坑也不少,但都是宝贵的经验。希望这篇文章对想尝试 Tauri 的同学有所帮助。
GitHub: github.com/220529/file…
欢迎 Star ⭐️
相关文章: