前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。
![]() |
![]() |
当前歌曲播放列表 | 添加歌曲到队列 |
components->playlist目录下:创建playlist.vue -- 在play.vue中应用
一、.歌曲列表组件显示和隐藏的控制 |
- data中维护一个数据
showFlag: false
- 使用v-show判断showFlag控制显示隐藏
<div class="playlist" v-show="showFlag">
- methods中分别定义show()和hide(),设置showFlag为true和false
show() { this.showFlag = true }, hide() { this.showFlag = false }
- player.vue中:给列表按钮添加点击事件,并阻止事件冒泡,showPlaylist方法执行playlist中的show()控制列表的显示
<div class="control" @click.stop.prevent="showPlaylist"> <play-list ref="playlist">
showPlaylist() { this.$refs.playlist.show() }
- playlist.vue中:给列表的蒙层和关闭按钮都添加点击事件,触发hide方法,控制列表的隐藏
- 坑:点击列表本身也会关闭列表
- 原因:给列表蒙层添加点击事件时,实际上添加到了最外层<div class="playlist">上,点击子元素时会事件冒泡到最外层
- 解决:给list-wrapper添加@click.stop,阻止点击事件冒泡
<div class="list-wrapper" @click.stop>
二、歌曲列表组件播放列表的实现 |
- 通过mapGetters获取顺序播放的歌曲列表添加到列表项
import {mapGetters} from 'vuex' computed: { ...mapGetters([ 'sequenceList' ]) }
<li class="item" v-for="(item, index) in sequenceList" :key="index"> <span class="text">{{item.name}}</span>
- 应用Scroll组件实现歌曲列表滚动
<scroll class="list-content" :data="sequenceList" ref="listContent">
- 在show()时让scroll重新进行计算,确保当前高度是正确的
show() { this.showFlag = true setTimeout(() => { this.$refs.listContent.refresh() }, 20) }
- 给当前播放的歌曲添加current高亮显示的样式
<i class="current" :class="getCurrentIcon(item)"></i>
通过mapGetters获取当前歌曲: 'currentSong'
getCurrentIcon(item) { if(this.currentSong.id === item.id) { return 'icon-play' } return '' }
- 选择列表项播放歌曲
<li class="item" @click="selectItem(item, index)">
通过mapGetters获得currentSong和playlist,通过mapMutations调用setCurrentIndex提交数据
import {playMode} from '@/common/js/config' computed: { ...mapGetters([ 'sequenceList', 'currentSong', 'playlist', 'mode' ]) } selectItem(item, index){ //如果当前是随机播放,重新计算index if(this.mode === playMode.random) { inde = this.playlist.findIndex((song) => { return song.id === item.id }) } this.setCurrentIndex(index) this.setPlayingState(true) } ...mapMutations({ setCurrentIndex: 'SET_CURRENT_INDEX', setPlayingState: 'SET_PLAYING_STATE' })
- 歌曲列表滚动到当前播放的歌曲
- 需求:切换歌曲时,歌曲列表滚动到当前播放的歌曲位置;打开歌曲列表时,当前播放的歌曲始终在第一行显示
- 封装一个滚动到当前播放歌曲的方法
scrollToCurrent(current) { //找到当前歌曲在顺序列表中的索引 const index = this.sequenceList.findIndex((song) => { return current.id === song.id }) this.$refs.listContent.scrollToElement(this.$refs.listItem[index], 300) }
- 切换歌曲成功时调用
watch: { currentSong(newSong, oldSong) { if(!this.showFlag || newSong.id === oldSong.id) { return } this.scrollToCurrent(newSong) } }
- 歌曲列表显示时调用
show() { this.showFlag = true setTimeout(() => { this.$refs.listContent.refresh() this.scrollToCurrent(this.currentSong) }, 20) }
- 从歌曲播放列表中删掉所选歌曲
<span class="delete" @click.stop="deleteOne(item)">
- actions.js中:封装deleteSong()
export const deleteSong = function ({commit, state}, song){ let playlist = state.playlist.slice() //副本 let sequenceList = state.sequenceList.slice() //副本 let currentIndex = state.currentIndex let pIndex = findIndex(playlist, song) playlist.splice(pIndex, 1) let sIndex = findIndex(sequenceList, song) sequenceList.splice(sIndex, 1) if(currentIndex > pIndex || currentIndex === playlist.length){ currentIndex-- } commit(types.SET_PLAYLIST, playlist) commit(types.SET_SEQUENCE_LIST, sequenceList) commit(types.SET_CURRENT_INDEX, currentIndex) const playingState = playlist.length > 0 commit(types.SET_PLAYING_STATE,playingState) }
- 通过mapActions获取deleteSong方法
...mapActions([ 'deleteSong' ])
- methods中定义deleteOne(),调用deleteSong()
deleteOne(item) { this.deleteSong(item) if(!this.playlist.length){ this.hide() } }
- 坑:报错this.currentSong.getLyric is not a function
- 原因: action deleteSong修改了playlist和currentIndex,导致currentSong发生变化;在player.vue中的watch会监测到currentSong变化了,但其实这里列表中已经没有歌曲了;newSong为空的Object,此时将newSong和oldSong进行对比,都会返回undefined,所以报错
- 解决:在watch currentSong()中判断如果没有newSong.id,直接返回,不执行任何操作
if(!newSong.id) { return }
- 优化:给删除歌曲添加动画
- 将<ul>替换为
<transition-group name="list" tag="ul">
- transition-group的关键: 内容<li class="item">必须要有 :key="index"
.item height: 40px &.list-enter-active, &.list-leave-active transition: all 0.1s &.list-enter, &.list-leave-to height: 0
- 清除歌曲播放列表
- 同搜索历史列表,在点击清除按钮后,需要先显示一个confirm弹窗,然后选择确认或取消
- 给清空按钮添加点击事件,显示弹窗:
<span class="clear" @click="showConfirm">
showConfirm() { this.$refs.confirm.show() }
- 给弹窗监听confirm事件,同时封装action,在confirm()中调用
<confirm ref="confirm" text="是否清空播放列表" confirmBtnText="清空" @confirm="confirmClear"></confirm>
confirmClear() { this.deleteSongList() this.hide() } ...mapActions([ 'deleteSong', 'deleteSongList' ])
actions.js中:
export const deleteSongList = function ({commit}){ //将所有值都重置为初始状态 commit(types.SET_PLAYLIST, []) commit(types.SET_SEQUENCE_LIST, []) commit(types.SET_CURRENT_INDEX, -1) commit(types.SET_PLAYING_STATE, false) }
- 坑:点击取消时,歌曲播放列表也会被关闭
- 原因:在playlist.vue中<confirm>在<div class="playlist" @click="hide">内,点击confirm会事件冒泡,触发hide
- 解决:让confirm组件更加独立,阻止事件冒泡
<div class="confirm" v-show="showFlag" @click.stop>
三、playerMixin的抽象 |
- 需求:歌曲播放列表中的播放模式切换功能与播放器中的逻辑相同,可以使用Mixin复用
- mixin.js中:创建新的playerMixin对象
import {mapGetters, mapMutations} from 'vuex' import {playMode} from '@/common/js/config' import {shuffle} from '@/common/js/util' export const playerMixin = { computed: { iconMode(){ return this.mode === playMode.sequence ? 'icon-sequence' : this.mode === playMode.loop ? 'icon-loop' : 'icon-random' }, ...mapGetters([ 'sequenceList', 'currentSong', 'playlist', 'mode' ]) }, methods: { changeMode(){ const mode = (this.mode + 1) % 3 this.setPlayMode(mode) let list = null if(mode === playMode.random){ list = shuffle(this.sequenceList) }else{ list = this.sequenceList } this.resetCurrentIndex(list) this.setPlayList(list) }, resetCurrentIndex(list){ let index = list.findIndex((item) => { //es6语法 return item.id === this.currentSong.id }) this.setCurrentIndex(index) }, ...mapMutations({ setPlayingState: 'SET_PLAYING_STATE', setCurrentIndex: 'SET_CURRENT_INDEX', setPlayMode: 'SET_PLAY_MODE', setPlayList: 'SET_PLAYLIST' }) } }
- player和playlist中:应用playerMixin,同时删掉共用部分
import {playerMixin} from '@/common/js/mixin'
mixins:[playerMixin],
- playlist.vue中:动态绑定class,监听点击事件
<i class="icon" :class="iconMode" @click="changeMode">
computed: { modeText() { return this.mode === playMode.sequence ? '顺序播放' : this.mode === playMode.random ? '随机播放' : '单曲循环' } }
四、添加歌曲到列表add-song组件实现 |
- components->add-song目录下:创建add-song.vue
- 页面的显示隐藏:同playlist.vue
- 搜索框和搜索结果:与search.vue共有一些相同的逻辑,使用mixin复用
- mixin.js中创建新的searchMixin对象
export const searchMixin = { computed: { ...mapGetters([ 'searchHistory' ]) }, data() { return { query: '' } }, methods: { blurInput() { this.$refs.searchBox.blur() }, saveSearch() { this.saveSearchHistory(this.query) }, onQueryChange(query){ this.query = query }, addQuery(query) { this.$refs.searchBox.setQuery(query) }, ...mapActions([ 'saveSearchHistory', 'deleteSearchHistory' ]) } }
- search和add-song中:应用searchMixin,同时删掉共用部分
import {searchMixin} from '@/common/js/mixin' mixins:[searchMixin],
- add-song.vue中:绑定数据,监听事件
<search-box ref="searchBox" @query="onQueryChange" placeholder="搜索歌曲"> <suggest :query="query" :showSinger="showSinger" @select="selectSuggest" @listScroll="blurInput">
- 切换Tab组件
- base->switches目录下:创建switches.vue
<li class="switch-item" v-for="(item, index) in switches" :key="index" :class="{'active': currentIndex === index}" @click="switchItem(index)"> <span>{{item.name}}</span> </li>
props: { switches: { type: Array, default: [] }, currentIndex: { type: Number, default: 0 } }, methods: { switchItem(index) { this.$emit('switch', index) } }
- add-song.vue中应用:
<switches :switches="switches" :currentIndex="currentIndex" @switch="switchItem"></switches>
currentIndex: 0, switches: [ {name: '最近播放'}, {name: '搜索历史'} ] switchItem(index) { this.currentIndex = index }
- 最近播放列表
- Vuex管理数据
①states.js中:添加数据
playHistory: []
②mutations-types.js中:定义事件类型常量
export const SET_PLAY_HISTORY = 'SET_PLAY_HISTORY'
③mutations.js中:创建方法
[types.SET_PLAY_HISTORY](state, history){ state.playHistory = history }
④getters.js中:定义数据映射
export const playHistory = state => state.playHistory
- player.vue中:当歌曲ready后通过mapActions向Vuex中写入数据
ready() { this.songReady = true this.savePlayHistory(this.currentSong) } ...mapActions([ 'savePlayHistory' ])
- catch.js中:实现对本地缓存的操作
//将歌曲数据保存到本地缓存 export function savePlay(song) { let songs = storage.get(PLAY_KEY, []) insertArray(songs, song, (item) => { return item.id === song.id }, PLAY_MAX_LENGTH) storage.set(PLAY_KEY, songs) return songs } //从本地缓存中取出歌曲数据 export function loadPlay() { return storage.get(PLAY_KEY, []) }
- state.js中:修复playHistory初始值为当前本地缓存中的数据
playHistory: loadPlay()
- actions.js中:引入savePlay方法将数据同时存入Vuex和本地缓存
export const savePlayHistory = function({commit}, song){ commit(types.SET_PLAY_HISTORY, savePlay(song)) }
- add-song.js中:通过mapGetters获取Vuex中的播放历史
<div class="list-wrapper"> <scroll class="list-scroll" v-if="currentIndex===0" :data="playHistory"> <div class="list-inner"> <song-list :songs="playHistory"></song-list> </div> </scroll> </div>
import Scroll from '@/base/scroll/scroll' import {mapGetters} from 'vuex' import SongList from '@/base/song-list/song-list' computed: { ...mapGetters([ 'playHistory' ]) }
- add-song.js中:监听select事件,通过mapActionis从播放历史中选择歌曲插入播放列表
@select="selectSong"
import Song from '@/common/js/song' selectSong(song, index) { if(index !== 0) { //从playHistory中获取到的song还是一个对象,需要实例化为Song类 this.insertSong(new Song(song)) } }, ...mapActions([ 'insertSong' ])
- 搜索历史列表:复用SearchList组件以及searchMixin中的数据和方法
<scroll ref="searchList" class="list-scroll" v-if="currentIndex===1" :data="searchHistory"> <div class="list-inner"> <search-list @delete="deleteSearchHistory" @select="addQuery" :searches="searchHistory"></search-list> </div> </scroll>
- 优化:在search-list.vue中通过transition-group给删除列表时添加动画
- 关键:<transition-group>中的元素一定要有key值区分元素间的不同
- 优化:在添加歌曲到列表页面显示时,判断当前显示列表,对应scroll重新计算,确保高度正确
show() { this.showFlag = true setTimeout(() => { if(this.currentIndex === 0){ this.$refs.songList.refresh() }else{ this.$refs.searchList.refresh() } }) }
- 顶部提示框
- base->top-list目录下:创建top-list.vue
<transition name="drop"> <div class="top-tip" v-show="showFlag" @click.stop="hide"> <slot></slot> </div> </transition>
通过showFlag控制显示隐藏,同play-list.vue
- add-song.vue中:应用top-list,添加slot
<top-tip ref="topTip"> <div class="tip-title"> <i class="icon-ok"></i> <span class="text">1首歌曲已经添加到播放队列</span> </div> </top-tip>
分别在selectSuggest()和selectSong()中调用showTip()显示提示框
- top-list.vue中:在show()内调用定时器,设置提示框显示2s自动关闭
- 坑:如果快速的调用show(),会有很多定时器存在
- 解决:每次show()时,在调用新的timer前就先清空前面的timer
props: { delay: { type: Number, default: 2000 } } show() { this.showFlag = true clearTimeout(this.timer) this.timer = setTimeout(() => { this.hide() }, this.delay) }
五、歌曲列表组件scroll组件能力的扩展 |
- 坑:添加歌曲到列表后,歌曲播放列表的滚动位置不对了
- 原因:playlist中歌曲列表项的添加了transition-group动画,即添加歌曲后歌曲播放列表的高度需要一个100ms的过程;而外层<scroll :data="sequenceList">在watch到数据的变化后,20ms就重新计算了,计算的高度是不对的
- 解决:扩展scroll组件,添加props属性refreshDelay,让外部组件可以自定义重新计算的延迟时间;扩展后再在外部组件的data中定义refreshDelay:100,最后在<scroll>中传入:refreshDelay="refreshDelay"
refreshDelay: { type: Number, default: 20 } watch: { data() { //监测data的变化 setTimeout(() => { this.refresh() }, this.refreshDelay) } }
- 同上:search-list组件中的列表项也添加了transition-group动画;在引用到的search组件和add-song组件中都需要传入:refreshDelay="refreshDelay";因为两个组件复用了searchMixin,所以在searchMixin中定义refreshDelay:100
注:项目来自慕课网