{{song.name}}
{{getDesc(song)}}
多个组件都可以打开播放器,所以我们要控制这个播放器的数据,一定是全局的,所以我们把数据通过vuex来管理
1. Vuex数据设计,很多歌单都可以打开播放器,打开歌单缩小时,会出页面最下方出现小型的播放器,所以播放器的数据一定是全局的,所以把这个数据通过vuex来管理
在state.js中添加播放器的状态,并在common->config.js中添加播放器的播放模式(顺序,随机,循环)
首先,在state.js中添加与播放器相关的属性:
import {playMode} from 'common/js/config'
const state = {
singer: {},
playing: false, // 播放状态
fullScreen: false, // 全屏
playlist: [], // 正在播放的播放器列表,播放器后退会出现播放列表
sequenceList: [], // 顺序播放时的播放器列表
mode: playMode.sequence, // 顺序播放
currentIndex: -1 // 当前播放的歌曲的索引,前进后退播放修改currentIndex
}
export default state
其中mode的类型放在配置文件中,js->config.js
export const playMode = {
sequence: 0,
loop: 1,
random: 2
}
2. 添加完state中的初始状态,可以由初始状态计算而来的数据放在getters.js中,getters是对状态的一种映射
export const singer = state => state.singer // 使用getter取到组件里的数据
export const playing = state => state.playing
export const fullScreen = state => state.fullScreen
export const playlist = state => state.playlist
export const sequenceList = state => state.sequenceList
export const mode = state => state.mode
export const currentIndex = state => state.currentIndex
export const currentSong = (state) => { // 可以担任计算属性,当前播放的歌曲
return state.playlist[state.currentIndex] || {}
}
3. 操作state要写在mutations.js中,但是要先写mutation-types.js,做字符串映射
// 定义mutation的常量
export const SET_SINGER = 'SET_SINGER'
export const SET_PLAYING_STATE = 'SET_PLAYING_STATE'
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'
export const SET_PLAYLIST = 'SET_PLAYLIST'
export const SET_SEQUENCE_LIST = 'SET_SEQUENCE_LIST'
export const SET_PLAY_MODE = 'SET_PLAY_MODE'
export const SET_CURRENT_INDEX = 'SET_CURRENT_INDEX'
利用mutation-types.js去写mutation.js修改state数据
import * as types from './mutation-types'
const mutations = { // mutation相关的修改方法
[types.SET_SINGER](state, singer) { // 当前状态树的state,提交mutation时传的payload
state.singer = singer
},
[types.SET_PLAYING_STATE](state, flag) { // boolean用flag来表示
state.playing = flag
},
[types.SET_FULL_SCREEN](state, flag) {
state.fullScreen = flag
},
[types.SET_PLAYLIST](state, list) {
state.playlist = list
},
[types.SET_SEQUENCE_LIST](state, list) {
state.sequenceList = list
},
[types.SET_PLAY_MODE](state, mode) {
state.mode = mode
},
[types.SET_CURRENT_INDEX](state, index) {
state.currentIndex = index
}
}
export default mutations
4. actions主要有两种操作,一个是异步操作,一个是mutation的一个封装,即我们可以通过一个action修改多个mutations
5.新建player.vue组件,分为nomal-player和mini-player,播放器组件放在哪里???
新建player.vue组件,创建初始架构,并将这个组件放在app.vue下,因为他不是任何一个路由相关的东西,只是一个应用相关的播放器,在任何路由下都可以去播放,切换路由也不会影响播放器的播放,所以将它定义到app.vue下,在app.vue中注册并添E:\my-vue\my-music-vue\src\common\js\dom.js加,
别忘了import和component组件
刷新之后播放器默认展开,盖住了原来的界面,所以我们要在app.vue中控制一下播放器的隐藏,回到player组件,用vuex的相关数据控制播放器器的展示;首先要从Vuex取一些数据到组件里面,在player.vue中去import我们想要的数据,控制播放器的出现,默认不出现,在play.vue中;
import {mapGetters} from 'vuex'
export default {
computed: {
...mapGetters([
'fullScreen',
'playlist'
])
}
}
整个播放器的显示,要求playlist.length > 0, normal的显示要求fullScreen(全屏显示),mini-player为!fullScreen
这样播放器就不会默认展示了,我们希望在点击歌曲的时候才会弹出播放器,回到song-list.vue添加事件
-
{{song.name}}
{{getDesc(song)}}
然后去methods中实现点击参数selectitem
selectItem(item, index) { // 只派发一个函数,告知父组件music-list.vue我已被点击
this.$emit('select', item, index)
}
然后回到父组件music-list.vue添加监听事件,
在methods中定义selectItem,接受item和index两个参数,当我们点击列表的时候我们要设置playList或者是sequencdList,其次根据我们点击的索引,我们可以设置currentIndex;点击的时候歌曲就要播放了,playstate也是要设置的,我们要默认展开全屏的播放器,fullScreen也是要设置的
所以,在selectItem中设置有关播放器的数据就需要提交mutations,如果在一个动作中多次去改变mutation,会封装一个action,则回到actions.js,定义一个动作,selectPlay选择播放
import * as types from './mutation-types'
// commit方法和state属性,可以拿到state,第二个参数是一个payload,告知列表和索引
export const selectPlay = function ({commit, state}, {list, index}) {
// 提交mutations,设置list,默认是顺序播放
commit(types.SET_SEQUENCE_LIST, list)
commit(types.SET_PLAYLIST, list)
commit(types.SET_CURRENT_INDEX, index)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
}
回到music-list.vue中进行调用,获取actions,并在监听函数selectItem中调用修改播放器状态
import {mapActions} from 'vuex'
...mapActions([
'selectPlay'
])
在music-list.vue中继续编写监听函数selectItem;
selectItem(item, index) { // 在song-list.vue中点击歌曲传过来的song和item
this.selectPlay({ // 完成action的调用
list: this.songs, // 整个songs都要传进去
index
})
action调用以后,action就要逻辑执行,mutation就会改变,然后通过player.vue中的mapGetter映射到player.vue(播放器)中,再传递到DOM中,这样就完成了对播放器播放状态的改变
接着 ,填充play组件的DOM区域,先写样式,再填充数据,先通过mapGetter把currentSong映射到组件里面
computed: {
...mapGetters([
'fullScreen',
'playlist',
'currentSong',
])
}
然后将currentSong相关的数据填充到DOM结构中,如下:
然后在看min-player部分,因为min-player是需要把fullScreen设为false的,也就是我们点击左上角缩小按钮的时候,进行缩小
这里我们不能直接将fullScreen设置为false,应该通过mutation进行改变,首先引入mapMutations,
import {mapGetters, mapMutations} from 'vuex'
在methods中定义...mapMutations
...mapMutations({
setFullScreen: 'SET_FULL_SCREEN' // 对应到mutation-types
})
在mapMutations中取到SET_FULL_SCREEN的映射之后,在回到back中调用setFullScreen
back() {
// 此处通过mutation改变fullScreen,引入mapMutations,通过...mapMutations取得映射
this.setFullScreen(false)
}
点击小型播放器的时候可以打开全屏
open() {
this.setFullScreen(true)
}
播放器展开收起动画的添加,用transition去包裹这两个区块
&.normal-enter-active, &.normal-leave-active
transition: all 0.4s
.top, .bottom
transition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32)
&.normal-enter, &.normal-leave-to
opacity: 0
.top
transform: translate3d(0, -100px, 0)
.bottom
transform: translate3d(0, 100px, 0)
&.mini-enter-active, &.mini-leave-active
transition: all 0.4s
&.mini-enter, &.mini-leave-to
opacity: 0
当大的播放器缩小时,我们希望中间的图片能有一个从中间往左下角飞入的一个样式,利用vue给我提供的一个js的一个钩子,在钩子里创建一个csss3的一个animition,首先给nomal添加几个事件
对应钩子函数实现响应的动画效果即可,在methods中定义钩子函数
import animations from 'create-keyframe-animation'
引入了第三方库,我们想到首先我们要获取图片的位置,所以定义一个函数计算位置,是中心点到中心点的偏移
定义函数计算大小图片之间的缩放比例和x,y之间的偏移量
_getPosAndScale() {
const targetWidth = 40 // min播放器的宽度
const paddingLeft = 40 // min图片的圆心距离左侧有40的偏移
const paddingBottom = 30 // min图片的圆心到底部偏移为30px
const paddingTop = 80 // 大图片到屏幕顶部的高度
const width = window.innerWidth * 0.8 // cd-wrapper的宽度
const scale = targetWidth / width // 初始的缩放比例
const x = -(window.innerWidth / 2 - paddingLeft) // 初始的x,y坐标的偏移量
const y = window.innerHeight - paddingTop - width / 2 - paddingBottom
return {
x,
y,
scale
}
}
定义动画:
enter(el, done) { // DOM, 动作,done执行完机会跳入下一个
// 首先要获取图片在animal播放器的具体位置
const {x, y, scale} = this._getPosAndScale()
// 定义animation
let animation = {
0: {
transform: `translate3d(${x}px,${y}px,0) scale(${scale})`
},
60: {
transform: `translate3d(0,0,0) scale(1.1)`
},
100: {
transform: `translate3d(0,0,0) scale(1)`
}
}
// 注册animation
animations.registerAnimation({
name: 'move', // 动画名称
animation,
presets: { // 预设字段
duration: 400,
easing: 'linear'
}
})
// 运行animation,绑定dom
animations.runAnimation(this.$refs.cdWrapper, 'move', done)
},
afterEnter() { // 清空animation
animations.unregisterAnimation('move')
this.$refs.cdWrapper.style.animation = ''
},
leave(el, done) { // leave执行完之后就会跳入afterLeave
this.$refs.cdWrapper.style.transition = 'all 0.4s'
// 最终目标运行的位置
const {x, y, scale} = this._getPosAndScale()
this.$refs.cdWrapper.style[transform] = `translate3d(${x}px,${y}px,0) scale(${scale})`
// 监听事件transitionend,执行完之后就执行afterLeave
this.$refs.cdWrapper.addEventListener('transitionend', done)
}
歌曲的播放功能,主要是利用html5的audio标签实现的,指定url和play方法
我们在currentSong发生改变的时候去调用play函数,在watch中观测currentSong的变化,必须要保证url获取到之后去调用play函数。所以我们可以加一个延时
watch:{
currentSong() {
this.$nextTick.(() => {
this.$refs.audio.play()
})
}
}
现在音乐可以播放了,但是我们无法控制音乐,现在我们来控制音乐的暂停
添加点击事件控制音乐的暂停,我们在state中定义了playing来控制音乐的播放暂停,点击歌曲列表的时候回提交一个action,在action中有一个mutation,提交了一个commit(types.SET_PLAYING_STATE, true);所以在我们点击歌曲列表的时候playing的状态就为true了,在play.vue中,我们可以通过maapGetter获取当前player的状态
computed: {
...mapGetters([
'fullScreen',
'playlist',
'currentSong',
'playing', // 获取播放的状态
]),
这里的playing就映射到getters.js中的export const playing = state => state.playing,所以我们在组件中就可以通过this.playing来获取当前的playing的状态,并通过一个mutation来改变我们当前的状态,再通过mapMutation中设置一个setPlayingState: 'SET_PLAYING_STATE',映射到mutation-type中的'SET_PLAYING_STATE'状态
...mapMutations({
setFullScreen: 'SET_FULL_SCREEN', // 对应到mutation-types
setPlayingState: 'SET_PLAYING_STATE',
})
},
然后就可以通过调用this.setPlayingState方法,去切换playing的状态
togglePlaying() {
this.setPlayingState(!this.playing)
}
仅仅设置paying是不能设置我们播放器的停止的,因为真正控制音乐播放的还是播放器,所以说我们还要去watch playing的状态,通过playing状态的改变来控制播放器的paly或是pause,不要忘了添加nextTick
playing(newPlaying) {
this.$nextTick(() => {
const audio = this.$refs.audio
newPlaying ? audio.play() : audio.pause()
})
}
},
我们是实现了点击之后播放暂停音乐的功能,但是暂停键和播放键的样式并没有改变
playIcon() {
return this.playing ? 'icon-pause' : 'icon-play'
}
对应到DOM中的播放按钮
并将min-play中的图标也更新,别忘了添加点击事件togglePlaying
miniIcon() {
return this.playing ? 'icon-pause-mini' : 'icon-play-mini'
},
点击小窗口暂停的时候nomal播放器也弹出来了,这是因为子元素的点击事件会冒泡到父元素上,父元素的点击事件打开了播放器,所以要添加@click.stop = "togglePlaying"
接下来实现cd的旋转
img
border-radius: 50%
&.play
animation: rotate 10s linear infinite
&.pause
animation-play-state: paused
之后在大小播放器的图片上添加class
cdCls() {
return this.playing ? 'play' : 'play_pause'
},
接下来实现前进后退功能
我们在vuex中有一个state是currentIndex:-1;表示当前播放歌曲的索引,当我们点击歌曲列表的时候,派发一个action,action里面有一个mutation的提交,修改了currentIndex(commit(types.SET_CURRENT_INDEX, index)),curentIndex触发了currentSong的变化,currentSong的audio的src发生变化,然后watch中就观察到currentSong的变化,然后去调用play()进行播放,就改变了歌曲,我们只需要给icon-pre和icon-next绑定两个事件,改变currentIndex的值就可以实现歌曲的前进后退了
首先在mapGetter中拿到currentIndex的值
computed: {
...mapGetters([
'fullScreen',
'playlist',
'currentSong',
'playing', // 获取播放的状态
'currentIndex'
]),
在next函数中改变currentIndex的状态
next () {
let index = this.currentIndex + 1
if(index === this.playlist.length) { // 最后一首歌的情况
index = 0
}
}
改变了状态之后,要通过一个mapMutaions定义的setCurrentIndex将值的变化传递给state
...mapMutations({
setFullScreen: 'SET_FULL_SCREEN', // 对应到mutation-types
setPlayingState: 'SET_PLAYING_STATE',
setCurrentIndex: 'SET_CURRENT_INDEX'// 相当于提交一个mutatation去改变我们的currentINdex
})
之后,回到index函数中调用setCurrentIndex函数,将修改后的index传递给state
next () {
let index = this.currentIndex + 1
if(index === this.playlist.length) { // 最后一首歌的情况
index = 0
}
this.setCurrentIndex(index) // 提交mutation,修改index
},
prev的逻辑与之类似
prev() {
let index = this.currentIndex - 1
if(index === -1) {
index = this.playlist.length
}
this.setCurrentIndex(index)
},
点击暂停之后,切换到下一首歌,歌曲播放了,但是按钮图标却没有发生变化,在prev和next中添加
if (!this.playing) { // 暂停的时候通过toggleplaing是改变playing的状态,让其变成播放状态
this.togglePlaying()
}
当快速切换下一首的时候,会出现之前DOMException的情况,其实audio中有两个事件,一个是canplay事件,歌曲从加载到播放会派发一个事件叫canplay,当歌曲发生错误请求不到地址的时候会派发一个函数@error;
之后当歌曲ready的时候,才能点下一首歌,不然就不能点下一首歌,我们可以使用一个标志位来控制,首先我们在data中添加一个变量叫songReady
data() {
return {
songReady: false
}
}
然后添加一个ready函数,使其songReady为true时才可出发canPlay
ready() {
this.songReady = true
}
写好songReady之后,回到next和prev函数中控制歌曲的播放
next () {
if (!this.songReady) {
return
}
let index = this.currentIndex + 1
if(index === this.playlist.length) { // 最后一首歌的情况
index = 0
}
this.setCurrentIndex(index) // 修改index
if (!this.playing) { // 暂停的时候通过toggleplaing是改变playing的状态,让其变成播放状态
this.togglePlaying()
}
this.songReady = false // 重置,下一首还没准备好呢
},
prev() {
if (!this.songReady) {
return
}
let index = this.currentIndex - 1
if(index === -1) {
index = this.playlist.length
}
this.setCurrentIndex(index)
if (!this.playing) { // 暂停的时候通过toggleplaing是改变playing的状态,让其变成播放状态
this.togglePlaying()
}
this.songReady = false // 重置,下一首还没准备好呢
},
顺便定义一下error,当url失败时执行一下songReady逻辑,以免之后的点击播放不能使用
error() {
// 当歌曲加载失败时出发error函数
this.songReady = true
},
当按钮不能点击的时候,我们给按钮一个disable的属性,让其
disableCls() {
return this.songReady ? '' : 'disable'
}
并将disable属性用到三个icon按钮上
播放器的进度:左侧为歌曲播放的时间,右侧为总时间,中间是进度条
{{format(currentTime)}}
{{format(currentSong.duration)}}
现在data中定义表示当前时间的变量
data() {
return {
songReady: false,
currentTime: 0
}
},
当歌曲在播放的时候audio标签会派发一个事件,timeupdate
在methods中定义这个updateTime
updateTime(e) { // target就是audio标签,有currentTime属性
this.currentTime = e.target.currentTime
}
我们不可以直接将currentTime直接写到DOM中,他是一个时间戳,不能直接写到界面上
format(interval) {
interval = interval | 0 // 一个正数的向下取整
const minute = interval / 60 | 0
const second = this._pad(interval % 60)
return `${minute}:${second}`
}
其中this._pad是对秒数进行补零
_pad(num, n = 2) { // 设计补位,补0,默认是两位数
let len = num.toString().length
while(len < n) {
num = '0' + num
len++
}
return num
}
回到DOM中调用这个函数,当前时间
{{format(currentTime)}}
总时间
{{format(currentSong.duration)}}
接下来实现进度条组件和圆形进度条组件,下面在base下编写进度条的基础组件,progress-bar.vue
将组件引入到player组件中,并填充到DOM里
添加css样式后基本样式有了,接下来就是实现进度条位置的移动,进度中应该从父类player中接受一个百分比的属性,所以在progress-bar中,添加props
props: {
percent: {
type: Number,
default: 0
}
}
然后去watch,percent是从外部组件传递过来的一个不断改变的值
const progressBtnWidth = 16 // 原点按钮的长度
watch: {
percent(newPercent) {
if (newPercent >= 0 && !this.touch.initiated) { // 没有在拖动的时候采取改变percent
// 整个进度条的宽度,要减去按钮的宽度,通过常量定义
const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
// 偏移的宽度
const offsetWidth = newPercent * barWidth
// 设置progress的宽度,即进度条走过的长度
this._offset(offsetWidth)
}
}
}
定义offset函数,实现进度的移动和btn的偏移
import {prefixStyle} from 'common/js/dom'
const transform = prefixStyle('transform')
_offset(offsetWidth) {
// 已经走过的进度条的长度
this.$refs.progress.style.width = `${offsetWidth}px`
// 小球按钮的长度
this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px,0,0)`
}
回到父组件player中计算persent的值,计算属性
percent() {
return this.currentTime / this.currentSong.duration
},
然后将percent传递给子组件
接下来实现进度条的拖拽和点击,首先在btn上添加几个点击事件,不要忘了添加prevent去阻止浏览器的默认行为
我们需要一个实力上的touch target对象来维护不同点击事件中的通信
created() {
this.touch = {} // 在不同的回调函数中将共享数据挂载到touch对象上
}
去methods中定义这三个事件函数
progressTouchStart(e) {
this.touch.initiated = true // 已经被初始化了
this.touch.startX = e.touches[0].pageX // 横向坐标
this.touch.left = this.$refs.progress.clientWidth // 进度条的初始偏移量,进度条已经走过的长度
},
progressTouchMove(e) {
if (!this.touch.initiated) {
return
}
const deltaX = e.touches[0].pageX - this.touch.startX // 计算偏移量
// this.touch.left + deltaX为相对于整个界面的偏移,但是不能超出整个进度条的宽度 // 已经偏移的量加上deltaX,值要大于0
const offsetWidth = Math.min(this.$refs.progressBar.clientWidth - progressBtnWidth, Math.max(0, this.touch.left + deltaX))
// 设置进度条的偏移和btn的transform
this._offset(offsetWidth)
},
progressTouchEnd() { // 重置为false
this.touch.initiated = false
this._triggerPercent() // 派发事件,将拖动进度条信息派发出去
}
至此,进度条已经可以拖动了,进度条滚动过的长度和btn都会随着进度条的拖动儿移动,但是还没有,但是进度条被拖动之后会跳回到正在播放歌曲所在进度条的位置,那是因为只要歌曲在播放,percent就在变化,她控制着进度条的位置,在watch中又修改了它的宽度
我们希望在拖动的时候不受到percent更新的困扰,在watch percent的时候加入判断条件: !this.touch.initiated,保证watch到的percent的变化实在进度条没有被拖动的前提下
watch: {
percent(newPercent) {
if (newPercent >= 0 && !this.touch.initiated) { // 没有在拖动的时候采取改变percent
// 整个进度条的宽度,要减去按钮的宽度,通过常量定义
const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
// 偏移的宽度
const offsetWidth = newPercent * barWidth
// 设置progress的宽度,即进度条走过的长度
this._offset(offsetWidth)
}
}
}
等我们拖动完进度条之后,touch.initated被设置为false,percent的值又可以被watch到了,解决办法就是在end的时候将拖动的进度告诉给外层播放器,让歌曲播放的进度(peercent)与进度条位置相同,所以在end的时候派发一个函数
progressTouchEnd() { // 重置为false
this.touch.initiated = false
this._triggerPercent() // 派发事件,将拖动进度条信息派发出去
},
在touchEnd中调用triggerPercent,使其派发一个监听函数,将percent的变化派发出去
_triggerPercent() {
// 进度条的长度
const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth
// 运动的percent
const percent = this.$refs.progress.clientWidth / barWidth
this.$emit('percentChange', percent)
}
然后回到player组件中监听percentChange
操作audio对象的currentTime才能更新进度条的进度,currentTime是进度条自带的属性
onProgressBarChange(percent) {
// 设置audio的currentTime,当前歌曲的总时间乘以percent
const currentTime = this.currentSong.duration * percent
this.$refs.audio.currentTime = currentTime
if (!this.playing) { // 若是拖动进度条之后暂停了,就变为继续播放
this.togglePlaying()
}
}
先暂停,然后拖动到某个位置,他不会自动播放
if (!this.playing) { // 若是拖动进度条之后暂停了,就变为继续播放
this.togglePlaying()
}
实现了进度条的拖动之后,我们为进度条添加点击事件,在progress-bar中