暂未播放
0:00
0:00

音乐页软导航改造:播放不中断的排错记录

2821 字
14 分钟
音乐页软导航改造:播放不中断的排错记录

音乐页有个一直没解决的问题:在首页放着歌,点进音乐页,播放会断。

文章之间切页是连着的,因为 Firefly 用的是 Swup 软导航,只换内容区、不重载文档,挂在 Layout 上的 <audio> 一直活着。唯独音乐页不行 —— 当初为了让全屏可视化不跟 Swup 打架,给它设了整页加载。而整页加载会把整个 JS 上下文连同那个常驻播放器一起销毁。

为了续播我做过一版跨整页的方案:pagehide 时把曲目索引、进度、是否在播写进 localStorage,重载后读出来 seek 回去再 play()。进度是能继承的,播放状态不行 —— 整页导航会重置浏览器的用户手势,这时候再调 play() 会被 autoplay 政策直接拒。我又加了静音兜底(先 muted 播再恢复),还是拒;再加”首次点击或按键时续播”的一次性监听,那就不叫无缝了,跟”点一下才能听”没区别。

绕到这儿结论很清楚:只要音乐页走整页加载,就不可能无缝。得回到软导航。

音乐页缺了 Swup 的容器#

回到软导航,先得解释上次为什么放弃了它。

上次的现象是”进音乐页界面乱、内链还失效”,当时的判断是可视化器那个 position: fixed 全屏层被 Swup 过渡期加在容器上的 transform 弄坏了。这次我去翻了 git,那层从来没改过,这个假设站不住。

真正的根子,校验一下 Swup 的容器配置就浮出来了。astro.config.mjscontainers 配了六个:

  • #swup-container
  • #banner-overlay-container
  • #banner-dim-container
  • #left-sidebar-dynamic
  • #right-sidebar-dynamic
  • #floating-toc-wrapper

把这六个 id 拿去搜谁渲染,答案只有一个:MainGridLayout。而 music.astro 用的是 <Layout> 直出 —— 也就是说音乐页的 HTML 里,这六个容器一个都不存在。

Swup 换内容的方式是”拿目标页的同名容器来替换”。目标页没有容器,这次导航就只能半途失败:地址变了、内容没换,过渡用的 is-page-transitioning 类也摘不掉,表现出来就是界面乱、内链点不动。

之所以一直没暴露,是因为音乐链接上挂着 fullPage: true,从来没真正软导航过。上次我把这个标志去掉,等于第一次真的走软导航,问题立刻现形。

还有条佐证。swup-transitions.tspage:view 里会跑 updateMainGridCols()updateSidebarComponentsVisibility()syncFullscreenOverlays() 这些,全都依赖 MainGridLayout 的 DOM。用 Layout 直出,它们不是空转就是报错。

所以修法很直接:music.astro 改用 MainGridLayout,内容塞一个空占位。引申一下,凡是要参与软导航的路由,都不能用 Layout 直出。

把可视化器挪成常驻覆盖层#

容器补齐,音乐页能正常软导航了。但可视化器本身还渲染在内容区里,软导航会把它跟内容一起换掉,three 场景和 AudioContext 都得重建。虽然播放不断了,但可视化的状态每次都要重来,不干净。

做法是把 MusicVisualizer 从页面里提出来,挂进 Layout,做成常驻覆盖层。它和 MusicManager 一样待在 #swup-container 外面,软导航碰不到。进出音乐页只切 body 上的 music-active 类来显隐,three 场景和播放器都不销毁。

<body>
#top-row 导航栏(常驻)
<audio> MusicManager 的播放器(常驻)
#music-overlay 可视化器覆盖层(常驻,软导航不销毁)
.content-panel
└─ #swup-container Swup 只换这里面的东西

页面那边只剩一个路由占位,内容是个空的 <main>,回退按钮走 window.swup.navigate("/")

常驻是有代价的:组件的脚本和样式从此在每个页面都会加载。所以 three 改成懒加载,第一次进音乐页才 await import("three"),不然那六百多 KB 会摊到每一页的首屏。同理,覆盖层的 DOM 现在每页都在,里面就不能留会自动发请求的东西 —— 之前有个 src="" 的空封面 <img>,顺手去掉了。

过渡的几十毫秒#

覆盖层如果只靠 astro:page-load 切类,Swup 的过渡动画期间会先露出音乐页的外壳,闪一下。所以在 visit:start 里按目标地址提前把类切好,visit:endastro:page-load 再按真实地址校准一次。

覆盖层本身给了不透明的底色,three 首帧就绪之前不会透出下面的页面。

几个小问题#

导航栏白字发白#

音乐页场景是深色的,导航栏得透明加白字。我写的是 body.music-active #navbar > div { background: transparent !important },结果导航栏还是浅色毛玻璃,白字叠上去就是发白。

原因是 navbar.css 里导航栏底色和阴影用的都是 !important,选择器还是 html[data-has-wallpaper] #navbar[data-transparent-mode="…"] > div 这种。两条都带 !important 时,比的不是谁写在后面,是特异性。

改法是选择器也带上 [data-transparent-mode],把特异性抬上去。顺手把 --navbar-surface-blur 清零,不然 ::before 那层毛玻璃还在糊背景。

还有个连带的地方。下拉面板跟搜索、主题、音乐、显示设置面板共用 .float-panel,而且都挂在 #navbar 里。一开始只敢改 .dropdown-content,怕整片刷会把那几块面板的文字也刷白。结果留着的那几块成了”亮色面板配浅色文字”,比不改还难看,只能回头再收拾一遍。

音乐页没了滚动条,导航栏会挪#

这个问题只在返回的时候露出来:点回首页,导航栏肉眼可见地往左移了一下。

用浏览器一量就清楚了。音乐页的 body.music-active 上有 overflow: hidden,把页面滚动条去掉了。滚动条一没,视口可用宽度就多出整整一条 —— 本机是 15px,1249 变成 1264。导航栏外壳 #top-rowfixedmx-auto,还挂着 transition-all duration-700,宽度一变,靠右和居中的导航项整体位移,再被那 700ms 过渡一放,就是一个明显的”往左挪”。

这里有个很容易量错的点:#navbar 自己的 left 两次都是 16,根本没变,变的是宽度。只盯着左边缘看,永远看不出问题,得量 #top-row 的宽度或者右边元素的 right

改法是给根元素锁住滚动条槽位:html { scrollbar-gutter: stable }。这样布局宽度恒等于”视口宽减滚动条宽”,滚动条出现或消失都不再引起位移。首页本来就有滚动条、槽位被它占着,位置一点没变。

两个细节。scrollbar-gutter 必须挂 html,我试过挂 body,计算值确实生效了,但视口槽位不保留 —— gutter 不跟着 overflow 一起传播。另外不能只在音乐页加,离开音乐页时 music-active 是在内容替换之前就被摘掉的,那一瞬页面还没滚动条,宽度会先弹回去,反而抖一下。

锁完槽位还有个副作用:空出来的那一条露的是根元素背景,亮色主题下是浅色,在深色场景右边留一条亮边。音乐页把根背景刷成近黑就看不见了。

切进音乐页没有歌词#

在首页放着歌切进音乐页,歌词区一直停在”加载歌词”;但在音乐页刷新一下就一直正常。

歌词是事件驱动的(fm:lyrics),可视化器在初始化的时候才绑监听。问题就出在这条路径上:

首页 取歌词 → 发 fm:lyrics (监听器这时还没绑)
进音乐页 才绑监听 → 收不到 → 歌词区空着
刷新 绑监听 → 取歌词 → 发事件 → 正常

改法是初始化时主动从管理器状态里补一次,getState() 里本来就带着歌词数组和当前行号。

顺带修了个同类的:覆盖层隐藏时是 display: none,量出来的歌词高度全是 0;而换行事件只在行真正切换时才发,不会立刻把位置纠正回来,所以恢复显示的时候要重算一次滚动位置。

hover 的高亮太亮#

导航栏一 hover 或者按下就亮一块,在那个深色场景上很刺眼。

那块底色不是元素自己的背景,是主题的 expand-animation 画在 ::before 上的,亮色主题里用的颜色是 oklch(0.95 0.025 …),基本就是白的。

排查的时候也绕了一下。读 getComputedStyle(el).backgroundColor 拿到的是透明,因为背景压根不在元素上。得读伪元素,getComputedStyle(el, "::before").backgroundColor,才看得到那块底色。

改法就是把音乐页的 ::before 在 hover、聚焦、按下这几种状态下的底色换成半透明白。

浮层面板还是亮的#

导航栏里不只有下拉菜单,搜索面板、主题面板、音乐面板、显示设置面板都挂在 #navbar 里,共用 .float-panel。只改 .dropdown-content 的话,那几块还是亮色毛玻璃,在深色场景上非常突兀。

更麻烦的是它们内部的文字走 Tailwind 的 dark: 变体。主题自定义了 @custom-variant dark (&:where(.dark, .dark *)),意思是只要在面板自身或祖先挂上 dark 类,内部 dark:text-* 就会生效。所以修法分两步:给这几个面板统一加 dark 类,同时用 CSS 把 .float-panel 的背景、边框、阴影强制改成深色毛玻璃。

搜索框是另一个特例。它用的是亮色主题配色 bg-black/4 text-black/30,在音乐页的深色背景上等于完全隐形,但点击区域还在,于是出现”点得到却看不见”的怪现象。要单独覆盖成半透明白底白字。

显示设置面板又更隐蔽一层。它的分区标题 .section-titledisplay-settings.css 里默认是深色,dark 类按道理能让它变浅,但实际因为规则优先级或加载顺序,“主色色相""页面布局""卡片样式”这些标题还是几乎读不出来。最后是在 music-visualizer.css 里直接对 body.music-active #display-setting .section-title 强制浅色,并把面板里的 --btn-content 也翻成浅色,才把所有文字统一压住。

移动端进音乐页会停一下#

桌面软导航后播放一直连着,手机却从首页进音乐页时会闪一下、然后播放暂停。反向(音乐页回首页)没事。

根子在 Web Audio 的接管时机。可视化器要拿音频数据,必须用 createMediaElementSource(audio)<audio> 的输出改道进 AudioContext。如果 AudioContext 处于 suspended 状态(移动端没有用户手势就新建上下文,默认就是 suspended),这一接管会把正在播的声音直接闷掉。

桌面通常能直接 running,所以无感;移动端从首页点进音乐页时还没手势,于是播放被掐。

修法是把 AudioAnalyzer 拆成三步:ensureContext() 只建上下文和各节点、ensureRunning() 等用户交互把状态顶到 running、connect() 只在 running 时执行接管。如果当前跑不起来,先把 <audio> 记下来,挂一个一次性 pointerdown/keydown 监听,首个手势时再做接管。离开音乐页时不 disconnect,反向导航自然不受影响。

结语#

到这里整个修复流程基本就结束了,现在整个网站的音乐都能一首歌不中断播放,而不是像之前的播放器和音乐页各放各的。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
音乐页软导航改造:播放不中断的排错记录
https://x1anyu.cn/posts/15/
作者
羡鱼
发布于
2026-09-10
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
羡鱼
临渊空慕水中鱼, 不如携风自渡河.
分类
标签
最新动态
站点统计
文章
14
分类
7
标签
35
总字数
19,969
运行时长
0
最后活动
0 天前
文章目录