VUE移动端音乐APP学习【二十二】:搜索检索
当搜索内容时会出现搜索结果列表,将这个结果列表封装成一个组件。这个组件就可以根据搜索的关键词请求服务端数据来检索不同的结果。
其基本的dom结构代码如下:
<template> <div class="suggest"> <ul class="suggest-list"> <li class="suggest-item"> <div class="icon"> <i></i> </div> <div class="name"> <p class="text"></p> </div> </li> </ul> </div> </template> <script> export default { name: 'name', }; </script> <style scoped lang="scss"> .suggest { height: 100%; overflow: hidden; .suggest-list { padding: 0 30px; .suggest-item { display: flex; align-items: center; padding-bottom: 20px; } .icon { flex: 0 0 30px; width: 30px; [class^="icon-"] { font-size: 14px; color: $color-text-d; } } .name { flex: 1; font-size: $font-size-medium; color: $color-text-d; overflow: hidden; .text { @include no-wrap(); } } } .no-result-wrapper { position: absolute; width: 100%; top: 50%; transform: translateY(-50%); } } </style>
这个组件是依赖检索词的,所以可以接收数据query。还要watch这个query的变化,当query发生变化时就调用接口请求服务端的检索数据。
props: { query: { type: String, default: '', }, }, methods: { search() { // 请求服务端的检索数据 }, }, watch: { query() { this.search(); }, },
设置接口请求服务端的检索数据
//search.js
回到suggest组件调用这个api请求
有了数据后就可以在dom上遍历
<ul class="suggest-list"> <li class="suggest-item" v-for="(item,index) in result" :key="index"> <div class="icon"> <i class="iconfont icon-music"></i> </div> <div class="name"> <p class="text" v-html="getDisplayName(item)"></p> </div> </li> </ul> getDisplayName(item) {
在search组件应用search组件
<div class="search"> <div class="search-box-wrapper"> ... </div> <div class="shortcut-wrapper"> ... </div> <div class="search-result"> <suggest></suggest> </div> </div>
先定义query变量,在suggest上传入query数据。
<suggest :query="query"></suggest> data() { return { hotKey: [], query: '', }; },
因为之前的search-box如果文本框发生变化就会派发事件,所以在search.vue可以监听search-box的query事件,就可以从搜索框里拿到query的变化值,然后再把这个值通过props传递给suggest,suggest组件就会触发watch query的变化就会调用search方法去调用api请求获取检索数据。
<div class="search-box-wrapper"> <search-box ref="searchBox" @query="onQueryChange"></search-box> </div> onQueryChange(query) { this.query = query; },
热门搜索和检索列表有重合,需要用v-show判断其显示
<div class="shortcut-wrapper" v-show="!query"> <div class="search-result" v-show="query">
检索结果列表不能上下滚动,suggest需要引入scroll组件
<scroll class="suggest" :data="result"> ... </scroll> import Scroll from '../../base/scroll/scroll.vue'; components: { Scroll, },
给检索结果列表实现歌曲的点击,点击歌曲跳转到歌曲播放页面。
思路:
- 需要往播放列表添加被点击的歌曲,即playlist和sequenceList添加这首歌曲,currentIndex也要发生变化。如果当前的playlist已经有了这首歌的话,插入这首歌还要把之前同样的歌曲删掉。
- 这个过程需要操作三个state和mutation,所以需要到actions做个封装。
在actions.js定义一个动作,插入歌曲
export const insertSong = function ({ commit, state }, song) { let { playlist } = state; let { sequenceList } = state; let { currentIndex } = state; // 记录当前歌曲 let currentSong = playlist[currentIndex]; // 查找当前列表中是否有待插入的歌曲并返回其索引 let fpIndex = findIndex(playlist, song); // 在当前索引的下一位插入歌曲,因为是插入歌曲,索引要+1 currentIndex++; playlist.splice(currentIndex, 0, song); // 如果已经包含了这首歌 if (fpIndex > -1) { // 如果当前插入的序号大于列表中的序号,原歌曲索引不变 if (currentIndex > fpIndex) { playlist.splice(fpIndex, 1); // 删掉歌曲后长度会-1 currentIndex--; } else { // 因为在原歌曲之前插入了一位,整体长度+1,原歌曲索引需要增加一位 playlist.splice(fpIndex + 1, 1); } } // sequenceList应该要插入的位置 let currentSIndex = findIndex(sequenceList, currentSong) + 1; // 查找sequenceList有没有这首歌 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); };
在suggest组件使用这个action
<li @click="selectItem(item)" class="suggest-item" v-for="(item,index) in result" :key="index"> import { mapActions } from 'vuex'; selectItem(item) { this.insertSong(item); }, ...mapActions([ 'insertSong', ]),
执行之后会报错:[vuex] do not mutate vuex store state outside mutation handlers. 我们只能在mutations.js修改其state。我们在actions.js直接引用了state.xx会影响到它本身所以会报错。
解决方法:使用slice()返回其副本进行使用
let playlist = state.playlist.slice();
let sequenceList = state.sequenceList.slice();
效果:
优化:对suggest做一些边界情况处理
- 搜索结果为空,需要友好告知用户搜索没有结果
基础组件no-result,基本结构代码如下:
<template> <div class="no-result"> <div class="no-result-icon"></div> <p class="no-result-text">{{title}}</p> </div> </template> <script> export default { props: { title: { type: String, default: '', }, }, }; </script> <style scoped lang="scss"> .no-result { text-align: center; .no-result-icon { width: 86px; height: 90px; margin: 0 auto; @include bg-image('no-result'); background-size: 86px 90px; } .no-result-text { margin-top: 30px; font-size: $font-size-medium; color: $color-text-d; } } </style>
在suggest组件调用这个no-result组件
<div v-show="!result.length" class="no-result-wrapper"> <no-result title="抱歉,暂无搜索结果"></no-result> </div> import NoResult from '../../base/no-result/no-result.vue'; components: { Scroll, NoResult, }, //修改search()方法,判断是否有歌曲 search() { // 请求服务端的检索数据 search(this.query).then((res) => { if (res.data.code === ERR_OK) { if (res.data.result.songCount === 0) { this.result = []; } else { this.result = this._normalizeSongs(res.data.result.songs); } } }); },
- 每输1个字符或回退1个字符,都在发送请求,这样实际上会有流量上的浪费,需要对input的输入进行节流。
在search-box中query和input是双向绑定的,当我们在input做输入的时候,query发生变化,回调就会执行。
而我们要做的就是让这个回调函数延迟执行,因为一旦这个回调函数执行了就会派发事件,外层组件就会收到该事件去执行query赋值从而引起query发生变化去请求数据。
在common.js中实现节流函数:
export function debounce(func, delay) { // 接收一个函数和节流时间参数 let timer;// 计时器 // 对一个函数做节流就会返回一个新的函数,这个新的函数就会延迟执行我们要节流的函数。在延迟期间这个函数又被调用了,之前的计时器就会清空,又会设置定时器去延迟执行这个函数。 return function (...args) { if (timer) { clearTimeout(timer); } timer = setTimeout(() => { func.apply(this, args); }, delay); }; }
在search-box使用节流函数
import { debounce } from '../../common/js/util'; created() { this.$watch('query', debounce((newQuery) => { this.$emit('query', newQuery); }, 200)); },
- 在手机上显示有个需要优化的地方:当点击输入框时,input是focus状态,手机键盘会弹出来,在滚动搜索结果列表时也是focus状态导致键盘无法收起来。
我们得知道这个列表被滚动的事件,其次列表在滚动之后让input去焦:
扩展scroll组件,在scroll组件中添加属性 beforeScroll,beforeScroll在滚动一开始会派发beforeScrollStart事件。
methods:{ _initScroll() { ...... if (this.beforeScroll) { this.scroll.on('beforeScrollStart', () => { this.$emit('beforeScroll'); }); } } }
在suggest组件监听beforeScroll然后去派发事件listScroll
<scroll class="suggest" :data="result" :beforeScroll="beforeScroll" @beforeScroll="listScroll"> data() { return { result: [], beforeScroll: true, }; },
在search-box添加一个对外的方法blur()
blur() { this.$refs.query.blur(); },
实际上关心scroll组件的是search组件,search监听listScroll,然后调用子组件的方法
<div class="search-result" v-show="query"> <suggest @listScroll="blurInput" :query="query"></suggest> </div>
blurInput() { this.$refs.searchBox.blur(); },