因为我很喜欢听音乐(尤其是电子音乐),所以我希望我自己的博客能给浏览者安利很多很多好听的音乐。

总感觉这个全局吸底aplayer如果只能播一个歌单的话太浪费了。我希望它能像家里摆着的唱片机一样,当客人来做客时,任他在唱片架挑选中意的唱片,并放进这个唱片机里开始聆听。

虽然我是技术渣,但还是想试试。

乐辑

这里直接照搬了butterfly主题中图册页的样式和标签插件(确实很好看),就不赘述了。

收藏的唱片一多,确实就需要对唱片架中的唱片进行归类。

唱片架

一定程度上参考了butterfly的图片页。

概述

大概长这样:

当鼠标移至唱片封面时,会出现个人的吐槽评价,以及黑胶唱片被拿出的效果。

点击下方的播放,底部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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* AuroraCafe
* musicGroup and musicItem
* {% musicGroup [name] [descr] [url] [img] %}
* {% musicItem [cover] [title] [author] [id] [server] [type] [description] %}
*/

'use strict'

const urlFor = require('hexo-util').url_for.bind(hexo)

/**
* musicGroup省略
*/

function musicItem(args) {
const cover = args[0]
const title = args[1]
const author = args[2]
const id = args[3]
const server = args[4]
const type = args[5]
const description = args[6]

let extraHTML = ''
if(description && typeof(description)!=="undefined") {
extraHTML = `<div class="description">
<p class="des-inner">${description}</p>
</div>
`
}

return `
<div class="music-item">
<div class="pic-item">
<img class="cover no-lightbox" src="${cover}?x-oss-process=image/resize,s_300">
<div class="inner">
</div>
<img no-lazy src="/img/vinyl.png" class="vinyl no-lightbox">
${extraHTML}
</div>
<p class="title">${title}</p>
<p class="author">${author}</p>
<button class="music-play-btn" data-id="${id}" data-server="${server}" data-type="${type}">播放</button>
</div>
`
}

// hexo.extend.tag.register('musicGroup', musicGroup)
hexo.extend.tag.register('musicItem', musicItem)

样式

/source/css/tags中创建了musicItem.styl,用于唱片架样式。

别忘了引入到总样式文件里。

每一个唱片是.music-item,一个唱片架就是.music-list

老实说我还是比较习惯scss的写法,stylus让我觉得好别扭orz。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#article-container

.music-list
display: grid;
grid-template-columns: 1fr 1fr;
grid-row-gap: 1.5rem;
justify-items: center;
margin: 2rem 0;

@media screen and (max-width: 500px)
&
grid-template-columns: 1fr;

.music-item
width: 14rem;
height: 18rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;

.pic-item
position: relative;
width: 12rem;
height: 12rem;

.cover
width: 12rem;
height: 12rem;
border-radius: 0.5rem;
position: absolute;
z-index: 2;
transition: 0.1s;

.inner
width: 11.5rem;
height: 11.5rem;
background-color: rgba(255, 255, 255, .3);
border-radius: 1rem;
position: absolute;
top: 0.25rem;
left: 1.25rem;
transition: 0.1s;
z-index: 1;


.vinyl
width: 12rem;
height: 12rem;
position: absolute;
background-color: transparent;
left: 1.5rem;
transition: 0.1s;

.description
visibility: hidden;
opacity: 0;
width: 12rem;
height: 12rem;
background-color: rgba(0,0,0,.5);
color: #fff;
border-radius: 0.5rem;
overflow: hidden;
position: absolute;
transition: 0.1s;
z-index: 5;

.des-inner
width: 12rem;
height: 12rem;
color: #fff;
padding: 0.5rem;
border-radius: 0.5rem;
font-size: 0.8rem;
letter-spacing: 0.05rem;
line-height: 1.1rem;
text-indent: 1.6rem;
overflow-y: scroll;
transition: 0.1s;
z-index: 5;

&:hover
.vinyl
left: 3.5rem;

.cover
box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 15px;

.inner
box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 15px;

.description
opacity: 1;
visibility: visible;


p
margin: 0;
cursor: default;


.title
font-size: 1rem;
white-space: nowrap;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 0.5rem;

.author
font-size: 0.8rem;
white-space: nowrap;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;


button
border: none;
background-color: #fff;
padding: 0.25rem
border-radius: 1rem;
margin-top: 0.5rem;
transition: 0.1s;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
&:hover
cursor: pointer;
box-shadow: rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;

行为

/source/js中创建了changePlay.js,用于唱片架样式。

其实也就点击播放后切换播放列表这一个行为。

查阅了其他人的一些博客,各位推介的最佳实践都是使用meting.js对aplayer进行增强,这样只要输入几个参数就能获取整个网络音乐平台的播放列表,不需要自己想办法获得音乐的api。就像这样:

1
<meting-js server="netease" type="album" id="154548341">

但是这样并不符合我的要求,因为当我查阅meting.js的文档时,我发现一个问题:meting.js仿佛是一个完全的黑箱操作,它好像没有提供任何透露流程细节的API,我只能输入几个参数看着它自己在网页的该位置生成一个aplayer播放器。

这明显不符合我的需求,我希望操作博客已有的全局吸底播放器,而不是生成一个新播放器。

于是我查看了meting.js的源码,从其中摘取了一段核心代码,并另写了一个普通的函数。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
const getMusicList = async (options) => {
if (typeof (options) !== "object") {
return
}

let api
let result

//初始化
init()
await parse()

function init() {
api =
options.api ||
window.meting_api ||
'https://api.i-meto.com/meting/api?server=:server&type=:type&id=:id&r=:r'

if (options.auto) _parse_link()

function _parse_link() {
let rules = [
['music.163.com.*song.*id=(\\d+)', 'netease', 'song'],
['music.163.com.*album.*id=(\\d+)', 'netease', 'album'],
['music.163.com.*artist.*id=(\\d+)', 'netease', 'artist'],
['music.163.com.*playlist.*id=(\\d+)', 'netease', 'playlist'],
['music.163.com.*discover/toplist.*id=(\\d+)', 'netease', 'playlist'],
['y.qq.com.*song/(\\w+).html', 'tencent', 'song'],
['y.qq.com.*album/(\\w+).html', 'tencent', 'album'],
['y.qq.com.*singer/(\\w+).html', 'tencent', 'artist'],
['y.qq.com.*playsquare/(\\w+).html', 'tencent', 'playlist'],
['y.qq.com.*playlist/(\\w+).html', 'tencent', 'playlist'],
['xiami.com.*song/(\\w+)', 'xiami', 'song'],
['xiami.com.*album/(\\w+)', 'xiami', 'album'],
['xiami.com.*artist/(\\w+)', 'xiami', 'artist'],
['xiami.com.*collect/(\\w+)', 'xiami', 'playlist'],
]

for (let rule of rules) {
// 返回匹配
// eg: "https://y.qq.com/n/yqq/song/001RGrEX3ija5X.html"
// ["y.qq.com/n/yqq/song/001RGrEX3ija5X.html", "001RGrEX3ija5X"]
let patt = new RegExp(rule[0])
let res = patt.exec(options.auto)

if (res !== null) {
options.server = rule[1]
options.type = rule[2]
options.id = res[1]
return
}
}
}
}

async function parse() {
if (options.url) {
// 直接构建 APlayer 配置
let res = {
name: options.name || options.title || 'Audio name',
artist: options.artist || options.author || 'Audio artist',
url: options.url,
cover: options.cover || options.pic,
lrc: options.lrc || options.lyric || '',
type: options.type || 'auto',
}

result = res

return
}

// // 1. 通过 meta 拼凑接口参数获得完整接口 (_init 中存放的默认 api)
// // 2. 请求接口,得到播放列表数据
let url = api
.replace(':server', options.server)
.replace(':type', options.type)
.replace(':id', options.id)
.replace(':auth', options.auth)
.replace(':r', Math.random())

const r = await fetch(url)
const res = await r.json()

result = res
}

return result
}

源码是一个继承了HTMLElement的类(因为要以HTML标签形式使用),其中最核心的两个部分即init()(调整参数)和parse()(获得播放列表)两个方法。

我把它简单修改成了一个传入配置对象参数就可以获得播放列表的函数,把后续各种构建aplayer实例以及创建DOM元素之类的行为都舍弃了。

你非得问我的话就是我也不懂(

然后就是为按钮绑定事件了,但好像产生了新问题:hexo中,用户自定义js好像只能全局引入。

如果确实有按页面引入的方法的话请务必告诉我

这意味着如果我是为music-list还是为music-item绑定事件都不行,因为只有在特定的页面中才会有这两类元素,在其他页面载入话就会产生空指针报错。

最后我想了个笨方法:直接给全局绑定点击事件,并检测点击目标(

别嘲笑我,我要脸orz。

具体代码:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
window.addEventListener("click", async (event) => {
const target = event.target
const tagName = target.tagName
if (tagName.toUpperCase() == "BUTTON" && target.classList.contains("music-play-btn")) {

const dataset = target.dataset

if (dataset.server == "undefined" || dataset.type == "undefined" || dataset.id == "undefined") {
console.log("param err")
return
}

//调用上面的魔改meting.js,获取播放列表
const res = await getMusicList({
server: dataset.server,
type: dataset.type,
id: dataset.id
})

// 根据全局吸底aplayer构建实例
const ap = new APlayer({
container: document.getElementById("bottom-aplayer")
})

if (ap) {
// 清空目前的播放列表
ap.list.clear()
// 添加播放列表
ap.list.add(res)
// 获取缓存的音量信息
const metingStr = localStorage.getItem("metingjs")
const metingjs = JSON.parse(metingStr)
let vol
if (typeof (metingjs.volume) == "number") {
vol = metingjs.volume
} else {
vol = ap.audio.volume
}
ap.volume(vol, true)
// 展开给浏览者看一下,“我已经改好播放列表了!”
ap.setMode("normal")
// 开始播放
ap.play()
} else {
return
}
}
}, true)

碎碎念

随后我又想尝试新的功能:添加一个按钮,点击后不改变播放列表,只是在播放列表末尾插入曲目。

我尝试了一阵子,始终未能成功,但发现了问题所在:

加载站点后所处的的第一个页面,可以从console找到该页面中所有aplayer实例(全部都被butterfly框架放在了aplayers这个数组中,包括吸底aplayer)。

但前往其他页面时,再看aplayers数组,就找不到刚才的实例了。

确实hexo博客不是单页面应用,这样其实也挺正常的。但全局吸底aplayer托了pjax的福,即便页面连续跳转,播放也不间断。

经观察,上面的代码本质上是给原本全局aplayer的位置新建了一个aplayer实例,如果我只想末尾插入曲目,我可能需要获得原本的aplayer实例。

到底该怎么做呢(我不知道pjax的运行机制)。