前言:以下内容均为学习慕课网高级实战课程的实践爬坑笔记。
项目github地址:https://github.com/66Web/ljq_vue_music,欢迎Star。
搜索歌手歌曲 搜索历史保存
一、搜索页面布局
search-Box组件开发
export default {
props: {
placeholder: {
type: String,
default: ‘搜索歌曲、歌手’
}
},
data() {
return {
query: ‘’
}
},
methods: {
clear() {
this.query = ‘’
},
setQuery(query) {
this.query = query
}
},
created() {
this.KaTeX parse error: Expected '}', got 'EOF' at end of input: … this.emit(‘query’, newQuery)
})
}
}
search数据获取及应用
数据获取就是普通的jsonp请求抓取,同前面的页面一样
data中维护一个数据
hotKey: []
抓取到数据后取前10个给hotKey赋值
this.hotKey = res.data.hotkey.slice(0, 10)
热搜功能开发
拿到数据后开始渲染热搜页面结构
热门搜索
QQ音乐搜索数据的获取也禁止跨域,需要通过后端配置Webpack强制修改请求头,返回json数据
search组件中监听search-box的query的变化
<search-box ref=“searchBox” @query=“onQueryChange”>
data中维护一个数据
query: ‘’
如果query发生改变,就赋值给query
onQueryChange(query){
this.query = query
}
这时就把query传给suggest组件
suggest组件依赖获取到的query,发送请求检索对应内容
定义数据接口时,有四个值是变化的,所以需要传入四个参数:
const perpage = 20 //抓取数据一页有多少数据
search() {
search(this.query, this.page, this.showSinger, perpage).then((res) => {
//console.log(res)
if(res.code === ERR_OK) {
this.result = this._genResult(res.data)
//console.log(this.result)
}
})
}
直接拿到的数据是不理想的,需要进行处理
处理后的数据是一个数组,其中包含两个对象:一个歌手、一个歌曲列表
_genResult(data) {
let ret = []
if(data.zhida && data.zhida.singerid) {
//使用es6对象扩展运算符…把两个对象添加到一个对象上
ret.push({…data.zhida, …{type: TYPE_SINGER}})
}
if(data.song){
ret = ret.concat(data.song.list)//合并时出现难题
}
return ret
}
监听接收到的query的变化,如果变化,调用search()检索:
watch: {
query() {
this.search()
}
}
拿到数据后在DOM中使用
_nomalizeSongs(list){
let ret = []
let pushIndex =0 //判断是否是最最后一次push
list.forEach((musicData)=>{
if(musicData.songid && musicData.albummid){
//获取歌曲源url数据
let songUrl = ‘’
getSongs(musicData.songmid).then((res)=>{
if(res.code === ERR_OK){
songUrl = res.req_0.data.midurlinfo[0].purl
ret.push(createSong(musicData,songUrl))
/把歌曲源数据push后判断是否异步完成**/
pushIndex++
this.pushOver = list.length===pushIndex
}
})
}
})
watch:{
//监听异步问题,对数据无法操作,把值赋值出来
searchSongs(newValue){
console.log(this.pushOver)
//判断异步完成后去合并已存在的数组和singer
if(this.pushOver){
this._genResult(this.zhida,newValue)
}
}
},
//有zhida就合并对象到数组中
_genResult(data,newValue){
let ret = []
//push歌手进空数组
if(data.singerid){
ret.push({…this.zhida,…{type:TYPE_SINGER}}) //es6语法,对象拓展符。等同于object.assign()新建对象
}
//合并歌曲进数组
if (newValue) {
ret = ret.concat(value)
}
this.result = ret
},
最后可以取到一个result是21项的数组(20歌曲、1歌手)
参考链接
搜索结果上拉加载
引入scroll组件,替换掉根元素div
扩展scroll.vue,实现上拉刷新
添加props参数,在父组件调用时传入控制是否执行上拉刷新
pullup: {
type: Boolean,
default: true
}
如果有上拉刷新的选项,在初始化scroll时执行以下操作
if(this.pullup) {
this.scroll.on(‘scrollEnd’, () => {
// 当滚动距离小于等于最大的滚动条的距离 + 50 的时候,向外传递一个scrollToEnd的事件
if(this.scroll.y <= (this.scroll.maxScrollY + 50)) {
this.KaTeX parse error: Expected 'EOF', got '}' at position 29: …oEnd') }̲ }) } sugges…refs.suggest.scrollTo(0, 0) //scroll位置重置到顶部
this.hasMore = true
getSearch(this.query, this.page, this.showSinger, perpage).then((res) => {
// console.log(res.data)
if(res.code === ERR_OK) {
this.zhida = res.data.zhida
this.firstList = res.data.song.list //记录第一次加载后获得的歌曲
this.searchSongs = this._normalizeSongs(res.data.song.list)
// this.result = this._genResult(this.zhida, this.searchSongs)
this._checkMore(res.data.song)
}
})
}
引用loading组件,通过标志为hasMore控制显示
使用二级路由实现点击搜索结果项跳转
search.vue中添加路由容器
router->index.js中为Search路由添加二级路由
{
path: ‘/search’,
component: Search,
children: [
{
path: ‘:id’,
component: SingerDetail
}
]
}
suggest.vue中给列表项添加点击事件
@click=“selectItem(item)”
selectItem(item) {
if(item.type === TYPE_SINGER) {
const singer = new Singer({
id: item.singermid,
name: item.singername
})
this.KaTeX parse error: Expected '}', got 'EOF' at end of input: …path: `/search/{singer.id}`
})
this.setSinger(singer)
}else{
this.insertSong(item)
}
}
…mapMutations({
setSinger: ‘SET_SINGER’
}),
…mapActions([
‘insertSong’
])
点击歌手: 调用Singer方法实例化当前歌手的singer对象,通过mapMutations提交singer,同时,跳转到二级路由
点击歌曲: 与从歌手详情页和歌单详情页列表选歌跳转不同,这些都是在跳转的同时将当前歌曲列表全部添加到了播放列表;而搜索结果中点击歌曲,要执行的是添加当前歌曲到播放列表中,同时判断如果播放列表存在所选歌曲需将其删掉
actions.js中封装insertSong():
export const insertSong = function ({commit, state}, song){
let playlist = state.playlist.slice() //副本
let sequenceList = state.sequenceList.slice() //副本
let currentIndex = state.currentIndex
//记录当前歌曲
let currentSong = playlist[currentIndex]
//查找当前列表中是否有待插入的歌曲并返回其索引
letfpIndex = findIndex(playlist, song)
//因为是插入歌曲,所以索引+1
currentIndex++
//插入这首歌到当前索引位置
playlist.splice(currentIndex, 0, song)
//如果已经包含了这首歌
if(fpIndex > -1) {
//如果当前插入的序号大于列表中的序号
if(currentIndex > fpIndex) {
playlist.splice(fpIndex, 1)
currentIndex–
}else{
playlist.splice(fpIndex+1, 1)
}
}
let currentSIndex = findIndex(sequenceList, currentSong) + 1
let fsIndex = findIndex(sequenceList, song)
sequenceList.splice(currentSIndex, 0, song)
if(fsIndex > -1){
if(currentSIndex > fsIndex){
sequenceList.splice(fsIndex, 1)
}else{
sequenceList.splice(fsIndex + 1, 1)
}
}
commit(types.SET_PLAYLIST, playlist)
commit(types.SET_SEQUENCE_LIST, sequenceList)
commit(types.SET_CURRENT_INDEX, currentIndex)
commit(types.SET_FULL_SCREEN, true)
commit(types.SET_PLAYING_STATE, true)
坑: 报错[vuex] Do not mutate vuex store state outside mutation handlers.
原因: Vuex 规定state中的值必须在回调函中更改
解决: 将state中要修改的数据复制一个副本.slice()进行修改,再提交
suggest组件边界情况的处理
没有检索到结果时的情况
src->base目录下:创建no-result.vue
suggest.vue中应用:当hasMore为false且result无内容时显示
对input数据进行截流,避免输入过程中数据的每次改变都发送请求
common->js->util.js中:定义截流函数
//截流
//对一个函数做截流,就会返回新的函数,新函数是在延迟执行原函数
//如果很快的多次调用新函数,timer会被清空,不能多次调用原函数,实现截流
export function debounce(func, delay){
let timer
return function (...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}
search-box.vue中:引用并对KaTeX parse error: Expected '}', got 'EOF' at end of input: …d() { this.watch(‘query’, debounce((newQuery) => {
this.KaTeX parse error: Expected 'EOF', got '}' at position 29: … newQuery) }̲, 200)) } 移动端滚动…emit(‘beforeScroll’)
})
}
suggest.vue中:data中维护一个数据
beforeScroll: true
中传入并监听事件:
:beforeScroll=“beforeScroll” @beforeScroll=“listScroll”
listScroll()中同样也派发一个listScroll事件:
this.$emit(‘listScroll’)
search-box.vue中:给添加引用
ref=“query”
定义blur()供外部调用:
this.$refs.query.blur()
search.vue中:
<suggest :query=“query” @listScroll=“blurInput”>
blurInput() {
this.$refs.searchBox.blur()
}
三、搜索历史记录保存功能实现
关键:新建vuex的state数组变量,点击时存入和调用时读取数据即可
利用本地缓存插件,取、操作后再存
Vuex配置
state.js中:添加数据
searchHistory: []
mutation-types.js中:定义事件类型常量
export const SET_SEARCH_HISTORY = ‘SET_SEARCH_HISTORY’
mutiaions.js中:创建方法
[types.SET_SEARCH_HISTORY](state, history){
state.searchHistory = history
}
getters.js中:添加数据映射
export const searchHistory = state => state.searchHistory
suggest.vue中:在点击搜索结果项的同时,向外派发一个select事件 – 用于外部监听进行存取搜索结果
search.vue中:给监听select事件,保存搜索结果
@select=“saveSearch”
需求: 搜索结果不仅要显示在“搜索历史”组件中,还需要保存在本地浏览器的localStorage缓存
实现: 多个数据操作,封装action
第三方开源Storage库:https://github.com/ustbhuangyi/storage
安装:
npm install good-storage --save
common->js目录下:创建catch.js – 定义storage操作相关的方法
catch.js中
引入本地存储插件
import storage from ‘good-storage’
设置数组添加项到第一个,并且限定个数,把旧的删除,重复的删除
const SEARCH_KEY = ‘search’ //双下划线标识内部key, 避免与外部key冲突
const SEARCH_MAX_LENGTH = 15 //搜索历史最多存入数组15个
//操作搜索历史数组的方法
//参数:搜索记录数组,添加的项,筛选方法,最大数量
function insertArray(arr, val, compare, maxLen){
const index = arr.findIndex(compare) //判断是否以前有搜索过,compare在外部编写
if (index === 0) { //上一条搜索历史就是这个,就不需要添加历史
return
}
if (index > 0) { //历史记录中有这条,把历史记录删了,重新添加
arr.splice(index, 1)
}
arr.unshift(val) //没有历史记录,添加项目到第一项
if (maxLen && arr.length > maxLen) { //大于最大数量的时候,删除最后一项
arr.pop()
}
}
使用插件的方法get和set,获取到本地缓存的数据,操作新内容后,存入本地缓存
//插入最新搜索历史到本地缓存,同时返回新的搜索历史数组
export function saveSearch(query) {
let searches = storage.get(SEARCH_KEY, []) //如果已有历史就get缓存中的数组,没有就空数组
insertArray(searches, query, (item) => { //对传入的项与已有数组进行操作
return item === query
}, SEARCH_MAX_LENGTH)
storage.set(SEARCH_KEY, searches) //把操作过后的数组set进缓存,直接替换掉原历史
return searches
}
actions.js中:commit调用js方法,存入本地缓存
import {saveSearch} from ‘@/common/js/catch’
export const saveSearchHistory = function({commit}, query){
commit(types.SET_SEARCH_HISTORY, saveSearch(query))
}
search.vue中:通过mapActions实现数据映射
…mapActions([
‘saveSearchHistory’
])
saveSearch() {
this.saveSearchHistory(this.query)
}
坑:重启服务器之后本地缓存还是没了,是因为在state的初始值中空数组
解决:把空数组改成获取本地缓存,在catch.js中设置一个新的方法,来获取
catch.js中:
//states获取本地缓存中的数据
export function loadSearch() {
return storage.get(SEARCH_KEY, [])
}
state.js中:
import {loadSearch} from ‘@/common/js/catch’
searchHistory: loadSearch()
注:可以在控制台输入localStorage,查看浏览器本地缓存
四、搜索页面search-list组件功能实现
search.vue中:
通过mapGetters获取state中的searchHistory
computed: {
…mapGetters([
searchHistory’
])
}
引用scroll替换shortcut的div:实现滚动
shortcut() {
return this.hotKey.concat(this.searchHistory)
}
注意:里面包含多个同级元素,要在外层再嵌套一个
watch中监听query(),当query值发生变化后,searchHistory的值一定会变化,此时需要强制scroll重新计算
watch: {
query(newQuery) {
if (!newQuery) {
setTimeout(() => {
this.$refs.shortcut.refresh()
}, 20)
}
}
}
添加search-history的DOM结构:设置只有当searchHistory有内容时显示
搜索历史
base->search-list目录下:创建search-list.vue 引入search.vue
应用mixin实现播放器底部自适应
首先添加引用:
ref=“shortcutWrapper”
ref=“searchResult”
ref=“suggest”
import {playListMixin} from ‘@/common/js/mixin’
mixins: [playListMixin],
suggest.vue中:暴露一个refresh(),代理scroll组件的refresh()
this.KaTeX parse error: Expected '}', got 'EOF' at end of input: … : '' this.refs.shortcutWrapper.style.bottom = bottom
this.
r
e
f
s
.
s
h
o
r
t
c
u
t
.
r
e
f
r
e
s
h
(
)
t
h
i
s
.
refs.shortcut.refresh() this.
refs.shortcut.refresh()this.refs.searchResult.style.bottom = bottom
this.KaTeX parse error: Expected 'EOF', got '}' at position 24: …gest.refresh() }̲ search-list.v…emit(‘select’, item)
}
给删除按钮添加点击事件,向外派发事件
@click.stop.prevent=“deleteOne(item)”
deleteOne(item) {
this.$emit(‘delete’, item)
}
search.vue中:
监听select事件,添加query同时进行检索
@select=“addQuery”
监听delete事件,从vuex数据和本地缓存中删掉query
@delete=“deleteOne”
catch.js中:创建函数将query从localStorage中删除,同时返回操作后的新数组
function deleteFromArray(arr, compare){
const index = arr.findIndex(compare)
if(index > -1) {
arr.splice(index, 1)
}
}
export function deleteSearch() {
let searches = storage.get(SEARCH_KEY, [])
deleteFromArray(searches, (item) => {
return item === query
})
storage.set(SEARCH_KEY, searches)
return searches
}
actions.js中:封装action,将已从localStorage删除指定query后的新数组存入searchHistory
export const deleteSearchHistory = function({commit}, query){
commit(types.SET_SEARCH_HISTORY, deleteSearch(query))
}
通过mapActions调用action:
‘deleteSearchHistory’
deleteOne(item) {
this.deleteSearchHistory(item)
}
清空按钮添加点击事件,清除数据的方法同上:
catch.js中:
export function clearSearch() {
storage.remove(SEARCH_KEY)
return []
}
actions.js中:
export const clearSearchHistory = function ({commit}){
commit(types.SET_SEARCH_HISTORY, clearSearch())
}
search.vue中:
@click=“deleteAll”
‘clearSearchHistory’
deleteAll(){
this.clearSearchHistory()
}
注意:deleteOne()和deleteAll()纯粹是action方法的代理,可以直接将action方法用在DOM上,省略methods的定义
优化:在点击清空按钮后,先弹出一个确认弹窗,如果确认清空,执行后面的操作;否则,不请空
base->confirm目录下: 创建confim.vue 在需要的组件处使用
在需要的组件处使用的好处:
①每个confirm是独立的,各自传递需要的事件和参数
②confirm和外部调用的组件时紧密关联的,外部组件更容易调用confirm中的方法;confirm更容易通过外部组件的数据控制显示隐藏
confirm.vue作为基础组件:只负责向外提供确认和取消事件,接收从父组件传来的props参数
<confirm ref=“confirm” text=“是否清空所有搜索历史” confirmBtnText=“清空”
@confirm=“clearSearchHistory”>