从 Electron 到 Tauri 2:我用 3.5MB 做了个音乐播放器

之前用 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。

typescript 复制代码
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。

typescript 复制代码
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. 封面和歌词自动加载

封面和歌词文件需要和音乐文件同名,但扩展名不同。一开始写死了扩展名,后来改成支持多种格式。

typescript 复制代码
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 事件,这样更高效。

typescript 复制代码
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 更可靠。

typescript 复制代码
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. 递归扫描文件夹

用户选择一个文件夹后,需要递归扫描所有子文件夹,找到所有音乐文件。

typescript 复制代码
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. 进度条拖动

进度条拖动需要处理两个问题:一是拖动时要暂停播放,二是拖动结束后要跳转到指定位置。

typescript 复制代码
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,类型很明确。

typescript 复制代码
const fileContent: Uint8Array = await readFile(filePath);

这样写代码时 IDE 能给出很好的提示,减少错误。

10. 配置简单

相比 Electron 的配置,Tauri 的配置更简单清晰。大部分配置都在 src-tauri/capabilities/default.json 里,需要什么权限就声明什么权限。

json 复制代码
{
  "identifier": "default",
  "description": "Default capability",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "dialog:default",
    "fs:default",
    "store:default"
  ]
}

遇到的坑

1. Blob 内存泄漏

用 URL.createObjectURL 创建的 URL 需要手动释放,否则会内存泄漏。

typescript 复制代码
const audioUrl = URL.createObjectURL(blob);
// 使用完后
URL.revokeObjectURL(audioUrl);

不过对于音乐播放器来说,音频 URL 一直要用到,所以不需要释放。但如果频繁创建和销毁,就要注意这个问题。

2. 音频时长获取

获取音频时长需要等待 loadedmetadata 事件,不能直接读取 duration 属性。

typescript 复制代码
audio.addEventListener('loadedmetadata', () => {
  const duration = audio.duration;
});

而且有些音频文件可能没有元数据,duration 会是 NaN,需要处理这种情况。

3. 歌词解析

LRC 歌词的格式是 [分:秒.毫秒]歌词内容,需要用正则表达式解析。

typescript 复制代码
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:

GitHub - black542684/music-desktop

相关推荐
aykon2 小时前
DataSource详解以及优势
前端
Mintopia2 小时前
戴了 30 天智能手环后,我才发现自己一直低估了“睡眠”
前端
leolee182 小时前
react redux 简单使用
前端·react.js·redux
Fisschl2 小时前
Vue 聊天列表滚动方案
vue.js
仰望星空的小猴子2 小时前
常用的Hooks
前端
天才熊猫君2 小时前
Vue Fragment 锚点机制
前端
米丘2 小时前
Git 常用操作命令
前端
星_离2 小时前
SSE—实时信息推送
前端
wuhen_n2 小时前
响应式探秘:ref vs reactive,我该选谁?
前端·javascript·vue.js