建站日志 #2:音乐播放器,1400行代码的怪物

建站日志 #2:音乐播放器,1400行代码的怪物

2026/3/4 · 阿幻 · 约 7 分钟 · - 次阅读
📖 建站日志 · 第 2 / 2 篇

音乐播放器这个东西,做之前觉得”不就是放首歌吗”,做完之后发现——光 JavaScript 就写了1400多行。

 

为什么不用现成的

音乐播放器 - 1400行代码的成果

一开始用的是 APlayer,一个很流行的开源网页播放器。

但后来越用问题越多:样式改不动,想做毛玻璃风格很麻烦;跟 Astro 的 View Transitions 不兼容,页面一切换播放器就没了;想要的功能它也没有,双语歌词同步、封面高斯模糊什么的都得自己想办法。

所以决定自己写。BGM对我来说太重要了,追番篇里写过。

代码生成主要靠 Claude Code,我负责需求设计和代码审阅。但调试和修 bug 的过程真的痛苦。

 

浏览器不让自动播放

第一个大坑:浏览器的自动播放策略。

我想要的效果是:用户打开网站,滚动一下页面,音乐就自动播放。

结果发现 Chrome、Safari、Firefox 全部不允许。

它们的规则是:audio.play() 只能在”用户激活事件”里调用。点击、触摸算,滚动不算

所以如果用户第一次交互是滚动,audio.play() 会被浏览器拒绝,而且是静默拒绝——不报错,就是不播放。

最后只能把”显示播放器”和”播放音乐”拆成两步。滚动只负责把播放器显示出来,真正的播放等用户点击或触摸的时候再触发。要是播放被浏览器拒了,就做个标记,下次用户随便点哪里的时候再偷偷重试一次。

这个 bug 前前后后修了好几版才稳定。“为什么音乐没有自动播放”这个问题我反复测了几十次,一度以为是自己代码写出了什么玄学 bug,折腾半天才发现——浏览器压根不让你播。

 

拖动不跟手

播放器是可以拖动的——按住然后拖到屏幕上任何位置。

第一版实现用的是直接改 lefttop CSS 属性。能拖是能拖,但是很卡。特别是在手机上,手指滑动的时候播放器跟不上,有明显的延迟。

后来查了一下,left/top 属于”布局属性”,每改一次浏览器都要重新算一遍布局(reflow),所以才卡。

换成 transform: translate(x, y) + requestAnimationFrame 之后就好了。transform 不触发重排,浏览器可以拿 GPU 去算。改完之后丝滑了很多。

不过拖动还有另一个坑——拖动结束后如果手指释放的位置在播放器的按钮上,会触发按钮的点击事件。比如你拖完松手,刚好松在暂停按钮上,音乐就暂停了。

解决办法是在拖动结束后短暂屏蔽点击事件:在 capture 阶段注册一个 stopPropagation,然后 setTimeout(0) 后移除。

就一个拖动功能,前后翻了三遍。

 

歌词同步

双语歌词同步听起来很简单——按时间戳匹配当前歌词,显示出来就行。

但真做起来细节一堆。

时间戳是最烦的。每首歌的歌词都要手动标注每一句的时间,48首歌里有歌词的24首,每首十几二十行。对不上的时候就一行一行调——播放、暂停、微调0.3秒、再播放、还是不对、再调。纯体力活,干到后面脑子都是麻的。

滚动也有问题。歌词面板里当前行要居中显示,这用 scrollIntoView 就能做到。但要是用户自己在翻歌词,自动滚动就会跟手动滚动打架,画面一直在抢。

后来改成了:用户碰了滚轮或触摸之后,自动滚动先暂停 3 秒。3 秒内没有继续操作,再恢复自动滚动。

歌词面板打开/关闭的动画也调了一会儿。直接 display: none/block 切换太生硬,加了 opacity + transform + scale 的过渡,从下方滑入并放大,关闭时反向收缩淡出。

 

Apple Music 高斯模糊

后来想做 Apple Music 那种效果——播放器背景是当前歌曲封面的高斯模糊。

做法不算难:在播放器底层放一个跟封面一样的图片,用 CSS filter: blur(30px) 模糊掉,然后叠一层半透明的毛玻璃遮罩。

切歌的时候换一下背景图,每首歌的播放器颜色就不一样了。

但性能是个问题。filter: blur() 在手机上很吃性能,模糊值越大越卡。30px 的模糊在低端手机上直接掉帧。后来加了 transform: scale(1.5) 让模糊图放大超出容器,顺便把边缘的锐利截断也解决了。

歌词面板、字幕条也各自有一层封面模糊背景。三层模糊同时存在的时候,z-index 的层级关系得理清楚,不然会互相遮挡。

 

音量控制

音量滑块做成了竖向的——鼠标悬停在喇叭图标上方弹出。

但第一版有个 bug:拖动音量滑块的时候,因为鼠标事件冒泡到了播放器的拖动处理器,导致拖音量的时候整个播放器跟着一起跑。

修复方法是在音量滑块的 mousedowntouchstart 里加 stopPropagation(),阻止事件冒泡到上层的拖动逻辑。

还有个问题,音量滑块被播放器的 overflow: hidden 给裁掉了。滑块是往上弹出的,但播放器为了圆角设了 overflow: hidden,直接把滑块截了一半。最后改成 position: fixed + z-index: 9999,每次弹出的时候动态算位置,让它跟着喇叭按钮走。

这种”修一个小功能结果拔出萝卜带出泥”的事,整个开发过程中碰了无数次。到后面我都麻了,每次修 bug 都先给自己打个预防针:这个 bug 后面八成还藏着两个。

 

1400行

最后数了一下,音乐播放器相关的 JavaScript 居然有1400多行。

播放列表管理、歌词解析和同步、字幕条动画、完整歌词面板、拖动系统、音量控制、随机/顺序切换、视频联动暂停、页面可见性检测、View Transitions 持久化、自动播放策略兼容……

谁能想到”放首歌”这件事能堆出1400行代码。而且这些全塞在 Base.astro 一个文件里,快 1900 行了,编辑器每次打开都要卡好几秒。

但第一次在自己网站上听到音乐响起来的那一刻,还是挺激动的。坐在椅子上傻笑了好一会儿。

分享这篇文章

💬

留言板

( ´▽` )ノ 来聊聊吧~
载入中...
幻之空
0:00
0:00