1194 字
6 分钟
Fuwari 博客改造计划:为侧边栏添加音乐播放器卡片

为什么选择侧边栏?#

Fuwari 默认的 Aplayer 通常固定在左下角,虽然功能完整,但在视觉上略显突兀,且容易遮挡页面内容。通过将播放器封装为侧边栏卡片,不仅能让博客界面更加整洁、原生,还能利用 Fuwari 优秀的侧边栏粘性(Sticky)效果,让音乐控制始终触手可及。

设计架构#

为了在 Astro 这种静态站点生成器中实现一个稳定、不重叠的音乐播放器,我们采用了以下分层架构:

1. 组件封装层 (Component Layer)#

使用 MusicPlayer.astro 作为核心。它负责:

  • 资源注入:按需引入 APlayer 和 MetingJS 的 CDN 脚本与样式。
  • DOM 占位:提供一个标准的 card-base 容器,确保视觉样式与主题统一。
  • 配置声明:通过 Data Attributes(在脚本中动态设置)定义歌单来源。

2. 生命周期管理层 (Lifecycle Management)#

由于 Fuwari 使用了 Swup 进行无刷新跳转,传统的脚本执行逻辑会失效。我们构建了一个闭环的生命周期:

  • 初始化 (Init):在 DOMContentLoadedastro:page-load 时触发,确保首次进入或刷新页面时播放器可用。
  • 清理 (Cleanup):在 Swup 的 content:replace 阶段,主动调用 aplayer.destroy()。这是防止多重音轨叠加的核心逻辑。
  • 重建 (Rebuild):在 Swup 的 page:view 阶段重新实例化,确保在切换页面后播放器依然存在于新的侧边栏 DOM 中。

3. 数据与逻辑通信 (Data & Logic)#

  • 全局单例:将 APlayer 实例挂载到 window.musicPlayerInstance,实现跨组件、跨页面的实例追踪。
  • 异步适配:由于 MetingJS 是异步渲染 DOM,架构中包含了一个轮询捕获机制,确保在 DOM 准备就绪后再绑定实例控制权。

核心实现步骤#

1. 创建 MusicPlayer 组件#

创建文件 src/components/MusicPlayer.astro

---
---
<!-- 引入 APlayer 和 MetingJS 的 CDN 资源 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css" />
<script is:inline src="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
<script is:inline src="https://cdn.jsdelivr.net/npm/meting@2.0.1/dist/Meting.min.js"></script>
<div class="pb-4 card-base">
<!-- 卡片标题样式,契合 Fuwari 主题 -->
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:left-[-16px] before:top-[5.5px]">
音乐
</div>
<!-- 播放器容器 -->
<div class="px-4" id="music-player-container"></div>
</div>
<script is:inline>
// 全局变量存储播放器实例,用于后续销毁
window.musicPlayerInstance = null;
function initMusicPlayer() {
// 1. 销毁已存在的实例,防止页面切换后出现多个播放器叠加或报错
if (window.musicPlayerInstance) {
try {
window.musicPlayerInstance.destroy();
} catch (e) {
console.error('销毁旧播放器实例失败:', e);
}
window.musicPlayerInstance = null;
}
// 2. 清除并重建容器
const container = document.getElementById('music-player-container');
if (!container) return;
container.innerHTML = '';
// 3. 动态创建 meting-js 元素
const metingElement = document.createElement('meting-js');
metingElement.setAttribute('server', 'netease');
metingElement.setAttribute('type', 'playlist');
metingElement.setAttribute('id', '2501170701');
metingElement.setAttribute('fixed', 'false');
metingElement.setAttribute('autoplay', 'false');
metingElement.setAttribute('theme', 'var(--primary)');
metingElement.setAttribute('loop', 'all');
metingElement.setAttribute('order', 'list');
metingElement.setAttribute('preload', 'auto');
metingElement.setAttribute('list-folded', 'true');
metingElement.setAttribute('list-max-height', '300px');
container.appendChild(metingElement);
// 4. 手动触发 MetingJS 初始化
if (typeof window.Meting === 'function') {
window.Meting(metingElement);
}
// 5. 轮询获取生成的 APlayer 实例并存储
let retryCount = 0;
const maxRetries = 10;
const checkAPlayer = () => {
const aplayerElement = container.querySelector('.aplayer');
if (aplayerElement && aplayerElement.aplayer) {
window.musicPlayerInstance = aplayerElement.aplayer;
} else if (retryCount < maxRetries) {
retryCount++;
setTimeout(checkAPlayer, 200);
}
};
checkAPlayer();
}
// 适配多种页面加载场景
document.addEventListener('DOMContentLoaded', initMusicPlayer);
document.addEventListener('astro:page-load', initMusicPlayer);
// 特别适配 Swup 路由切换
if (window.swup) {
window.swup.hooks.on('content:replace', () => {
if (window.musicPlayerInstance) {
try {
window.musicPlayerInstance.destroy();
} catch (e) {
console.log('Swup 切换时销毁播放器:', e);
}
window.musicPlayerInstance = null;
}
});
window.swup.hooks.on('page:view', initMusicPlayer);
}
</script>

2. 集成到侧边栏#

打开 src/components/widget/SideBar.astro,将刚才创建的组件引入并放置在合适的位置。

---
// ... 其他导入
import MusicPlayer from "../MusicPlayer.astro";
// ...
---
<div id="sidebar" class:list={[className, "w-full"]}>
<!-- ... Profile 等组件 -->
<div id="sidebar-sticky" ...>
<Categories ... />
<Tag ... />
<!-- 在这里添加音乐播放器 -->
<MusicPlayer class="onload-animation" style="animation-delay: 250ms" />
</div>
</div>

技术细节说明#

Swup 路由适配#

Fuwari 使用了 Swup 实现无刷新页面切换。如果不做处理,切换页面时旧的 APlayer 实例会残留在 DOM 之外继续工作,导致出现“重音”或者无法控制的情况。我们在脚本中监听了 content:replace 钩子,确保在页面内容更新前彻底销毁旧实例。

实例获取#

由于 MetingJS 是异步生成 APlayer 结构的,我们使用了一个简单的 setTimeout 轮询机制来捕获生成的 aplayer 对象,以便我们在全局范围管理它。

如何自定义#

你只需要修改 MusicPlayer.astro 中的 meting-js 属性:

  • server: 平台(netease - 网易云,tencent - QQ音乐)。
  • id: 歌单、专辑或单曲的 ID。
  • theme: 播放器主色调,代码中已设为 var(--primary) 以匹配你的主题色。

结语#

通过这种方式实现的音乐播放器,不仅外观上与 Fuwari 的卡片式设计完美融合,而且在性能和稳定性上也针对 Astro 的特性进行了优化。快去换上你喜欢的歌单吧!


参考与致谢#

Fuwari 博客改造计划:为侧边栏添加音乐播放器卡片
https://fuwari.vercel.app/posts/fuwari主题侧边栏音乐播放器解决方案---xagunelのblog/
作者
Watch Your Back
发布于
2026-04-05
许可协议
CC BY-NC-SA 4.0