播放功能
播放功能是使用H5的Audio标签实现
先说下思路:
- 使用 audio 作为音乐的播放器
- 在歌曲被点击的时候,改变vuex的全局播放状态,并且在播放状态变更的时候 控制 audio 播放或则暂停
- 相关的操作包括控制按钮的样式 都依赖 这个全局的播放状态。可以使用计算属性进行返回class样式名称
<audio ref="audio" :src="currentSong.url"></audio>
computed: {
...mapGetters([
'fullScreen',
'playlist',
'currentSong',
'playing'
]),
playIcon () {
return this.playing ? 'icon-pause' : 'icon-play'
},
miniIcon () {
return this.playing ? 'icon-pause-mini' : 'icon-play-mini'
}
},
methods: {
/* 切换播放状态 */
togglePlaying () {
// 更改vuex中的playing值,还要编写控制Audio停止播放的代码,所以在watch中去监听这个播放状态
this.setPlaying(!this.playing)
}
},
watch: {
// 当前歌曲变化的时候 播放歌曲
currentSong () {
// 在dom没有变化之前调用play会出错,所以使用vue提供的dom更新后调用
this.$nextTick(() => {
this.$refs.audio.play()
})
},
playing (staus) {
// 这里暂时没有发现 有报错,可能和浏览器版本有关吧
staus ? this.$refs.audio.play() : this.$refs.audio.pause()
}
}
}
由于这里只是一些简单的样式和audio的控制,就不详细贴代码了。
在使用中出现了以下异常:
Uncaught (in promise) DOMException: The play() request was interrupted by a new load request.
经过排查定位在 watch-playing中,在首次点击歌曲的时候,会更改vuex中的播放状态,同时会给audio标签的src赋值歌曲地址,如果这个时候,dom还没有被更新(地址还没有赋值上),那么这个时候调用播放按钮就会报错。
所以还是需要更改为 $nextTick 的方式
playing (staus) {
// 这里暂时没有发现 有报错,可能和浏览器版本有关吧
console.log(staus) // 通过增加日志定位异常
this.$nextTick(() => {
staus ? this.$refs.audio.play() : this.$refs.audio.pause()
})
}
对于 nextTick 在vue的官网中有介绍,大致意思就是说,从数据到dom有一个异步队列更新的机制,这个机制就会造成不是非常精准的实时更新,所以提供了这么一个入口。来防止这种情况的发生
1. 迷你播放器播放按钮bug
按照上面的思路完整了播放的效果。可是在迷你播放器播放按钮上点击的时候,状态是改变了,但是同时还打开了全屏的播放器。这里有一个子元素的冒泡事件,如下
<transition name="mini">
// 这里是切换到全屏播放器的事件
<div class="mini-player" v-show="!fullScreen" @click="open">
<div class="icon">
<img width="40" height="40" :src="currentSong.image">
</div>
<div class="text">
<h2 class="name" v-html="currentSong.name"></h2>
<p class="desc">{{currentSong.singer}}</p>
</div>
// 这里是子元素的 播放控制按钮
<div class="control">
<i :class="miniIcon" @click.stop="togglePlaying"></i>
</div>
<div class="control">
<i class="icon-playlist"></i>
</div>
</div>
</transition>
所以这里使用 vue提供的 @click.stop 阻止冒泡到父元素
2. 上/下一首控制功能
对于前面已经铺好了路,所以这里的思路也比较简单:上/下一首的时候,更改当前播放歌曲的索引,就能播放上/下一首歌曲了
methos:{
prev () {
this.setCurrentIndex(this.currentIndex - 1)
// 处理边界
// 在顺序播放模式下才有这样的边界首尾相链接的处理
if (this.currentIndex === -1) {
this.setCurrentIndex(this.playlist.length - 1)
}
// 还要处理一种情况,如果 歌曲现在是暂停状态,上/下一首之后也需要切换到播放状态
if (!this.playing) {
this.togglePlaying()
}
},
next () {
this.setCurrentIndex(this.currentIndex + 1)
if (this.currentIndex === this.playlist.length) {
this.setCurrentIndex(0)
}
if (!this.playing) {
this.togglePlaying()
}
}
}
然而还有一个bug,在快速点击 上/下一首的时候,也会出现 Uncaught (in promise) DOMException: The play() request was interrupted by a new load request. 错误
,这个错误可能也是因为dom刷新不及时造成的。那么就要在歌曲可以播放后,才能点击上/下一首的功能
那怎么才能知道歌曲是否可以播放了呢?
利用audio标签的事件 canplay
和error
事件来处理,可以播放和播放出错或则,网络出错的问题。
<audio ref="audio" :src="currentSong.url" @canplay="ready" @error="error"></audio>
ready () {
this.songReady = true
},
error () {
this.songReady = true
}
思路:
- 使用 songReady 来标识是否可以点击上/下一首的控制按钮
- 在歌曲可以播放的时候 改变为 true,在出错的时候也要改变为true,不然下一首功能就不可用了
- 在歌曲切换后,改变为 false
- 再加上不可点击按钮的样式,样式依赖 这个 songReady的属性
线插播一个样式的bug
3. 播放器cd在不播放的时候没有透明边框
如果上图,只有cd加上转动的动画效果,边框才会把图片容纳进来,我也不知道是为什么,如果不加转动动画的话,图片大小和外面的边框大小是一致的,所以看不到边框。
但是加上动画效果,的时候,图片就被容纳在边框中了。 而且更符合现实,因为一开始就有动画css,但是动画暂停的,所以在切换播放/暂停的时候,cd转动的位置不会像之前一样,暂停后,位置和样式都还原了
到目前为止,有了播放/暂停,上一首/下一首 的功能,接下来要做播放进度条功能
4. 播放时间和更新
播放器样式效果图如下(外围架子,控制部分抽取成组件):
如上样式,分为3部分,左中右,左右各占30px
<div class="progress-wrapper">
<span class="time time-l">0.00</span>
<div class="progress-bar-wrapper"></div>
<span class="time time-r">04.60</span>
</div>
.progress-wrapper {
display flex
align-items center
width 80%
margin 0px auto
padding 10px 0
.time {
color $color-text
font-size $font-size-small
flex 0 0 30px
line-height 30px
width 30px
&.time-l {
text-align left
}
&.time-r {
text-align right
}
}
.progress-bar-wrapper {
flex 1
}
}
这里要获取 当前播放时间,和歌曲的总时间。
- 当前播放时时间,利用 audio的timeupdate事件得到
- 歌曲的总时间从当前播放歌曲数据中得到
// 进度条部分
<div class="progress-wrapper">
<span class="time time-l">{{format(currentTime)}}</span>
<div class="progress-bar-wrapper"></div>
<span class="time time-r">{{format(currentSong.duration)}}</span>
</div>
// audio 播放标签
<audio ref="audio" :src="currentSong.url"
@canplay="ready" @error="error"
@timeupdate="timeupdate"
></audio>
methods:{
timeupdate (e) {
this.currentTime = e.target.currentTime
},
/** 格式化时间,单位秒,返回 00:59 这样的 分:秒 字符串 */
format (interval) {
interval = interval | 0 // 向下取整,同函数Math.floor(7/2)相同功能
const minute = interval / 60 | 0
const second = interval % 60
// 这里有一个问题,返回的秒小于10的时候没有补0填充,需要编写一个工具方法补0
return `${minute}:${this._pad(second)}`
},
/* pad 有填充的意思
* num : 数字
* n : 要填充的位数
*/
_pad (num, n = 2) {
// 这里针对秒做补零操作
let len = num.toString().length
let result = num
while (len < n) {
result = '0' + result
len++
}
return result
}
}