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

项目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.currentIndexexport 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
  1. 基础DOM:

    <div class="normal-player">播放器
    </div>
    <div class="mini-player"></div>

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

    <player></player>

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

    import {mapGetters} from 'vuex'computed: {...mapGetters(['fullScreen','playlist'])
    }

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

    <div class="player" v-show="playlist.length"><div class="normal-player" v-show="fullScreen">播放器</div><div class="mini-player" v-show="!fullScreen"></div>
    </div>

  • song-list.vue中添加点击播放事件:基础组件不写业务逻辑,只派发事件并传递相关数据
    @click="selectItem(song, index)

    selectItem(item, index){this.$emit('select', item, index)
    }

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

  • music-list.vue中监听select事件
    <song-list :songs="songs" @select="selectItem"></song-list>

  1. 设置数据,提交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)
    }

  2. 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背景图片渐隐渐现,展开时头部标题从顶部下落,底部按钮从底部回弹,收起时相反
  • 实现:动画使用<transition>,回弹效果使用贝塞尔曲线
  1. normal-player设置动画<transition name="normal">

    &.normal-enter-active, &.normal-leave-activetransition: all 0.4s.top, .bottomtransition: all 0.4s cubic-bezier(0.86, 0.18, 0.82, 1.32)
    &.normal-enter, &.normal-leave-toopacity: 0.toptransform: translate3d(0, -100px, 0).bottomtransform: translate3d(0, 100px, 0)

  2. mini-player设置动画<transition name="mini">
    &.mini-enter-active, &.mini-leave-activetransition: all 0.4s
    &.mini-enter, &.mini-leave-toopacity: 0

  • 需求:展开时,mini-player的专辑图片从原始位置飞入CD图片位置,同时有一个放大缩小效果, 对应顶部和底部的回弹;收起时,normal-player的CD图片从原始位置直接落入mini-player的专辑图片位置
  • 实现:Vue提供了javascript事件钩子,在相关的钩子中定义CSS3动画即可
  1. 利用第三方库:create-keyframe-animation 使用js编写CSS3动画
  2. github地址:https://github.com/HenrikJoreteg/create-keyframe-animation
  3. 安装: 
    npm install create-keyframe-animation --save  

  4. 引入:
    import animations from 'create-keyframe-animation'

    <transition name="normal" @enter="enter" @after-enter="afterEnter" @leave="leave" @after-leave="afterLeave">

  5. methods中封装函数_getPosAndScale获取初始位置及缩放尺寸: (计算以中心点为准)
    _getPosAndScale(){ const targetWidth = 40 //mini-player icon宽度const width = window.innerWidth * 0.8 //cd-wrapper宽度const paddingLeft = 40 const paddingTop = 80const paddingBottom = 30 //mini-player icon中心距底部位置const scale = targetWidth / widthconst x = -(window.innerWidth / 2 - paddingLeft) //X轴方向移动的距离const y = window.innerHeight - paddingTop - width / 2 - paddingBottomreturn {x,y, scale}
    }

  6. 给cd-wrapper添加引用:
    <div class="cd-wrapper" ref="cdWrapper">

  7. 定义事件钩子方法:
    //事件钩子:创建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] = ''
    }

  8. transform属性使用prefix自动添加前缀:
    import {prefixStyle} from '@/common/js/dom'
    const transform = prefixStyle('transform')

五、播放器歌曲播放功能实现--H5 audio
  • 添加H5 <audio>实现歌曲的播放

    <audio :src="currentSong.url" ref="audio"></audio>

  • 在watch中监听currentSong的变化,播放歌曲
    watch: {currentSong() {this.$nextTick(() => { //确保DOM已存在this.$refs.audio.play()})}
    }

  • 给按钮添加点击事件,控制播放暂停
    <i class="icon-play" @click="togglePlaying"></i>

  1. 通过mapGetters获得playing播放状态
  2. 通过mapMutations定义setPlayingState方法修改mutation:
    setPlayingState: 'SET_PLAYING_STATE'

  3. 定义togglePlaying()修改mutation:传递!playing为payload参数
    togglePlaying(){this.setPlayingState(!this.playing)
    }

  4. 在watch中监听playing的变化,执行播放器的播放或暂停:
    playing(newPlaying){const audio = this.$refs.audiothis.$nextTick(() => { //确保DOM已存在newPlaying ? audio.play() : audio.pause()})
    }

  5. 坑:调用audio标签的play()或pause(),都必须是在DOM audio已经存在的情况下,否则就会报错
  6. 解决: 在this.$nextTick(() => { })中调用
  • 图标样式随播放暂停改变:动态绑定class属性playIcon,替换掉原原来的icon-play

    <i :class="playIcon" @click="togglePlaying"></i>

    playIcon() {return this.playing ? 'icon-pause' : 'icon-play'
    }

  • CD 旋转动画效果
  1. 动态绑定class属性cdCls:

    <div class="cd" :class="cdCls">

    cdCls() {return this.playing ? 'play' : 'pause'
    }

  2. CSS样式:
    &.playanimation: rotate 20s linear infinite
    &.pauseanimation-play-state: paused@keyframes rotate0%transform: rotate(0)100%transform:  rotate(360deg)

六、播放器歌曲前进后退功能实现
  • 给按钮添加点击事件

    <i class="icon-prev" @click="prev"></i>
    <i class="icon-next" @click="next"></i>

  • 通过mapGetters获得currentIndex当前歌曲index

  • 通过mapMutations定义setCurrentIndex方法修改mutation
    setCurrentIndex: 'SET_CURRENT_INDEX'

  • 定义prev()和next()修改mutation: 限制index边界
    next() {let index = this.currentIndex + 1if(index === this.playlist.length){index = 0}this.setCurrentIndex(index)
    },
    prev() {let index = this.currentIndex - 1if(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有两个事件:
  1. 当歌曲地址请求到时,会派发canplay事件;
  2. 当没有请求到或请求错误时,会派发error事件
    <audio :src="currentSong.url" ref="audio" @canplay="ready" @error="error"></audio>

    在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的样式
    <div class="icon i-left" :class="disableCls">
    <div class="icon i-center" :class="disableCls">
    <div class="icon i-right" :class="disableCls">

    disableCls() {return this.songReady ? '' : 'disable'
    }

    &.disablecolor: $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 | 0const second = this._pad(interval % 60)return `${minute}:${second}`
    }

  • 坑:秒一开始显示个位只有一位数字,体验不好
  • 解决:定义_pad()用0补位
    _pad(num, n = 2){ //用0补位,补2位字符串长度let len = num.toString().lengthwhile(len < n){num = '0' + numlen++}return num
    }

  • 格式化后的数据填入DOM,显示当前播放时间和总时间:
    <span class="time time-l">{{format(currentTime)}}</span>
    <span class="time time-r">{{format(currentSong.duration)}}</span>

八、播放器progress-bar进度条组件实现
  • base->progress-bar目录下:创建progress-bar.vue

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

  • 实现:
  1. 从父组件接收props参数:进度比percent(player.vue中通过计算属性得到)
  2. 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 - progressBtnWidthconst offsetWidth = newPercent * barWidththis.$refs.progress.style.width = `${offsetWidth}px` //进度条偏移this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` //小球偏移
         }}
    }

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

  • 实现:
  1. 监听touchstart、touchmove、touchend事件,阻止浏览器默认行为;

    <div class="progress-btn-wrapper" ref="progressBtn"@touchstart.prevent="progressTouchStart"@touchmove.prevent="progressTouchMove"@touchend="progressTouchEnd">

  2. created()中创建touch空对象,用于挂载共享数据;
    created(){this.touch = {}
    }

  3. 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 - progressBtnWidthconst 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 = falsethis._triggerPercent()
    },
    _triggerPercent(){const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidthconst percent = this.$refs.progress.clientWidth / barWidththis.$emit('percentChange', percent)
    },
    _offset(offsetWidth){this.$refs.progress.style.width = `${offsetWidth}px` //进度条偏移this.$refs.progressBtn.style[transform] = `translate3d(${offsetWidth}px, 0, 0)` //小球偏移
    }

  4. watch中添加条件设置拖动时,进度条不随歌曲当前进度而变化:
    watch: {percent(newPercent) {if(newPercent >= 0 && !this.touch.initiated){const barWidth = this.$refs.progressBar.clientWidth - progressBtnWidthconst offsetWidth = newPercent * barWidththis._offset(offsetWidth)}}
    }

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

    onProgressBarChange(percent) {this.$refs.audio.currentTime = this.currentSong.duration * percentif(!this.playing){this.togglePlaying()}
    }

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

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

    <div class="progress-bar" ref="progressBar" @click="progressClick">

    progressClick(e) {const rect = this.$refs.progressBar.getBoundingClientRect()const offsetWidth = e.pageX - rect.leftthis._offset(offsetWidth)this._triggerPercent()
    }

九、播放器progress-circle圆形进度条实现 -- SVG
  • base->progress-circle目录下:创建progress-circle.vue
  1. 使用SVG实现圆:

    <div class="progress-circle"><!-- viewBox 视口位置 与半径、宽高相关 stroke-dasharray 描边虚线 周长2πr stroke-dashoffset 描边偏移 未描边部分--><svg :width="radius" :height="radius" viewBox="0 0 100 100" version="1.1"xmlns="http://www.w3.org/2000/svg"><circle class="progress-backgroud" r="50" cx="50" cy="50" fill="transparent"/><circle class="progress-bar" r="50" cx="50" cy="50" fill="transparent" :stroke-dasharray="dashArray" :stroke-dashoffset="dashOffset"/></svg><slot></slot>
    </div>  

  2. 需要从父组件接收props参数:视口半径、当前歌曲进度百分比
    props: {radius: {type: Number,default: 100},percent: {type: Number,default: 0}
    }

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

    <progress-circle :radius="radius" :percent="percent"><!-- radius: 32 --><i :class="miniIcon" @click.stop.prevent="togglePlaying" class="icon-mini"></i>
    </progress-circle>

  • progress-circle.vue中维护数据dashArray,并使用computed计算出当前进度对应的偏移量:
    data() {return {dashArray: Math.PI * 100 //圆周长 描边总长
        }
    },
    computed: {dashOffset() { return (1 - this.percent) * this.dashArray //描边偏移量
       }
    }


注:项目来自慕课网

转载于:https://www.cnblogs.com/ljq66/p/10167951.html

【音乐App】—— Vue-music 项目学习笔记:播放器内置组件开发(一)相关推荐

  1. XSL学习笔记6 XSLT内置模板规则

    XSL学习笔记6 XSLT内置模板规则 定义正确的模板规则来匹配XML树中的节点是XSLT应用的关键.为了让源文档树的节点在没有明确匹配规则的情况下,能够被递归处理,XSLT定义了几个内置的模板规则, ...

  2. H5播放器内置播放视频(兼容绝大多数安卓和ios)

    关于H5播放器内置播放视频,这个问题一直困扰我很长一段时间,qq以前提供白名单已经关闭,后来提供了同层属性的控制,或多或少也有点差强人意. 后来一次偶然发现一个非常简单的方法可以实现. 只需要给vid ...

  3. vue高仿饿了么项目学习笔记之二:头部header组件的实现

    1. Vue-resource应用 在父组件APP.vue的钩子函数created使用vue-resource来ajax请求/api/seller,将数据赋值给vue实例的seller对象,并传值给子 ...

  4. vue综合项目--悦听播放器

    1.实现要求 2.相关接口 1.歌曲搜索接口请求地址:https://autumnfish.cn/search请求方法:get请求参数:keywords(查询关键字)响应内容:歌曲搜索结果 2.歌曲u ...

  5. Python学习笔记3.2-python内置函数大全

    学习python不可避免的首先要了解python的内置函数,熟悉了这些以后可以给编程带来很大的方便. 1.数学运算类 函数名 函数功能 备注 abs(x) 求绝对值 1.参数可以是整型,也可以是复数2 ...

  6. MySQL学习笔记_7_MySQL常用内置函数

    MySQL常用内置函数 说明: 1)可以用在SELECT/UPDATE/DELETE中,及where,orderby,having中 2)在函数里将字段名作为参数,变量的值就是字段所对应的每一行的值. ...

  7. [云炬python学习笔记]Numpy中内置函数min(),max(),sum()与Python中内置函数min(),max(),sum()性能对比分析

    众所周知,Python有许多内置函数(例如min(),max(),sum()),Numpy也有自己的内置函数(np.min(),np.max(),np.sum()).由于Numpy的函数是在编译码中执 ...

  8. Qt学习笔记之--Qt内置图标一览表

    Qt内置了一批图标,一共有70个(Qt 5.9.5),不同的平台图标有差异.由于官方文档中只有描述没有图示,所以写个Demo把这些图标全部显示出来.下面展示了windows.Linux.android ...

  9. ET游戏框架整理笔记3: 常用内置组件功能

    上一节说道 挂载组件的几个步骤 1. 组件工厂创建组件 2. 如果有泛型类以组件为参数 并且实现了IUpdateSystem接口的话会调用它的update方法 如下面这个类 [ObjectSystem ...

最新文章

  1. mvc的宿舍管理系统源码 基于jsp_[源码和文档分享]基于JSP的MVC框架实现的图书推荐系统展示平台网站...
  2. 机器学习-----有监督,无监督,半监督学习的简单阐释
  3. 本地Yum生成数据库及常用命令
  4. Java 中的关键字 final
  5. BZOJ 1003: [ZJOI2006]物流运输trans
  6. LTE之MIMO学习1-TB/codeword/layer/precoding/port
  7. hadoop+hive+flink+hbase交互的版本兼容性
  8. [html] 请实现一个网站加载进度条
  9. 基于Echarts+HTML5可视化数据大屏展示—企业生产销售作战指挥室数据平台
  10. linux-文件的删除与创建
  11. 【狂神JAVA】MyBatis笔记
  12. html file对象修改,HTML DOM
  13. arp协议属于哪一层_TCP/IP协议栈-之-ARP协议分析
  14. Oracle程序开发小技巧(一)
  15. 40页PPT勾画“互联网颠覆性思维”----诠释互联网思维
  16. 计算机套题库c版,计算机二级C语言上机题库100套(最新版)
  17. Spring中的@DependsOn注解
  18. 您使用的是不受支持的命令行标记 -no-sandbox
  19. papers-06-07
  20. 命令行重启Mysql

热门文章

  1. Windows编程-- 用户方式中线程的同步---关键代码段(临界区)
  2. 为ML带来拓扑学基础,Nature子刊提出拓扑数据分析方法
  3. Python3编程语言之zip() 函数使用示例
  4. 霍夫变换(hough transform)原理
  5. LimeSDR性能参数介绍及如何用它实现通信过程
  6. MFC EDIT控件的使用记录
  7. 3.2计算机系统教案,计算机系统教案.ppt
  8. java enumerable_java - Java相当于C#的'Enumerable.Any' - 堆栈内存溢出
  9. 使用 TreeSet 生成数组
  10. sql用什么替代or