Pjax是一款常用的插件,主要用于实现跳转不刷新网页实现某些事件的连续,例如本站使用了Pjax实现了音乐tag全站连续播放,其原理是通过不刷新网页的方式获取js资源,从而不会阻断连续事件的发生。但是pjax会引入比较大的问题,某些页面跳转时需要刷新加载的部件无法正常加载,例如评论模块、自建Aplayer等,只能手动刷新,导致每次在Music页选择完音乐,重新进入时无法获取正在播放的列表。

最常用的解决方法是:在每次pjax调用完成后,使用回调函数加载js资源,Aplayer列表确实不会丢失了,但是音乐却只能在当页播放,体验一般,本文提供了一种可行的方案,能够完美兼容Aplayer加载问题与Pjax调用问题,主要骨干是pjax回调函数初始化Aplayer,以及用于辅助记录Aplayer歌曲播放状态的函数。

建立一个Aplayer

Aplayer可以使用列表语法调用,也可以使用js调用,为了方便函数互调,使用了js语法,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function BuildPlayer(){

globalPlayer = new APlayer({
container: document.getElementById('aplayer'),
narrow: false,
autoplay: false,
mode: 'random',
showlrc: 3,
mutex: true,
theme: '#e6d0b2',
preload: 'metadata',
listmaxheight: '513px',
music: [
{title: 'xxx',author: 'xxx',url: 'xxx',lrc:'xxx'}]
});
// 监听播放状态变化事件,savePlayState用于记录播放状态,后文给出
globalPlayer.on('play', savePlayState);
globalPlayer.on('pause', savePlayState);
globalPlayer.on('seeked', savePlayState);
}

状态记录与恢复

pjax全局生效时跳转播放是没问题的,但是会导致js重复加载,点击进入html页面,pjax在首次进入时会完成一次调用使用回调函数新建Aplayer,那么再次点击进入,就会出现再新建一次Aplayer,出现了Aplayer无限叠加的问题。因此必须使用函数用于记录上次歌曲播放状态,并且再次点击时如果Aplayer对象是存在的,就创建新建对象用于拷贝旧对象的状态再销毁对象。新建对象的目的在于在不刷新的前提(刷新代表所有资源被覆盖)下,能够正常加载Aplayer的列表,并且恢复到上次的状态。保存的内容包括正在播放的歌曲、歌曲时间、暂停状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function savePlayState() {
if (globalPlayer) {
const playState = {
currentTime: globalPlayer.audio.currentTime,
paused: globalPlayer.audio.paused,
currentMusicIndex: globalPlayer.list.index
};
localStorage.setItem('playState', JSON.stringify(playState));
}
}

// 恢复播放状态函数
function restorePlayState() {
const playState = JSON.parse(localStorage.getItem('playState'));
if (playState && globalPlayer) {

globalPlayer.list.switch(playState.currentMusicIndex);
globalPlayer.audio.currentTime = playState.currentTime;
if (!playState.paused) {
globalPlayer.play();
} else {
globalPlayer.pause();
}
}
}

Aplayer的初始化策略

有了上面的基础,就可以进行Aplayer的初始化了。Aplayer的初始化分为两种情况:其一是用户第一次进入html,此时没有Aplayer对象;其二是用户听着音乐了,Aplayer存在: 1. 第一次初始化:建立列表

1
2
3
4
5
6
7
if(!globalPlayer){
BuildPlayer();

// 恢复播放状态
restorePlayState();
}

2. 带Aplayer对象:新建————拷贝————恢复状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
else{
savePlayState();
const newPlayer = new APlayer({
container: document.getElementById('aplayer'),
narrow: false,
autoplay: false,
mode: 'random',
showlrc: 3,
mutex: true,
theme: '#FF6347',
preload: 'metadata',
listmaxheight: '1026px',
music: globalPlayer.list.audios
});
// 将旧实例的状态复制到新实例中
newPlayer.list.index = globalPlayer.list.index;
newPlayer.audio.currentTime = globalPlayer.audio.currentTime;
newPlayer.audio.paused = globalPlayer.audio.paused;
// 销毁旧实例
globalPlayer.destroy();
// 更新全局播放器实例引用
globalPlayer = newPlayer;
// 恢复播放状态
restorePlayState();
}

pjax回调

pjax回调保证每次进入时都能够根据有无Aplayer对象进行初始化,不知道为什么pjax:complete无效,pjax:success可以,效果是一致的。

1
2
3
4
document.addEventListener('pjax:success', function() {
console.log('pjax:success - Restoring/initializing player');
initPlayer();
});

细节问题

有一个特别要注意的地方是:假如在当前页面刷新时,由于本页没有主动引入pjax,所以刷新是不会导致pjax:success事件发生,也就是说,刷新时不会进入初始化,列表资源会显示空白。为了解决这个问题,只需要在刷新时加入资源初始化:

1
2
3
//冲突方法
if (!globalPlayer) {
initPlayer();}
每次刷新时Aplayer必然是被销毁的,因此该代码会重新引入初始化,虽然之前的Aplayer被销毁了,但是歌曲播放状态仍然可以保留,这也是我在初始化时尽管没有Aplayer对象,也要恢复歌曲状态的原因,每次刷新都能够保留歌曲播放的状态。

但是又会导致新的故障:上述这段if代码和pjax:success回调函数在第一次进入时会发生冲突:因为第一次进入时既满足pjax:success,也满足!globalPlayer,导致程序未来得及pjax后回调,就进入初始化,导致第一次进入必然是初始化失败的。解决方法是不要使用直接判断,引入标志位和DOMContentLoaded事件,该事件在浏览器文本加载完成即可调用,不等待js和css加载:

1
2
3
4
5
6
7
8
let flag=false;
document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
if (!flag) {
initPlayer();
flag=true;}
}, 50);
});
首先flag被置为false,当浏览器文档加载完毕调用该函数,在第一次进入时会进行50ms的谦让,等待pjax:success事件完毕,初始化完成;在后续的刷新中,flag每次都会被重新置为false,pjax:success事件不会生效,由该函数执行初始化。

总结

可以说整个配置过程原理并不复杂,但是步骤比较繁琐,由于Aplayer版本以及pjax版本不一样,相关封装函数也不一样,函数位置错了就得不到需要的效果。这里要感谢Claude-3-Opus的帮助,尽管GPT-4-Turbo数据库已经更新到24年的4月份,但在代码意见上Claude-3-Opus仍然是当之无愧的榜首,上述代码性能还有很大的调整空间,但就个人而言也算是合格的方案。