之前用 Electron 做过几个桌面应用,打包出来动不动就 100MB 起步。这次想做个音乐播放器,听说 Tauri 2 打包体积小很多,就试试看。项目做完了,确实挺爽的,打包出来只有 3.5MB。
项目介绍
这个音乐播放器功能不算复杂,主要是:
- 导入本地音乐文件夹,支持 MP3、WAV、FLAC、M4A、AAC、OGG
- 播放控制(播放/暂停、上一首/下一首、进度条拖动)
- 显示歌曲信息(标题、艺术家、专辑、时长)
- 自动加载封面和歌词
- 播放列表管理
- 数据持久化(重启后恢复播放列表)
技术栈是 Tauri 2 + Vue 3 + TypeScript,UI 用了 Element Plus。
为什么选 Tauri 2
之前一直用 Electron,这次想试试 Tauri 2,主要原因是打包体积。Electron 每个应用都自带了 Chromium 和 Node.js,所以体积很大。Tauri 用的是系统自带的 WebView,所以打包出来小很多。
除了体积,Tauri 的性能也更好,启动快,内存占用少。不过对于我这个简单的音乐播放器来说,性能差别不是特别明显,主要还是体积优势。
开发心得体会
1. 插件系统很好用
Tauri 2 的插件系统设计得不错,常用的功能都有现成的插件。这个项目用到了这几个插件:
@tauri-apps/plugin-dialog:选择文件夹@tauri-apps/plugin-fs:读取文件@tauri-apps/plugin-store:数据持久化
这些插件都有完整的 TypeScript 类型定义,用起来很顺手。而且不需要自己写 Rust 代码,对前端开发者很友好。
2. 文件读取有点绕
一开始想直接用 HTML5 的 Audio 标签播放本地文件,但发现浏览器安全限制不能直接访问本地文件路径。只能用 Tauri 的 fs 插件读取文件内容,转成 Blob,然后用 URL.createObjectURL 创建可访问的 URL。
const fileContent = await readFile(filePath);
const blob = new Blob([fileContent]);
const audioUrl = URL.createObjectURL(blob);
const audio = new Audio(audioUrl);
这个方案虽然能工作,但感觉有点绕。不过这是浏览器安全限制,没办法。
3. 音频元数据读取
需要读取歌曲的标题、艺术家、专辑等信息。Tauri 没有现成的插件,只能在前端用 jsmediatags 库。
这个库有点老,但还能用。需要注意的是它只能读取 Blob 对象,所以还是得先读取文件内容转成 Blob。
const fileContent = await readFile(filePath);
const blob = new Blob([fileContent]);
jsmediatags.read(blob, {
onSuccess: (tag) => {
artist = tag.tags.artist;
album = tag.tags.album;
},
onError: (error) => {
console.error('读取失败:', error);
}
});
如果音频文件没有元数据,就用文件名作为标题,"未知艺术家"作为艺术家。
4. 封面和歌词自动加载
封面和歌词文件需要和音乐文件同名,但扩展名不同。一开始写死了扩展名,后来改成支持多种格式。
const coverExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
const lyricsExtensions = ['.lrc'];
// 尝试加载封面
for (const ext of coverExtensions) {
const coverPath = fileNameWithoutExt + ext;
try {
await readFile(coverPath);
cover = coverPath;
break;
} catch {
continue;
}
}
这样用户只要把封面图片和歌词文件放在音乐文件同目录下,命名相同就能自动加载。
5. 歌词同步
LRC 歌词需要根据播放时间实时显示。一开始想用 setInterval 定时检查,但这样性能不太好。
后来改用 Audio 的 timeupdate 事件,这样更高效。
audio.addEventListener('timeupdate', () => {
updateCurrentLyric(audio.currentTime);
});
function updateCurrentLyric(currentTime: number) {
let currentIndex = -1;
for (let i = 0; i < currentLyrics.value.length; i++) {
if (currentTime >= currentLyrics.value[i].time) {
currentIndex = i;
} else {
break;
}
}
if (currentIndex >= 0) {
currentLyricText.value = currentLyrics.value[currentIndex].text;
}
}
6. 数据持久化
需要保存播放列表和音乐文件夹路径。一开始想用 localStorage,但 Tauri 的环境里 localStorage 可能不太可靠。
后来用 Tauri 的 @tauri-apps/plugin-store 插件,它会专门创建一个 JSON 文件来存储数据,比 localStorage 更可靠。
const store = await Store.load('music-store.json');
// 保存数据
await store.set('songList', songList);
await store.set('musicFolder', folderPath);
await store.save();
// 读取数据
const savedSongList = await store.get('songList');
const savedFolder = await store.get('musicFolder');
7. 递归扫描文件夹
用户选择一个文件夹后,需要递归扫描所有子文件夹,找到所有音乐文件。
const entries = await readDir(folderPath, { recursive: true });
function collectMusicFiles(entries: any[], basePath: string) {
for (const entry of entries) {
const fullPath = basePath + '\\' + entry.name;
if (!entry.children) {
// 是文件,检查是否是音乐文件
if (supportedFormats.some(format => entry.name.endsWith(format))) {
musicFiles.push({ name: entry.name, path: fullPath });
}
} else {
// 是文件夹,递归扫描
collectMusicFiles(entry.children, fullPath);
}
}
}
8. 进度条拖动
进度条拖动需要处理两个问题:一是拖动时要暂停播放,二是拖动结束后要跳转到指定位置。
function handleProgressChange(value: number) {
if (audioPlayer) {
const seekTime = (value / 100) * audioPlayer.duration;
audioPlayer.currentTime = seekTime;
progress.value = value;
currentSong.value.currentTime = seekTime;
}
}
Element Plus 的 Slider 组件用起来很方便,直接绑定 v-model 就行。
9. 类型安全
Tauri 的 API 都是 TypeScript 类型定义好的,开发体验不错。比如 readFile 返回的是 Uint8Array,类型很明确。
const fileContent: Uint8Array = await readFile(filePath);
这样写代码时 IDE 能给出很好的提示,减少错误。
10. 配置简单
相比 Electron 的配置,Tauri 的配置更简单清晰。大部分配置都在 src-tauri/capabilities/default.json 里,需要什么权限就声明什么权限。
{
"identifier": "default",
"description": "Default capability",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"fs:default",
"store:default"
]
}
遇到的坑
1. Blob 内存泄漏
用 URL.createObjectURL 创建的 URL 需要手动释放,否则会内存泄漏。
const audioUrl = URL.createObjectURL(blob);
// 使用完后
URL.revokeObjectURL(audioUrl);
不过对于音乐播放器来说,音频 URL 一直要用到,所以不需要释放。但如果频繁创建和销毁,就要注意这个问题。
2. 音频时长获取
获取音频时长需要等待 loadedmetadata 事件,不能直接读取 duration 属性。
audio.addEventListener('loadedmetadata', () => {
const duration = audio.duration;
});
而且有些音频文件可能没有元数据,duration 会是 NaN,需要处理这种情况。
3. 歌词解析
LRC 歌词的格式是 [分:秒.毫秒]歌词内容,需要用正则表达式解析。
const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/);
if (match) {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = parseInt(match[3].padEnd(3, '0'));
const time = minutes * 60 + seconds + milliseconds / 1000;
const text = match[4].trim();
}
毫秒部分可能是 2 位或 3 位,需要补齐到 3 位。
总结
这次用 Tauri 2 做音乐播放器的体验还不错。相比 Electron,最大的优势是打包体积小了很多,从 100MB 降到了 3.5MB。性能也有提升,不过对于这个简单的应用来说差别不是特别明显。
Tauri 2 的插件系统很好用,大部分功能都有现成的插件,不需要自己写 Rust 代码。对于前端开发者来说,学习成本不算太高。
如果你要做桌面应用,我建议:
- 如果对打包体积和性能要求高,用 Tauri
- 如果需要用到很多 Node.js 的生态,或者团队更熟悉 JavaScript,用 Electron
- 如果两个都能接受,可以都试试,看哪个更顺手
总的来说,Tauri 2 是个值得尝试的框架,特别是对于前端开发者来说,学习成本不算太高,而且能带来实实在在的好处。
项目地址
项目源码已开源,欢迎 Star 和 Fork: