前言
笔者前端小兵一枚,在学习了一段时间的小程序后,决定亲自动手做一款模仿一款手机软件来练手,自己平常也热爱音乐,并且发现各家的音乐平台的小程序都比较简单,于是就选择了这个方向来进行模仿学习,在这个过程中也遇到了很多问题,在解决这些问题后,也有了一些收获,今天就来和大家分享在这个小程序中,最难的音乐播放这一部分的种种问题和解决。
首先,先感谢本项目的api提供者binaryify
选择这个项目,也是因为后端api有大佬提供了,需要数据的时候只用发起一些接口请求就可以了,比较适合像我这样的初学者入门,只用写一些简单的前端逻辑就可以了。
由于播放页面需要处理的事情较多(例如歌词的处理与展示、进度条的快进快退等等),并且坑比较多,为了尽可能的描述清楚,所以本篇文章主要着重介绍和音乐播放有关的种种操作,有关于本项目其他页面的详情介绍,将放在后续文章进行详细叙述,感谢各位读者大大的理解。
项目界面预览:
git地址
github.com/shengliangg…
云村和视频模块目前还没有开发,后续有时间就写,本项目会不定期更新,日后有时间就写一篇项目使用文档
正式开始
有关于音乐播放的几个接口请求中,几乎都需要携带歌曲 id,在本项目的所有页面中,播放页面作为一个独立的页面存在,当别的页面跳转到播放页面时,都会携带歌曲 id
接口封装
本项目使用的接口请求有点多,为了方便,我将其封装在utils文件夹中的api.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 | const GET = 'GET' ;
const POST = 'POST' ;
const baseURL = 'http://neteasecloudmusicapi.zhaoboy.com' ;
function request(method, url, data) {
return new Promise( function (resolve, reject) {
let header = {
'content-type' : 'application/json' ,
};
wx.request({
url: baseURL + url,
method: method,
data: method === POST ? JSON.stringify(data) : data,
header: header,
success(res) {
if (res.data.code == 200) {
resolve(res);
} else {
reject( '运行时错误,请稍后再试' );
}
},
fail(err) {
reject(err)
}
})
})
}
const API = {
getSongDetail: (data) => request(GET, `/song/detail`, data),
getSongUrl:(data) => request(GET, `/song/url`, data),
};
module.exports = {
API: API
}复制代码
|
这里只展示了两个在本页面用到的请求API,在需要接口请求的页面引入就可以使用了
const $api = require('../../utils/api.js').API;
音乐处理
页面数据源
本页面的使用到的data数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | data: {
musicId: -1,
hidden: false,
isPlay: true,
song: [],
hiddenLyric: true,
backgroundAudioManager: {},
duration: '' ,
currentTime: '00:00' ,
totalProcessNum: 0,
currentProcessNum: 0,
storyContent: [],
marginTop: 0,
currentIndex: 0,
noLyric: false,
slide: false
},复制代码
|
其他页面跳转举例:其他页面跳转到play页面,携带musicId参数
1 2 3 4 5 6 7 8 |
playMusic: function (e) {
let musicId = e.currentTarget.dataset.in.id
wx.navigateTo({
url: `../play/play?musicId=${musicId}`
})
},复制代码
|
onLoad生命周期
在play.js的onLoad生命周期函数中,通过options拿到其他页面传过来的musicId这个参数,并且调用play()函数
1 2 3 4 5 6 7 |
onLoad: function (options) {
const musicId = options.musicId
this.play(musicId)
},复制代码
|
播放函数
play()函数需要一个形参:musicId,这个形参非常重要,之后的接口请求都需要用到它
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 |
play(musicId) {
const that = this;
that.setData({
hidden: false,
musicId
})
app.globalData.musicId = musicId
$api .getSongDetail({ ids: musicId }).then(res => {
if (res.data.songs.length === 0) {
that.tips( '服务器正忙~~' , '确定' , false)
} else {
app.globalData.songName = res.data.songs[0].name
that.setData({
song: res.data.songs[0],
})
wx.request({
url: 'http://47.98.159.95/m-api/lyric' ,
data: {
id: musicId
},
success: res => {
if (res.data.nolyric || res.data.uncollected) {
that.setData({
noLyric: true
})
}
else {
that.setData({
storyContent: that.sliceNull(that.parseLyric(res.data.lrc.lyric))
})
}
}
})
$api .getSongUrl({ id: musicId }).then(res => {
if (res.data.data[0].url === null) {
that.tips( '音乐播放出了点状况~~' , '确定' , false)
} else {
that.createBackgroundAudioManager(res.data.data[0]);
}
})
. catch (err => {
that.tips( '服务器正忙~~' , '确定' , false)
})
}
})
. catch (err => {
that.tips( '服务器正忙~~' , '确定' , false)
})
},复制代码
|
总体大致的思路是:
先通过musicId请求歌曲的详细信息(歌曲、歌手、歌曲图片等信息)
在获取成功后接着获取该歌曲的歌词信息(原歌词请求地址有问题,导致这里换了一个接口,所以没封装,直接使用的wx.request做的请求),请求结果如果有歌词,就将请求回来的歌词数据设置到数据源中的storyContent中,这时的歌词还没有经过处理,之后还要处理一下歌词,先调用parseLyric()格式化歌词,再调用sliceNull()去除空行。 如果该歌没有歌词(情况比如:钢琴曲这种纯音乐无歌词的、或者一些非常小众的个人歌曲没有上传歌词的),就设置数据源中的noLyric为true,设置了之后,页面就会显示:纯音乐,无歌词。
点击切换歌词和封面
1 2 3 4 5 | showLyric() {
this.setData({
hiddenLyric: !this.data.hiddenLyric
})
},复制代码
|
格式化歌词
在请求回歌词之后,还需要对歌词进行分行处理
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 | parseLyric: function (text) {
let result = [];
let lines = text.split( '\n' ),
pattern = /\[\d{![](https:
while (!pattern.test(lines[0])) {
lines = lines.slice(1);
};
lines[lines.length - 1].length === 0 && lines.pop();
lines.forEach( function (v , i , a ) {
var time = v.match(pattern),
value = v.replace(pattern, '' );
time.forEach( function (v1, i1, a1) {
var t = v1.slice(1, -1).split( ':' );
result.push([parseInt(t[0], 10) * 60 + parseFloat(t[1]), value]);
});
});
result.sort( function (a, b) {
return a[0] - b[0];
});
return result;
},复制代码
|
歌词去除空白行
1 2 3 4 5 6 7 8 9 | sliceNull: function (lrc) {
var result = []
for ( var i = 0; i <ol start= "3" >
<li><p>再接着通过id去获取歌曲的播放路径,获取到音频的数据源后,则调用createBackgroundAudioManager()函数,传入刚刚获取到的音频数据源。(下文详细介绍)</p></li>
<li>
<p>如果其中的任意一个环节出现了问题,则会弹出提示信息,调用tips()函数,并返回主页</p>
<h3 data-id= "heading-12" >友好提示</h3>
</li>
</ol>
|
播放页面接口请求较多,并且调用频繁,加上一些网络波动,接口调用难免会出现一些失败的情况,为了给用户一些更好的反馈和提示,就使用了微信官方的显示模态对话框wx.showModal,写成了一个tips()函数,在想给提示对话框的时候,直接调用tips()函数就可以,在出现错误之后,用户点击确定会触发回调函数中的res.confirm判断,然后回到首页,这里因为网易云手机app的导航在头部,所以我是用的自定义组件做的导航,没有使用 tabBar,页面跳转用的wx.navigateTo(),如果大家使用了tabBar,那么跳转就应该换成wx.switchTab()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | tips(content, confirmText, isShowCancel) {
wx.showModal({
content: content,
confirmText: confirmText,
cancelColor: '#DE655C' ,
confirmColor: '#DE655C' ,
showCancel: isShowCancel,
cancelText: '取消' ,
success(res) {
if (res.confirm) {
wx.navigateTo({
url: '/pages/find/find'
})
} else if (res.cancel) {
}
}
})
},复制代码
|
接口的请求需要一些时间,在切歌、请求各类数据、页面加载时都有一段时间的等待期,为了提高用户的友好性,在加载时最好加上一些等待动画,我这里就直接使用的比较简单的方法,在wxml中加上一个loading标签,通过数据源中的hidden,来控制loading动画是否显示,一开始设置为false,,然后在数据请求完成后,将其更改为true。
wxml中:
1 2 3 | <loading>
拼命加载中...
</loading>复制代码
|
音频播放
上面提到,在接口请求回音频路径之后,就会调用这个函数,把请求会的数据作为参数传过来,那现在就来剖析这个函数吧。
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 |
createBackgroundAudioManager(res) {
const that = this;
const backgroundAudioManager = wx.getBackgroundAudioManager();
console.log(backgroundAudioManager.src);
if (res.url != null) {
if (backgroundAudioManager.src != res.url) {
that.setData({
currentTime: '00:00' ,
currentProcessNum: 0,
marginTop: 0,
currentIndex: 0,
})
backgroundAudioManager.title = that.data.song.name;
backgroundAudioManager.singer = that.data.song.ar[0].name;
backgroundAudioManager.coverImgUrl = that.data.song.al.picUrl;
backgroundAudioManager.src = res.url;
let musicId = that.data.musicId
app.globalData.history_songId = that.unique(app.globalData.history_songId, musicId)
}
that.setData({
isPlay: true,
hidden: true,
backgroundAudioManager
})
}
app.globalData.backgroundAudioManager = backgroundAudioManager
backgroundAudioManager.onTimeUpdate(() => {
that.setData({
totalProcessNum: backgroundAudioManager.duration,
currentTime: that.formatSecond(backgroundAudioManager.currentTime),
duration: that.formatSecond(backgroundAudioManager.duration)
})
if (!that.data.slide) {
that.setData({
currentProcessNum: backgroundAudioManager.currentTime,
})
}
if (!that.data.noLyric) {
that.lyricsRolling(backgroundAudioManager)
}
})
backgroundAudioManager.onEnded(() => {
that.nextSong();
})
},复制代码
|
音频播放函数里面的逻辑相对比较复杂,大致思路如下:
首先先创建一个BackgroundAudioManager 实例,通过 wx.getBackgroundAudioManager 获取。 然后这里就需要做一个判断,因为当调用本方法有几种情况,一是首次放歌或切换歌曲、二是进来没切换歌曲,所以要判断当前音乐id获取url地址是否等于backgroundAudioManager.src,如果不相等,那就是第一种情况,需要将歌曲的musicId调用unique()去重方法,存入全局的history_songId[],这个历史歌单主要用来给用户切换上一首歌曲用的,后面会详细讲 然后给实例设置title、singer、coverImgURL、src、当设置了新的 src 时,音乐会自动开始播放,设置这些属性,主要用于原生音频播放器的显示以及分享,(注意title必须设置),设置之后,在手机上使用小程序播放音乐,就会出现一个原生音频播放器,如图:
感觉还不错,可惜的是,好像一直目前为止,这个原生的音频播放器都不能设置歌词,只能设置一下基本属性,这也是一个小遗憾,希望微信团队日后能够完善它。
历史歌单去重
作用:用户每播放一首歌,就将其存入历史列表中,在存入之前,先判断这首歌是否已经存在,如果不存在,直接存入到历史歌单数组后面,如果这首歌已经存在,那就先去除老记录,存入新纪录。
1 2 3 4 5 6 7 8 9 10 11 |
unique(arr, musicId) {
let index = arr.indexOf(musicId)
if (index != -1) {
arr.splice(index, 1)
arr.push(musicId)
} else {
arr.push(musicId)
}
return arr
},复制代码
|
第二步就是更新数据源的一些数据,操作和作用都比较简单,就不详讲了
第三步就很重要了,使用 backgroundAudioManager.onTimeUpdate()监听背景音乐的进度更新,页面进度条的秒数更新就和这有关!
wxml:
1 2 3 4 5 6 7 8 9 | <view>
<view>
{{currentTime}}
</view>
<slider></slider>
<view>
{{duration}}
</view>
</view>复制代码
|
backgroundAudioManager.currentTime和backgroundAudioManager.currentTime分别会返回音频播放位置和音频长度,单位为秒,而进度条左边的当前时间和右边的歌曲总时长需要显示成00:00的格式,所以使用formatSecond()来格式化秒数
格式化时间
1 2 3 4 5 6 7 8 9 10 11 12 |
formatSecond(second) {
var secondType = typeof second;
if (secondType === "number" || secondType === "string" ) {
second = parseInt(second);
var minute = Math. floor (second / 60);
second = second - minute * 60;
return ( "0" + minute).slice(-2) + ":" + ( "0" + second).slice(-2);
} else {
return "00:00" ;
}
},复制代码
|
歌词滚动
wxml:
1 2 3 4 5 6 7 8 9 10 11 12 | <!-- 歌词 -->
<!-- 需要设置高度,否则scroll-top可能失效 -->
<scroll-view>
<view>
<view>纯音乐,无歌词 </view>
<block>
<view>
<view>{{item[1]}}</view>
</view>
</block>
</view>
</scroll-view>复制代码
|
歌词的随屏滚动通过歌词时间和音频当前位置来判断当前歌词是多少行,自动滚动是用行数来计算高度,通过设置数据源的marginTop,这个值作用于scroll-view的scroll-top,实现自动滚动的,需要注意的是,scroll-view需要设置高度,否则scroll-top可能失效
通过判断currentIndex是否和页面for循环中的index值是否相等,来给当前唱的歌词加上类名,使其高亮显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
lyricsRolling(backgroundAudioManager) {
const that = this
that.setData({
marginTop: (that.data.currentIndex - 3) * 39
})
if (that.data.currentIndex != that.data.storyContent.length - 1) {
for (let j = that.data.currentIndex; j parseFloat(that.data.storyContent[that.data.storyContent.length - 1][0])) {
that.setData({
currentIndex: that.data.storyContent.length - 1
})
return ;
}
} else {
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[j][0]) && parseFloat(backgroundAudioManager.currentTime) <h3 data-id= "heading-17" >进度条事件</h3><p>在进度条开始滑动的时候将数据源中的slide设置为true,这时backgroundAudioManager.onTimeUpdate()中的更新数据源currentProcessNum就不会再进行,这样就缓解了进度条抖动的问题。</p><p><strong>抖动问题</strong>:如图,在拖动进度条想快进或者快退音乐的时候,可以看到小滑块非常明显的抖动,这是由于onTimeUpdate()在不停的监听并更改数据源中的currentProcessNum,导致拖动过程中的小滑块不停的前后跳动。</p><figure><img class = "lazyload" src= "https://img.php-/upload/article/000/000/052/b9ba6053b8d7b81839bb3a83e6b1fbef-8.gif" data- style= "max-width:90%" data- style= "max-width:90%" alt= "那些年,看看微信小程序仿网易云音乐的相关播放" ></figure><pre class = "brush:php;toolbar:false" >//进度条开始滑动触发
start: function (e) {
this.setData({
slide: true
})
},复制代码
|
结束滑动的时候,通过backgroundAudioManager.seek(position)来让音频跳到指定位置,然后判断当前歌词到了多少行,立马设置数据源中的currentIndex,让歌词就会在上面的歌词跳转方法中改变marginTop的值,歌词就会跳转到相应的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | end : function (e) {
const position = e.detail.value
let backgroundAudioManager = this.data.backgroundAudioManager
backgroundAudioManager.seek(position)
this.setData({
currentProcessNum: position,
slide: false
})
for (let j = 0; j <ol start= "4" ><li>第四步使用backgroundAudioManager.onEnded()监听背景音乐的自然结束,结束就调用nextSong()函数,这个函数用来播放待放列表里面的歌。</li></ol><h3 data-id= "heading-18" >播放上一首、播放下一首</h3><figure><img class = "lazyload" src= "https://img.php-/upload/article/000/000/052/864e1386b63dfc50dea53aa88a2d23c5-10.png" data- style= "max-width:90%" data- style= "max-width:90%" alt= "那些年,看看微信小程序仿网易云音乐的相关播放" ><figcaption></figcaption></figure><p>播放前一首歌,那么现在这首歌就变成了下一首要放的歌,因为每一首当前播放的歌曲都会放到被push()到历史列表,那么将当前歌曲(把历史列表数组里面的最后一项从数组删除,并将其<strong>头插</strong>加入到待播放列表)放入待放歌单,然后调用play()方法就好了(传入删除了最后一项之后新的历史列表数组的最后一项,即原历史列表的倒数第二项)</p><pre class = "brush:php;toolbar:false" > // 播放上一首歌曲
beforeSong() {![](https:
if (app.globalData.history_songId.length > 1) {
app.globalData.waitForPlaying.unshift(app.globalData.history_songId.pop())
this.play(app.globalData.history_songId[app.globalData.history_songId.length - 1])
} else {
this.tips( '前面没有歌曲了哦' , '去选歌' , true)
}
},复制代码
|
播放下一首歌曲,如果待播放列表数组长度大于0,那就把数组第一个元素删除并返回传入到play()方法中
1 2 3 4 5 6 7 8 |
nextSong() {
if (app.globalData.waitForPlaying.length > 0) {
this.play(app.globalData.waitForPlaying.shift())
} else {
this.tips( '后面没有歌曲了哦' , '去选歌' , true)
}
},复制代码
|
暂停和播放
比较简单,拿到数据原中的backgroundAudioManager,通过其自带的pause()、play()的方法就可以实现播放和暂停
1 2 3 4 5 6 7 8 9 10 11 12 13 |
handleToggleBGAudio() {
const backgroundAudioManager = this.data.backgroundAudioManager
if (this.data.isPlay) {
backgroundAudioManager.pause();
} else {
backgroundAudioManager.play();
}
this.setData({
isPlay: !this.data.isPlay
})
},复制代码
|
总结
本项目并不复杂,适合初学者上手,因为免去了写复杂的后端,只用写好js逻辑就可以,并且在听到自己仿的小程序可以放出音乐的时候会有很大的成就感,但是同时还是存在一些小坑等待大家处理的,在写本小程序的时候,我也是遇到了挺多问题的,遇到问题先思考,想不出来,就去看看别的大佬写的经验分享,由于本人经验不是特别丰富,只是浅浅入门,很多问题的解决思考的并不到位,如果个位发现我在代码中有什么bug,欢迎个位读者大大指出,期待我们的共同成长。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
暂时没有评论,来抢沙发吧~