【音乐App】—— Vue

前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。

项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。

播放暂停

前进后退

一、播放器Vuex数据设计

需求: 播放器可以通过歌手详情列表、歌单详情列表、排行榜、搜索结果多种组件打开,因此播放器数据一定是全局的

state.js目录下:定义数据

import {playMode} from '@/common/js/config'

const state = {

singer: {},

playing: false, //播放状态

fullScreen: false, //播放器展开方式:全屏或收起

playlist: [], //播放列表(随机模式下与顺序列表不同)

sequenceList: [], //顺序播放列表

mode: playMode.sequence, //播放模式: 顺序、循环、随机

currentIndex: -1 //当前播放歌曲的index(当前播放歌曲为playlist[index])

}

common->js目录下:创建config.js配置项目相关

//播放器播放模式: 顺序、循环、随机

export const playMode = {

sequence: 0,

loop: 1,

random: 2

}

getter.js目录下:数据映射(类似于计算属性)

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] || {}

}

组件中可以通过mapgetters拿到这些数据

mutaion-type.js目录下:定义事件类型字符串常量

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中都是动作,前缀加SET、UPDATE等

mutaion.js目录下:操作state

const mutations = {

[types.SET_SINGER](state, singer){

state.singer = singer

},

[types.SET_PLAYING_STATE](state, 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

}

}

二、播放器Vuex的相关应用

components->player目录下:创建player.vue

基础DOM:

播放器

App.vue中应用player组件:因为它不是任何一个路由相关组件,而是应用相关播放器,切换路由不会影响播放器的播放

player.vue中获取数据:控制播放器的显示隐藏

import {mapGetters} from 'vuex'

computed: {

...mapGetters([

'fullScreen',

'playlist'

])

}

通过v-show判断播放列表有内容时,显示播放器,依据fullScreen控制显示不同的播放器

播放器

song-list.vue中添加点击播放事件:基础组件不写业务逻辑,只派发事件并传递相关数据

@click="selectItem(song, index)

selectItem(item, index){

this.$emit('select', item, index)

}

子组件行为,只依赖本身相关,不依赖外部调用组件的需求,传出的数据可以不都使用

music-list.vue中监听select事件

设置数据,提交mutations:需要在一个动作中多次修改mutations,在actions.js中封装

import * as types from './mutation-types'

export const selectPlay = function ({commit, state}, {list, index}) {

//commit方法提交mutation

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,并在methods中调用:

import {mapActions} from 'vuex'

selectItem(item, index){

this.selectPlay({

list: this.songs,

index

})

}

...mapActions([

'selectPlay'

])

三、播放器基础样式及歌曲数据的应用

通过mapGetter获取到currentSong数据填入到DOM中:点击切换播放器展开收起,需要修改fullScreen

import {mapGetters, mapMutations} from 'vuex'

methods: {

back() {

//错误做法: this.fullScreen = false

//正确做法: 通过mapMutations写入

this.setFullScreen(false)

},

open() {

this.setFullScreen(true)

},

...mapMutations({

setFullScreen: 'SET_FULL_SCREEN'

})

}

四、播放器展开收起动画

需求:normal-player背景图片渐隐渐现,展开时头部标题从顶部下落,底部按钮从底部回弹,收起时相反

实现:动画使用,回弹效果使用贝塞尔曲线

normal-player设置动画

&.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-player设置动画

&.mini-enter-active, &.mini-leave-active

transition: all 0.4s

&.mini-enter, &.mini-leave-to

opacity: 0

需求:展开时,mini-player的专辑图片从原始位置飞入CD图片位置,同时有一个放大缩小效果, 对应顶部和底部的回弹;收起时,normal-player的CD图片从原始位置直接落入mini-player的专辑图片位置

实现:Vue提供了javascript事件钩子,在相关的钩子中定义CSS3动画即可

利用第三方库:create-keyframe-animation 使用js编写CSS3动画

github地址:https://github.com/HenrikJoreteg/create-keyframe-animation

安装:

npm install create-keyframe-animation --save

引入:

import animations from 'create-keyframe-animation'

methods中封装函数_getPosAndScale获取初始位置及缩放尺寸: (计算以中心点为准)

_getPosAndScale(){

const targetWidth = 40 //mini-player icon宽度

const width = window.innerWidth * 0.8 //cd-wrapper宽度

const paddingLeft = 40

const paddingTop = 80

const paddingBottom = 30 //mini-player icon中心距底部位置

const scale = targetWidth / width

const x = -(window.innerWidth / 2 - paddingLeft) //X轴方向移动的距离

const y = window.innerHeight - paddingTop - width / 2 - paddingBottom

return {

x,

y,

scale

}

}

给cd-wrapper添加引用:

定义事件钩子方法:

//事件钩子:创建CSS3动画

enter(el, done){

const {x, y, scale} = this._getPosAndScale()

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)`

}

}

animations.registerAnimation({

name: 'move',

animation,

presets: {

duration: 400,

easing: 'linear'

}

})

animations.runAnimation(this.$refs.cdWrapper, 'move', done)

},

afterEnter() {

animations.unregisterAnimation('move')

this.$refs.cdWrapper.style.animation = ''

},

leave(el, done){

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})`

this.$refs.cdWrapper.addEventListener('transitionend', done)

},

afterLeave(){

this.$refs.cdWrapper.style.transition = ''

this.$refs.cdWrapper.style[transform] = ''

}

transform属性使用prefix自动添加前缀:

import {prefixStyle} from '@/common/js/dom'

const transform = prefixStyle('transform')

五、播放器歌曲播放功能实现--H5 audio

添加H5

在watch中监听currentSong的变化,播放歌曲

watch: {

currentSong() {

this.$nextTick(() => { //确保DOM已存在

this.$refs.audio.play()

})

}

}

给按钮添加点击事件,控制播放暂停

通过mapGetters获得playing播放状态

通过mapMutations定义setPlayingState方法修改mutation:

setPlayingState: 'SET_PLAYING_STATE'

定义togglePlaying()修改mutation:传递!playing为payload参数

togglePlaying(){

this.setPlayingState(!this.playing)

}

在watch中监听playing的变化,执行播放器的播放或暂停:

playing(newPlaying){

const audio = this.$refs.audio

this.$nextTick(() => { //确保DOM已存在

newPlaying ? audio.play() : audio.pause()

})

}

坑:调用audio标签的play()或pause(),都必须是在DOM audio已经存在的情况下,否则就会报错

解决: 在this.$nextTick(() => { })中调用

图标样式随播放暂停改变:动态绑定class属性playIcon,替换掉原原来的icon-play

playIcon() {

return this.playing ? 'icon-pause' : 'icon-play'

}

CD 旋转动画效果

动态绑定class属性cdCls:

cdCls() {

return this.playing ? 'play' : 'pause'

}

CSS样式:

&.play

animation: rotate 20s linear infinite

&.pause

animation-play-state: paused

@keyframes rotate

0%

transform: rotate(0)

100%

transform: rotate(360deg)

六、播放器歌曲前进后退功能实现

给按钮添加点击事件

通过mapGetters获得currentIndex当前歌曲index

通过mapMutations定义setCurrentIndex方法修改mutation

setCurrentIndex: 'SET_CURRENT_INDEX'

定义prev()和next()修改mutation: 限制index边界

next() {

let index = this.currentIndex + 1

if(index === this.playlist.length){

index = 0

}

this.setCurrentIndex(index)

},

prev() {

let index = this.currentIndex - 1

if(index === -1){

index = this.playlist.length - 1

}

this.setCurrentIndex(index)

}

坑:前进或后退后会自动开始播放,但播放按钮的样式没有改变

解决:添加判断,如果当前是暂停状态, 切换为播放

if(!this.playing){

this.togglePlaying()

}

坑:切换太快会出现报错:Uncaught (in promise) DOMException: The play() request was interrupted by a new load request

原因:切换太快audio 数据还没有加载好

解决:audio W3C文档中记录,audio有两个事件:

当歌曲地址请求到时,会派发canplay事件;

当没有请求到或请求错误时,会派发error事件

在data中维护一个标志位数据songReady,通过ready方法控制只有歌曲数据请求好后,才可以播放

data() {

return {

songReady: false

}

}

ready() {

this.songReady = true

}

在prev()、next()和togglePlaying中添加判断,当歌曲数据还没有请求好的时候,不播放

if(!this.songReady){

return

}

其中prev()和next()中歌曲发生改变了之后,重置songReady为false,便于下一次ready()

this.songReady = false

坑:当没有网络,或切换歌曲的url有问题时,songReady就一直为false,所有播放的逻辑就执行不了了

解决: error()中也使songReady为true,这样既可以保证播放功能的正常使用,也可以保证快速点击时不报错

优化: 给按钮添加disable的样式

disableCls() {

return this.songReady ? '' : 'disable'

}

&.disable

color: $color-theme-d

七、播放器时间获取

data中维护currentTime当前播放时间:currentTime: 0 (audio的可读写属性)

audio中监听时间更新事件:

@timeupdate="updateTime"

methods中定义updateTime()获取当前时间的时间戳,并封装format函数格式化:

//获取播放时间

updateTime(e) {

this.currentTime = e.target.currentTime //时间戳

},

format(interval){

interval = interval | 0 //向下取整

const minute = interval / 60 | 0

const second = this._pad(interval % 60)

return `${minute}:${second}`

}

坑:秒一开始显示个位只有一位数字,体验不好

解决:定义_pad()用0补位

_pad(num, n = 2){ //用0补位,补2位字符串长度

let len = num.toString().length

while(len < n){

num = '0' + num

len++

}

return num

}

格式化后的数据填入DOM,显示当前播放时间和总时间:

{{format(currentTime)}}

{{format(currentSong.duration)}}

八、播放器progress-bar进度条组件实现

base->progress-bar目录下:创建progress-bar.vue

需求:进度条和小球随着播放时间的变化而变化

实现:

从父组件接收props参数:进度比percent(player.vue中通过计算属性得到)

watch中监听percent,通过计算进度条总长度和偏移量,动态设置进度条的width和小球的transform

const progressBtnWidth = 16 //通过样式设置得到

props: {

percent: {

type: Number,

default: 0

}

},

watch: {

percent(newPercent) {

if(newPercent >= 0){

const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth

const offsetWidth = newPercent * barWidth

this.$refs.progress.style.width = `${offsetWidth}px` //进度条偏移

this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` //小球偏移

}

}

}

需求:拖动进度条控制歌曲播放进度

实现:

监听touchstart、touchmove、touchend事件,阻止浏览器默认行为;

@touchstart.prevent="progressTouchStart"

@touchmove.prevent="progressTouchMove"

@touchend="progressTouchEnd">

created()中创建touch空对象,用于挂载共享数据;

created(){

this.touch = {}

}

methods中定义3个方法,通过计算拖动偏移量得到进度条总偏移量,并派发事件给父组件:

progressTouchStart(e) {

this.touch.initiated = true //标志位 表示初始化

this.touch.startX = e.touches[0].pageX //当前拖动点X轴位置

this.touch.left = this.$refs.progress.clientWidth //当前进度条位置

},

progressTouchMove(e) {

if(!this.touch.initiated){

return

}

const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth

const deltaX = e.touches[0].pageX - this.touch.startX //拖动偏移量

const offsetWidth = Math.min(barWidth, Math.max(0, this.touch.left + deltaX))

this._offset(offsetWidth)

},

progressTouchEnd() {

this.touch.initiated = false

this._triggerPercent()

},

_triggerPercent(){

const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth

const percent = this.$refs.progress.clientWidth / barWidth

this.$emit('percentChange', percent)

},

_offset(offsetWidth){

this.$refs.progress.style.width = `${offsetWidth}px` //进度条偏移

this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` //小球偏移

}

watch中添加条件设置拖动时,进度条不随歌曲当前进度而变化:

watch: {

percent(newPercent) {

if(newPercent >= 0 && !this.touch.initiated){

const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidth

const offsetWidth = newPercent * barWidth

this._offset(offsetWidth)

}

}

}

player.vue组件中监听percentChange事件,将改变后的播放时间写入currentTime,并设置改变后自动播放:

@percentChange="onProgressBarChange"

onProgressBarChange(percent) {

this.$refs.audio.currentTime = this.currentSong.duration * percent

if(!this.playing){

this.togglePlaying()

}

}

需求:点击进度条任意位置,改变歌曲播放进度

实现:添加点击事件,通过react.left计算得到偏移量,设置进度条偏移,并派发事件改变歌曲播放时间

progressClick(e) {

const rect = this.$refs.progressBar.getBoundingClientRect()

const offsetWidth = e.pageX - rect.left

this._offset(offsetWidth)

this._triggerPercent()

}

九、播放器progress-circle圆形进度条实现 -- SVG

base->progress-circle目录下:创建progress-circle.vue

使用SVG实现圆:

xmlns="http://www.w3.org/2000/svg">

:stroke-dasharray="dashArray" :stroke-dashoffset="dashOffset"/>

需要从父组件接收props参数:视口半径、当前歌曲进度百分比

props: {

radius: {

type: Number,

default: 100

},

percent: {

type: Number,

default: 0

}

}

player.vue中使圆形进度条包裹mini-player的播放按钮,并传入半径和百分比:

progress-circle.vue中维护数据dashArray,并使用computed计算出当前进度对应的偏移量:

data() {

return {

dashArray: Math.PI * 100 //圆周长 描边总长

}

},

computed: {

dashOffset() {

return (1 - this.percent) * this.dashArray //描边偏移量

}

}

注:项目来自慕课网

2026-07-22 05:30:31
"班花、校花、校草" 这个在 日语 里怎么说?
六味地黄丸能不能长期吃、哪些人不能吃?