- Published on
DevLog: 写在 BBPlayer 立项七个月后
- Authors
- Name
- Roitium
开端
一年前,我正在考虑换掉正在使用的网易云音乐,因为它太臃肿了,各种不相干的功能堆砌在页面上,尽管每个版本都说「页面更加清爽」,但对我来说功能依然过剩了。我不需要什么私信、电台和直播,我只需要简单听首歌,有歌单功能就好。(趣事一则:BBPlayer 最初的项目名其实叫 IOWTLTFM —— I only want to listen to the fucking music。属于是我内心真实写照了)同时一年 50 左右的黑胶虽然说不上贵,但还是有点肉疼。更何况很多 P主的歌曲因为版权/独家等原因并不会上线网易云音乐,有时候听个歌还需要 B站和网易云音乐双线并行。
(早年间我还有想过用 Go 写一个 Bilibili 收藏夹同步工具,可以把收藏在 Bilibili 的歌曲自动下载下来并刮削,我就可以美美地用本地播放器听了)
「肯定有人跟我有一样的痛点,那么强大的开源社区,让我看看你有什么好东西吧!」,不出所料,已经存在着 azusa-player、bilisound 等项目了。但说实话,体验起来都差点意思,达不到能让我在心中喊出「Ahhh that's what I wanted.」的程度(azusa-player 功能其实很全,动态主题也很炫酷,但 UI/UX...嗯...不敢苟同😇)
「我可不可以自己做一个?」,这个在当时看来显然不切实际的想法第一次出现在了我脑海里。
「那何不做成一个网站,并靠 PWA 实现类客户端体验呢?」,听起来是个绝妙的主意,但仔细想想,完全不可行:
- 大部分 B站 API 请求应该都有 CORS,这意味着我必须要靠 Cloudflare Worker 或自己写一个后端来中转请求,就像我在 hfs-next 中所做的一样。
- 用起来并不方便。我每次听歌都需要打开浏览器 -> 访问网站 -> 挑选歌曲。哪怕用了 PWA,也依然会有很强的割裂感。
- 我真的想学习一下客户端开发,当看到自己的软件在手机上跑起来时,真的很酷/很有意思,不是嘛?
技术选型
既然已经定下来了要做客户端,那就考虑下技术选型:原生安卓开发是不可能的,我不会 Kotlin,短时间也没有精力学,并且 Android Studio 在我的辣鸡笔记本上也跑不起来。(分明就是在找理由吧啊喂!)Lynx JS?太新了不太敢用。Flutter?似乎是个不错的选择,工具链也很成熟,也有完善的 Material Design 3 支持。但最终还是选择了 React Native,毕竟 React 和 JS「一次学习,到处可用」嘛。
于是乎,React Native + Expo 的基础技术栈就这么定下了。
其他一些杂项:UI 库我选择 react-native-paper,因为我真的很喜欢 Material Design 3!TanStack Query 的大名早有耳闻,于是在网络请求方面就选了它!
迭代
v0 时代
最初的版本是 vibe coding 出来的,当我第一次看到页面出现时真的有一股难以言表的激动...于是就一点一点迭代。
有两个比较有意思的细节:
- B站播放流有个特性:120min 后会过期,需要重新获取。为了实现对过期刷新策略的更精细控制,我选择自己通过 Zustand store 维护播放器队列,只通过
TrackPlayer.load()
来实际切换音频。 - B站收藏夹「获取所有收藏内容」的 API 返回数据不包含视频的元数据,只有 bvid。这导致播放器播放列表无法显示歌曲标题、发布者等信息,只能等播放时获取后才能显示。于是当时做了一个 preload 机制,自动获取后 n 首歌曲元数据,既能保证不请求过多被风控,也能一定程度上提高用户体验。
v0 阶段确实只能被称为 MVP,无本地存储,所有数据都从 API 获取。这虽然实现起来简单,但也做了太多的 hack 并牺牲了大量用户体验,比如上面的 preload 机制。如果想实现更多功能,必须要有数据库,必须要 local-first。
v1 阶段
暑假补课的日子里,教室里两台空调依然抵挡不住窗外烈日带来的高温,课自然也是没什么心情听。于是我就趴在桌子上计划重构方案,思路大概是这样的:
整体采用「透明代理」的架构,给所有的 query 都增加一个前置的数据库缓存检查,如果有数据就直接返回(基本不用考虑设计 TTL,因为视频元数据基本不会改变),如果没有就发送网络请求,并保存到本地数据库后返回。本地歌单的实现也很简单,不就靠一张表记录一下歌曲 id 与播放列表 id 的映射关系就好了嘛。这架构设计简直太天才了!甚至这些改动对 UI 层完全是无感的,解耦到极致了啊!
...如果真的有这么顺利就好了。我画了三张草纸的整体架构、数据库 schema 和部分关键场景的数据流转图。然而当真正开始实现时,怎么跟我想的完全不一样呢...数据库 schema 的很多东西我都没考虑到,也没考虑到后期的可拓展性。至于整体架构更是扯淡,「透明代理」的乌托邦只能存在于脑海,哪怕是这么个简单的软件,复杂度都比我脑内想的高很多,很多边缘情况我都没考虑到。
但考虑到我真的是第一次摸数据库,我决定还是原谅我自己。那就从头开始吧,从 service 层的最佳实践,再到 facade 层对收藏夹同步、跨 service 复杂查询的实现,那段时间确实头都大了...每天跟 AI 聊得最多的就是「xxx 我的设计思路是 yyy,你觉得怎么样?」,他说我再改,到底谁是 AI 啊喂🥲。
这其中,我觉得「收藏夹同步功能」的实现是最有意思的。正如上面所说,收藏夹有两个 API,getAllBvid
和 getPaginationVideos
,前者能一次性获取所有内容 bvid,但没元数据。后者有元数据,但是是分页的。我的实现思路是,每次同步时都先调用 getAllBvid
,进行 diff,找出增加、删除的视频的 bvid。然后从 pn=0 开始,循环调用 getPaginationVideos
遍历整个收藏夹,直到找到所有新增的视频元数据为止。
虽然重构过程令人绝望,但工期竟然比意料的短,二十天左右就完成了 v1 版本的重构。
之后就是一些无聊的细节优化。另一个比较有意思的工作就是歌词功能了,写的时候让我有了一种在写 OOP 的熟悉感。
结尾
到底也不知道这篇文章在写些什么呢...如果一定要说的话,开发 BBPlayer 的过程确实让我学到了超级多东西吧:从 Expo 52 到 54,从 React Native 0.76 到 0.81,从 v0.0.1 到 v1.2.1...更深入地学习了 React,似乎搞懂了(我到现在都不敢说真的掌握了 React 记忆机制...这玩意太可怕) memo/useCallback/useMemo 等优化大法,成功变成了有点熟悉 TanStack Query、Drizzle ORM、Reanimated 等库的调包侠,也对原生多少有了点了解,甚至还第一次给开源社区提了 feature PR。
总之,这是一段很有意思的经历,这或许就是我喜欢软件开发的原因:创造和学习的过程总是、也应该是快乐的。如果你觉得学习不快乐,那你一定学习的不是自己喜欢的东西。(就如我学习应试知识一样...)
就如我开头所说,当时我觉得自己开发一个这样的软件是「不切实际」的,但是到现在,终究还是迈出了不小的一步不是吗?想起很多大佬们所说的:「别怕,先做起来再说。」
当然啊当然,我依旧没掌握、也不喜欢写单元测试。以及,最喜欢的 commit message 依然是 fix: 1,或许这就是青春期叛逆吧😇