从零构建桌面音乐播放器 – Electron + React + TypeScript 实践

=== ID=114 ===
TITLE: 从零构建桌面音乐播放器 – Electron + React + TypeScript 实践

传统桌面音乐播放器,感觉要么功能单一只能播本地文件,要么界面直接还停留在十年前的风格()作为一个开发者兼音乐爱好者,我一直想要一个既能管理本地音乐、又能从B站一键提取音频、界面还好看的播放器。在比较了WPF、Qt和Electron之后,最终选了Electron+React+TypeScript的技术栈,花了两周业余时间从零构建了FishMusic(MusicWave)。踩了不少坑,在这记录分享一下,希望能帮到也在搞类似东西的师傅们~

一、技术选型:为什么是Electron?

桌面应用的开发路线说白了就三条路:原生(C++/WinUI)、托管(C# WPF / Java Swing)、Web壳(Electron / Tauri)。跟师傅们一样,选技术栈之前我也纠结了好久,最后直接梭哈Electron,主要考量了这几个点()

维度 WPF(.NET) Qt(C++) Electron Tauri
UI开发效率
生态/组件库 极丰富
文件系统能力 强(Node.js) 强(Rust)
FFmpeg集成 需封装 需封装 npm直装 需Rust绑定
打包体积 大(~150MB) 小(~10MB)
跨平台 仅Windows

关键决策因素:FishMusic的核心功能依赖FFmpeg(音频转码)和HTTP请求(B站API调用),Node.js生态中fluent-ffmpegnet.request可以直接满足这些需求,无需额外Native Addon开发——说白了就是省事()。另外Ant Design 5提供了成熟的UI组件库,从Table、Modal到Slider一应俱全,直接拿来用就行,大幅降低了UI开发成本。打包体积虽然大了点(~150MB),但开发效率确实高太多了,所以就忍了()

二、整体架构设计

项目采用经典的Electron主进程+渲染进程双层架构,其实就是通过Preload脚本暴露安全的IPC接口。整体结构大概是这样的:

==========================================================
                    渲染进程 (Renderer)
  React 18 + TypeScript + Ant Design 5 + Zustand
  +-- pages/   (Home / Library / Playlists / Settings)
  +-- components/ (Layout / PlayerBar / ExtractModal)
  +-- stores/  (playerStore / playlistStore / persistence)
  +-- hooks/   (useAudioPlayer / useCoverUrl)
----------------------------------------------------------
              Preload (contextBridge)
  window.electronAPI = {
    extractBilibili, downloadAudio, convertToMP3,
    cropAudio, getCoverUrl, selectDirectory, ...
  }
----------------------------------------------------------
                    主进程 (Main)
  Electron + Node.js
  +-- ipc/bilibili.ts     (IPC handler)
  +-- services/bilibili.ts (B站API / WBI)
  +-- services/downloader.ts (net.request)
  +-- services/ffmpeg.ts  (fluent-ffmpeg)
  +-- store/json-store.ts (JSON原子读写)
==========================================================

安全模型:Electron应用最大的安全隐患就是渲染进程直接访问Node.js API,这个搞不好直接GG()。FishMusic遵循Electron安全最佳实践——contextIsolation: true(渲染进程与主进程完全隔离)、nodeIntegration: false(禁止渲染进程使用require())、Preload脚本通过contextBridge暴露有限API、所有参数经过类型校验。说白了,即使前端代码被XSS攻击了,攻击者也仅能调用Preload暴露的那几个方法,无法直接执行系统命令,算是把风险控住了()

三、B站音频提取:完整链路

这是FishMusic最核心也最复杂的功能,从用户粘贴链接到获得MP3文件,整个链路搞了好几个步骤。B站从2023年起启用了WBI(Web Interface)签名机制,所有API请求必须携带w_ridwts参数——说白了就是不能直接裸调接口了,得先签名()

// 1. 获取img_key和sub_key(每24h过期)
const { img_key, sub_key } = await fetch('https://api.bilibili.com/x/web-interface/nav')

// 2. 裁剪密钥:去除非十六进制字符,取第9到第40位
const mixinKey = (img_key + sub_key).replace(/[^a-f0-9]/g, '').slice(8, 40)

// 3. 对请求参数排序、拼接、加盐、取MD5
const params = { ...originalParams, wts: Math.floor(Date.now() / 1000) }
const sorted = Object.keys(params).sort()
  .map(k => k + '=' + encodeURIComponent(params[k])).join('&')
const w_rid = md5(sorted + mixinKey)

WBI密钥每24小时轮换,所以应用启动时需要重新获取。实现时加了缓存逻辑,避免短时间内重复请求。如果请求返回-352错误码,直接自动刷新密钥然后重试。这个签名机制说实话一开始搞了半天才搞清楚原理,算是WBI这一块比较坑的地方()

下载与转码:B站视频的音频流托管在多个CDN节点上,优先尝试最快的节点,每个节点15秒连接超时,失败直接自动切下一个。下载的音频为m4s格式(MPEG-DASH分段流),然后用fluent-ffmpeg转码为320kbps MP3。注意ffmpeg-static是预编译的FFmpeg二进制包,在electron-builder.yml中必须配置asarUnpack——因为.asar归档是只读的,ffmpeg二进制必须解压到文件系统才能执行。这个坑当初花了我一晚上才解决,查了半天文档才搞明白()

四、音频编辑器:wavesurfer.js波形可视化

音频编辑算是FishMusic的一大亮点了()核心依赖wavesurfer.js v7,基于Web Audio API的音频波形渲染库。流程就是:fetch加载音频文件→AudioContext.decodeAudioData()解码为PCM→降采样生成波形成像数据→Canvas绘制波形。然后在此基础上扩展了分割点管理、片段命名、独立试听和批量导出(FFmpeg的-ss和-t参数裁剪)功能,整体做下来感觉还挺顺的。

五、状态管理与数据持久化

选了Zustand而非Redux,理由很简单:零模板代码。Redux需要action types、creators、reducers、middleware四层,Zustand直接就create((set, get) => ({...}))完事,TypeScript原生支持,压缩后仅1KB。说白了就是小而美,没必要为了用而用Redux()

原子写入防数据损坏:写入JSON文件时如果进程崩了,文件可能只剩一半,这真没话说。解决方案就是先写.tmp文件,然后调用rename()原子替换——操作系统保证rename是原子操作,要么成功(新文件完整),要么失败(旧文件保留)。最坏情况遗留.tmp文件,启动时直接检测并删除就行了。这个思路其实是从大佬博客那里偷过来的(),但真的很好用,算是又学到了一个实用小技巧。

六、踩坑记录

  • ffmpeg-static的asarUnpack坑:Electron的asar是只读归档,ffmpeg二进制必须在文件系统执行。需在electron-builder.yml中配置asarUnpack,并在代码中通过app.isPackaged判断使用正确路径。这个坑搞了好久,差点直接放弃了()
  • WBI签名时效性:img_key/sub_key每24小时过期,需要缓存并自动刷新。长时间不关应用时需要处理签名过期自动重试,不然直接报错,很搞心态()
  • IPC通信不要传大对象:音频Buffer不要通过IPC传输,直接传文件路径让主进程读写就好。一开始傻傻地传Buffer,性能巨差,感觉这块踩得最蠢()
  • Zustand selector优化:始终使用useStore(state => state.specificField)而非解构整个state,避免无关状态变化触发组件重渲染。这个其实文档里写了,但第一次用的时候没注意,直接全解构了导致渲染爆炸()
  • Electron窗口性能:音频波形渲染时需开启backgroundThrottling: false,防止Chromium降低后台窗口帧率导致波形卡顿。这个坑比较隐蔽,测试了半天才定位到问题。

FishMusic从项目初始化到第一个可用版本大约花了两周业余时间,整体做下来感觉Electron+React的组合虽然打包体积偏大,但开发效率确实比原生方案高太多了。项目已开源在GitHub,欢迎各位师傅来玩~ 也算是完成了一个很久以来想搞的Side Project,能跑起来的那一刻还是挺开心的()

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注