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

0 阅读6分钟

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

GitHub - black542684/music-desktop