📢 新文章推送 · 每周更新优质内容 · 订阅更新 →
向下滚动
资源分享

全局音乐播放器功能实现

AI 智能总结

简介

Lumin 主题的音乐页面原本使用 APlayer 库实现单页播放——离开音乐页面,音乐就停了。经过重构,音乐播放器升级为全局跨页播放:无论你在博客的哪个页面,音乐都不会中断,右下角的迷你播放器挂件让你随时控制播放。

架构设计

双层播放器架构

系统采用全局播放器 + 页面播放器双层架构:

 1┌─────────────────────────────────────────────┐
 2              baseof.html                     
 3  ┌─────────────────────────────────────┐    
 4    <audio id="global-audio-player">       
 5    全局 HTML5 Audio 元素                  
 6    生命周期 = 整个站点会话                
 7  └─────────────────────────────────────┘    
 8  ┌─────────────────────────────────────┐    
 9    global-audio.js (ES Module)            
10    管理全局播放器 + 右下角 Widget         
11  └─────────────────────────────────────┘    
12  ┌─────────────────────────────────────┐    
13    toolbar.html (迷你播放器挂件)          
14    歌曲名(可点击回音乐页) + 控制按钮      
15  └─────────────────────────────────────┘    
16└─────────────────────────────────────────────┘
17
18┌─────────────────────────────────────────────┐
19           music/list.html                    
20  ┌─────────────────────────────────────┐    
21    音乐页面 UI(唱片+歌词+列表)          
22    优先使用全局播放器,降级到 APlayer      
23  └─────────────────────────────────────┘    
24└─────────────────────────────────────────────┘
  • 全局播放器<audio id="global-audio-player"> 放在 baseof.html 中,生命周期贯穿整个站点会话,swup 页面切换时不销毁
  • 页面播放器:音乐页面检测到全局播放器存在时,直接控制全局 Audio 元素;否则降级使用 APlayer 实例

状态同步核心:window._gMusic

跨模块通信的关键是一个全局状态对象 window._gMusic

 1window._gMusic = {
 2  src: '',          // 当前歌曲 URL
 3  name: '',         // 歌曲名
 4  artist: '',       // 歌手
 5  cover: '',        // 封面
 6  volume: 0.7,      // 音量
 7  position: 0,      // 播放位置(秒)
 8  playing: false,   // 是否正在播放
 9  index: 0,         // 当前歌曲索引
10  playMode: 'list', // 播放模式:list/random/single
11  playlist: [],     // 歌单数组
12  platform: ''      // 当前平台
13};

所有模块(全局播放器、音乐页面、Widget)都读写同一个 _gMusic 对象,实现状态共享。

核心功能实现

1. 跨页播放

Swup 等 PJAX 库在页面切换时会销毁并重建 DOM,但 <audio> 元素放在 baseof.html 的全局位置,不受 swup 影响:

 1// global-audio.js
 2export function init() {
 3  _s.audio = document.getElementById('global-audio-player');
 4  // 绑定事件(只绑一次)
 5  if (!_s.audio.dataset.hasGlobalEvents) {
 6    _s.audio.addEventListener('timeupdate', onTimeUpdate);
 7    _s.audio.addEventListener('play', onPlay);
 8    _s.audio.addEventListener('pause', onPause);
 9    _s.audio.addEventListener('ended', onEnded);
10    _s.audio.dataset.hasGlobalEvents = '1';
11  }
12}

页面切换时通过 swup:contentReplaced 事件重新绑定 Widget DOM(因为 Widget HTML 被重建了),但 Audio 元素和播放状态不受影响。

2. 多平台歌单

支持 QQ音乐、网易云音乐、酷狗音乐三个平台,通过 Meting API 获取歌单:

 1# hugo.toml 配置
 2[params.music]
 3  defaultPlatform = "tencent"
 4  defaultVolume = 0.7
 5  [params.music.platforms.tencent]
 6    enable = true
 7    id = "你的歌单ID"
 8  [params.music.platforms.netease]
 9    enable = true
10    id = "你的歌单ID"

双 API 并行请求,谁先返回用谁:

1var url1 = 'https://api.i-meto.com/meting/api?server=' + platform + '&type=playlist&id=' + pid;
2var url2 = 'https://api.injahow.cn/meting/?server=' + platform + '&type=playlist&id=' + pid;
3[url1, url2].forEach(function(url) {
4  fetch(url).then(/* ... */).catch(/* ... */);
5});

切换平台时,同步更新全局播放器状态和歌单:

 1function handleData(data) {
 2  playlist = data.map(/* 构建歌单 */);
 3  var ga = getGlobalAudio();
 4  if (ga && playlist.length > 0) {
 5    ga.src = playlist[0].url;
 6    ga.pause();
 7    window._gMusic.playlist = playlist;
 8    window._gMusic.platform = currentPlatform;
 9    // ... 更新其他状态
10    return; // 全局播放器模式下不创建 APlayer
11  }
12  // 降级:创建 APlayer 实例
13  initAPlayer(data);
14}

3. 播放模式

支持三种模式:顺序播放、随机播放、单曲循环。模式状态存储在 _gMusic.playMode 中,全局播放器和音乐页面共享:

1function toggleMode() {
2  var m = ['list', 'random', 'single'];
3  playMode = m[(m.indexOf(playMode) + 1) % 3];
4  if (window._gMusic) window._gMusic.playMode = playMode;
5}

歌曲播放结束时,全局播放器根据模式决定下一步:

 1// global-audio.js
 2function onEnded() {
 3  var mode = window._gMusic ? (window._gMusic.playMode || 'list') : 'list';
 4  if (mode === 'single') {
 5    _s.audio.currentTime = 0;
 6    _s.audio.play();
 7  } else {
 8    playNext(); // playNext 内部处理 random/list
 9  }
10}

4. 歌词同步

歌词使用 LRC 格式解析,通过 250ms 定时器持续同步:

1function startGlobalSync() {
2  _syncTimer = setInterval(function() {
3    var ga = getGlobalAudio();
4    if (!ga || !ga.src) { clearInterval(_syncTimer); return; }
5    updProgFromGlobal();  // 更新进度条
6    updLrcHi();           // 高亮当前歌词
7    updMiniProg();        // 更新迷你进度条
8  }, 250);
9}

歌词高亮算法:遍历所有歌词行,找到 time <= currentTime 的最后一行,将其设为 active,之前的设为 sung,之后的设为 upcoming

5. 进度条拖拽

进度条支持鼠标和触摸拖拽,优先操作全局播放器:

 1function seekAt(e, bar) {
 2  var globalAudio = getGlobalAudio();
 3  if (globalAudio && isFinite(globalAudio.duration)) {
 4    var pct = (e.clientX - bar.getBoundingClientRect().left) / bar.offsetWidth;
 5    pct = Math.max(0, Math.min(1, pct));
 6    globalAudio.currentTime = globalAudio.duration * pct;
 7    return;
 8  }
 9  // 降级到 APlayer
10  if (ap && ap.audio) ap.seek(ap.audio.duration * pct);
11}

6. 页面切换状态恢复

当用户从其他页面切回音乐页面时,init()_gMusic 恢复完整状态:

 1function init() {
 2  var globalState = window._gMusic;
 3  if (globalState && globalState.playlist && globalState.playlist.length > 0) {
 4    playlist = globalState.playlist;
 5    currentIndex = globalState.index || 0;
 6    playMode = globalState.playMode || 'list';
 7    volume = globalState.volume !== undefined ? globalState.volume : 0.7;
 8    // 恢复 UI:歌曲信息、播放列表、播放按钮、歌词、进度条
 9    updateSongInfo(currentIndex);
10    renderPlaylist();
11    setTimeout(hiCur, 150); // 延迟居中滚动到当前歌曲
12    bindGlobalAudioEvents();
13    startGlobalSync();
14    loadLrc();
15    preloadDur();
16    return;
17  }
18  // 没有全局播放器,降级使用 APlayer
19  ensureLibs(function() { fetchPlaylist(); });
20}

7. 播放列表居中高亮

当前播放的歌曲在列表中高亮显示,并自动滚动到可视区域中央:

 1function hiCur() {
 2  var rows = document.querySelectorAll('#playlist-body tr');
 3  for (var i = 0; i < rows.length; i++)
 4    rows[i].classList.toggle('active', i === currentIndex);
 5  if (!rows[currentIndex]) return;
 6  var container = document.querySelector('.playlist-box');
 7  if (!container) return;
 8  // 使用 getBoundingClientRect 精确计算滚动位置
 9  requestAnimationFrame(function() {
10    var rowRect = rows[currentIndex].getBoundingClientRect();
11    var boxRect = container.getBoundingClientRect();
12    var rowCenter = rowRect.top - boxRect.top + container.scrollTop + rowRect.height / 2;
13    container.scrollTop = Math.max(0, rowCenter - boxRect.height / 2);
14  });
15}

8. 迷你播放器 Widget

右下角悬浮的迷你播放器,显示当前歌曲信息,支持播放/暂停/上一首/下一首:

  • 歌曲名可点击,跳转回音乐页面
  • 鼠标悬停展开完整控制面板
  • 音乐页面自动隐藏 Widget(避免重复控制)
  • 播放时显示音频波形动画

关键技术点

避免双重播放

当全局播放器存在时,音乐页面不创建 APlayer 实例,直接操作全局 Audio 元素。handleData 中全局分支提前 return,跳过 initAPlayer()

事件监听器去重

bindCtrl() 使用 _bound 标记防止重复绑定,swup 页面切换时重置标记:

1function bindCtrl() {
2  if (bindCtrl._bound) return;
3  bindCtrl._bound = true;
4  // ... 绑定事件
5}
6// swup 切换时重置
7document.addEventListener('swup:contentReplaced', function() {
8  bindCtrl._bound = false;
9});

ended 事件防冲突

全局播放器的 onEnded 和音乐页面的 ended 监听器都会触发。解决方案:全局播放器负责切歌逻辑,音乐页面只负责 UI 同步:

 1// 音乐页面的 ended 监听器
 2ga.addEventListener('ended', function() {
 3  setTimeout(function() {
 4    var newIdx = window._gMusic.index || 0;
 5    if (newIdx !== currentIndex) {
 6      currentIndex = newIdx;
 7      updateSongInfo(currentIndex);
 8      hiCur();
 9      loadLrc();
10    }
11  }, 200);
12});

localStorage 歌单缓存

歌单数据缓存到 localStorage,避免重复请求 API:

1function getMuzCache(platform, pid) {
2  var raw = localStorage.getItem('muz_playlist_cache');
3  var cache = JSON.parse(raw);
4  if (cache.platform !== platform || cache.pid !== pid) return null;
5  return cache.data;
6}

文件结构

1layouts/
2├── _default/baseof.html          # 全局 Audio 元素
3├── music/list.html               # 音乐页面(播放器UI + 逻辑)
4└── partials/toolbar.html         # 迷你播放器 Widget
5
6assets/
7├── js/modules/global-audio.js    # 全局播放器模块
8└── css/main.css                  # 样式(含播放器样式)

配置说明

hugo.toml 中配置音乐功能:

 1[params.music]
 2  defaultPlatform = "tencent"    # 默认平台:tencent/netease/kugou
 3  defaultVolume = 0.7            # 默认音量 0-1
 4  autoplay = false               # 是否自动播放
 5
 6  [params.music.platforms.tencent]
 7    enable = true
 8    id = "你的QQ音乐歌单ID"
 9
10  [params.music.platforms.netease]
11    enable = true
12    id = "你的网易云音乐歌单ID"
13
14  [params.music.platforms.kugou]
15    enable = false
16    id = ""

总结

全局音乐播放器的核心挑战在于跨页面状态同步——swup 页面切换会销毁 DOM,但 Audio 元素和 _gMusic 状态对象不受影响。通过全局 Audio + 状态对象 + 事件绑定 + 定时器轮询的组合方案,实现了无缝的跨页播放体验。

8 / 9
版权声明

本文作者 Lumin

本文链接 https://www.zhengquan.xyz/share/music-player-implementation/

许可协议 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

请作者喝杯咖啡 ☕

  • 微信打赏
    微信支付
  • 支付宝打赏
    支付宝
点击按钮查看打赏二维码
🎁 推荐工具
试试这些实用在线工具,提升工作效率
前往工具集 →

留言评论

期待你的想法

评论加载中