这一篇博客是和 search-input篇 衔接的, 需要的可以看上文
移动端 - 搜索组件(search-list篇)
这里我们需要去封装这么一个组件
先说一下大致的方向:
1. 根据父组件传入的关键字数据发送请求获取后端数据, 进行模板渲染
2. 处理一些边界情况(后端返回数据为空, 初次加载数据的时候触发了上拉加载)
3. 实现上拉加载
4. 实现点击歌曲数据的逻辑交互
5. 实现点击歌手数据的逻辑交互
第一步:
首先定义 suggest 组件大体结构和样式
<template>
<div class="suggest">
<ul class="suggest-list">
<!-- 歌手名 -->
<li class="suggest-item" v-if="singer">
<div class="icon">
<i class="icon-mine"></i>
</div>
<div class="name">
<p class="text">{{ singer.name }}</p>
</div>
</li>
<!-- 歌名和专辑名 -->
<li class="suggest-item" v-for="song in songs" :key="song.id">
<div class="icon">
<i class="icon-music"></i>
</div>
<div class="name">
<p class="text">
{{song.singer}}-{{song.name}}
</p>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'suggestCom',
setup () {
}
}
</script>
<style lang="scss" scoped>
.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();
}
}
}
}
}
</style>
第二步:
根据父组件传入的关键字数据的改变, 发送请求获取后端数据; 改变模板数据
因为 suggest 组件是一个公共业务组件, 为了更好的进行复用; 所以需要接收到两个数据
1. query (父组件传入的搜索关键字)
2. showSinger (后端发送请求时所需参数, 返回数据中是否添加歌手数据)
我们将 showSinger 通过父组传入的是有原因的
因为 suggest 组件必经是一个公共的业务组件, 有这样一个场景; 就是添加歌曲数据, 这个需求是可以继续搜索的
因为指明了说是添加歌曲数据, 所以搜索的结果数据是不需要显示歌手的数据的; 所以后端接收到的 showSinger 是 false
<template>
...
</template>
<script>
export default {
name: 'suggestCom',
props: {
query: {
type: String,
default: ''
},
showSinger: {
type: Boolean,
default: true
}
},
setup () {
}
}
</script>
<style lang="scss" scoped>
...
</style>
然后定义后端需要的数据和存储后端响应式数据:
<template>
...
</template>
<script>
import { ref } from 'vue'
export default {
name: 'suggestCom',
props: {
query: {
type: String,
default: ''
},
showSinger: {
type: Boolean,
default: true
}
},
setup () {
// 模板渲染所需数据
const singer = ref(null)
const songs = ref([])
// 后端请求所需数据
const hasMore = ref(true) // 初始化是否可以加载更多数据
const page = ref(1)
}
}
</script>
<style lang="scss" scoped>
...
</style>
然后我们需要监听 query 的数据变化, 调用请求接口 api 发送请求获取数据
<script>
watch(() => props.query, async (newQuery) => {
// 如果搜索数据query为空就什么都不做
if (!newQuery) return
await searchFirst()
})
// 第一次搜索需要对数据进行重置
const searchFirst = async () => {
page.value = 1
songs.value = []
singer.value = null
hasMore.value = true
// 然后调用接口获取数据, 将模板使用的数据进行覆盖
const result = await search(props.query, page.value, props.showSinger)
// 这里是调用获取歌曲的url数据接口
songs.value = await processSongs(result.songs)
singer.value = result.singer
hasMore.value = result.hasMore
}
</script>
这里所说的 "第一次搜索" , 并不是真正的第一次搜索; 因为后面会涉及到加载更多
所以 "第一次搜索" 所说的是, 除了上拉加载以外的数据获取都会去调用 searchFirst 这个方法获取数据
当数据还未返回时, 可以显示一个 loading 的效果, v-loading全局自定义指令封装讲解
<template>
<div class="suggest" v-loading:[loadingText]="loading">
...
</div>
</template>
<script>
export default {
name: 'suggestCom',
setup () {
const loadingText = ''
...
const loading = computed(() => {
return !singer.value && !songs.value.length
})
async function searchFirst () {
...
}
return {
singer,
songs,
hasMore,
loadingText,
loading
}
}
}
</script>
<style lang="scss" scoped>
...
</style>
最后实在 search 父组件中进行导入, 给 suggest 传入 query 参数
根据 query 参数动态渲染对应组件(热门搜索和搜索结果组件不会同时显示)
<template>
<div class="search">
<!-- search-input组件 -->
<div class="search-input-wrapper">
<SearchInput v-model="query"></SearchInput>
</div>
<!-- 热门搜索 -->
<div class="search-content" v-show="!query">
<div class="hot-keys">
<h1 class="title">热门搜索</h1>
<ul>
<li
class="item"
v-for="item in hotKeys"
:key="item.id"
@click="addQuery(item.key)"
>
<span>{{item.key}}</span>
</li>
</ul>
</div>
</div>
<!-- 搜索结果 -->
<div class="search-result" v-show="query">
<Suggest :query="query" />
</div>
</div>
</template>
第三步:
对 suggest 组件根据 query 搜索, 后端返回数据为空的情况进行处理
其实这里只需要通过判断 songs 的数据和 loading 的数据, 通过一个自定义指令来显示 "没有搜索结果" 结果的效果
因为 v-no-result 的效果和 v-loading 的效果是一样的, 所以直接使用同一个配置项来完成 这里有讲到
<template>
<div
class="suggest"
v-loading:[loadingText]="loading"
v-no-result:[noResultText]="noResult"
>
<ul class="suggest-list">
<!-- 歌手名 -->
<li class="suggest-item" v-if="singer">
<div class="icon">
<i class="icon-mine"></i>
</div>
<div class="name">
<p class="text">{{ singer.name }}</p>
</div>
</li>
<!-- 歌名和专辑名 -->
<li class="suggest-item" v-for="song in songs" :key="song.id">
<div class="icon">
<i class="icon-music"></i>
</div>
<div class="name">
<p class="text">
{{song.singer}}-{{song.name}}
</p>
</div>
</li>
</ul>
</div>
</template>
<script>
import { ref, watch, computed, nextTick } from 'vue'
import { search } from '@/api/search'
import { processSongs } from '@/api/song'
export default {
name: 'suggestCom',
props: {
query: {
type: String,
default: ''
},
showSinger: {
type: Boolean,
default: true
}
},
emits: ['selectSong', 'selectSinger'],
setup (props, { emit }) {
const singer = ref(null)
const songs = ref([])
const hasMore = ref(true)
const page = ref(1)
const loadingText = ref('')
const noResultText = ref('没有搜索到相关的歌手、歌曲')
const loading = computed(() => {
return !singer.value && !songs.value.length
})
// singer, songs, hasMore为false的时候就说明后端没有数据了
// hasMore每一次发送请求获取后端数据式, 会返回这个数据
// 当hasMore为false的时候, 就说明后端没有数据了
const noResult = computed(() => {
return !singer.value && !songs.value.length && !hasMore.value
})
watch(() => props.query, async (newQuery) => {
if (!newQuery) return
await searchFirst()
})
async function searchFirst () {
page.value = 1
songs.value = []
singer.value = null
hasMore.value = true
const result = await search(props.query, page.value, props.showSinger)
songs.value = await processSongs(result.songs)
singer.value = result.singer
hasMore.value = result.hasMore
}
return {
singer,
songs,
hasMore,
loadingText,
noResult,
noResultText,
}
}
}
</script>
<style lang="scss" scoped>
...
</style>
第四步:
实现上拉加载交互
这里我们会使用到 betterScroll 中的 pullup 插件来完成
npm install @better-scroll/core --save
npm install @better-scroll/pull-up --save
npm install @better-scroll/observe-dom --save
然后我们会将上拉加载的逻辑进行抽离成单独的 js 文件(use-pull-up-load.js), 预防其他组件会使用到
// 导入核心滚动BScroll
import BScroll from '@better-scroll/core'
// 导入上拉加载插件PullUp
import PullUp from '@better-scroll/pull-up'
// 导入动态监听DOM变化插件
import ObserveDOM from '@better-scroll/observe-dom'
import { ref, onMounted, onUnmounted } from 'vue'
// 将插件注册到BScroll中
BScroll.use(PullUp)
BScroll.use(ObserveDOM)
// 向外默认提供一个钩子函数
export default function usePullUpLoad (requestData) {
// new BScroll存储实例对象
const scroll = ref(null)
// 模板DOM元素实例
const rootRef = ref(null)
// 是否正在加载变量, 模板中需要使用到
const isPullUpLoad = ref(false)
// 在组件挂载的时候
onMounted(() => {
// new BScroll获取实例对象
const scrollVal = scroll.value = new BScroll(rootRef.value, {
// 上拉加载的配置
pullUpLoad: true,
observeDOM: true,
click: true
})
// 那到实例之后, 监听上拉加载事件
scrollVal.on('pullingUp', pullingUpHandler)
// 上拉加载的事件处理函数
async function pullingUpHandler () {
isPullUpLoad.value = true
// 调用请求获取数据
await requestData()
// 数据返回后, 结束上拉加载行为
scrollVal.finishPullUp()
// 刷新模板数据高度
scrollVal.refresh()
isPullUpLoad.value = false
}
})
onUnmounted(() => {
scroll.value.destroy()
})
return { scroll, rootRef, isPullUpLoad }
}
usePullUpLoad 这一个钩子函数做的事情也很简单:
1. 告知 BScroll 页面中哪一个 DOM 需要去做上拉加载操作
2. 拿到 scroll 实例之后, 监听上拉加载的事件
3. 触发上拉行为的时候, 发送请求获取数据; 最后更新原先获取 DOM 元素的模板高度
然后在 suggest 组件中进行导入使用
<template>
<div
ref="rootRef"
class="suggest"
...
>
<ul class="suggest-list">
<!-- 歌手名 -->
...
<!-- 歌名和专辑名 -->
...
<!-- 上拉加载行为 -->
<div class="suggest-item" v-loading:[loadingText]="pullUpLoading"></div>
</ul>
</div>
</template>
<script>
import { ref, watch, computed, nextTick } from 'vue'
import { search } from '@/api/search'
import { processSongs } from '@/api/song'
import usePullUpLoad from './use-pull-up-load'
export default {
name: 'suggestCom',
props: {
query: {
type: String,
default: ''
},
showSinger: {
type: Boolean,
default: true
}
},
emits: ['selectSong', 'selectSinger'],
setup (props, { emit }) {
const singer = ref(null)
const songs = ref([])
const hasMore = ref(true)
const page = ref(1)
const loadingText = ref('')
const noResultText = ref('没有搜索到相关的歌手、歌曲')
const loading = computed(() => {
return !singer.value && !songs.value.length
})
const noResult = computed(() => {
return !singer.value && !songs.value.length && !hasMore.value
})
// 正在加载中的loading效果
const pullUpLoading = computed(() => {
return isPullUpLoad.value && hasMore.value
})
// 使用钩子函数, 传入上拉加载函数; 拿到rootRef和isPullUpLoad数据
const { rootRef, isPullUpLoad, scroll } = usePullUpLoad( preventPullUpLoad)
watch(() => props.query, async (newQuery) => {
if (!newQuery) return
await searchFirst()
})
async function searchFirst () {
page.value = 1
songs.value = []
singer.value = null
hasMore.value = true
const result = await search(props.query, page.value, props.showSinger)
songs.value = await processSongs(result.songs)
singer.value = result.singer
hasMore.value = result.hasMore
}
// 上拉加载函数
async function searchMore () {
if (!hasMore.value) return
page.value++
const result = await search(props.query, page.value, props.showSinger)
songs.value = songs.value.concat(await processSongs(result.songs))
hasMore.value = result.hasMore
}
return {
singer,
songs,
hasMore,
loadingText,
noResult,
noResultText,
rootRef,
pullUpLoading
}
}
}
</script>
<style lang="scss" scoped>
...
</style>
第五步:
处理 "初次加载" 发送请求时, 用户上拉触发上拉加载行为; 页面中就会出现两个 loading 效果问题
我们的处理方法是, 当 "初次加载" 还在进行的时候不要让用户触发上拉加载行为
// loading为真时, 说明 "初次加载" 还未结束
const preventPullUpLoad = computed(() => {
return loading.value
})
// 然后传给钩子函数
const { rootRef, isPullUpLoad, scroll } = usePullUpLoad(searchMore, preventPullUpLoad)
不让用户触发上拉加载就是取消上拉加载行为
export default function usePullUpLoad (requestData, preventPullUpLoad) {
const scroll = ref(null)
const rootRef = ref(null)
const isPullUpLoad = ref(false)
onMounted(() => {
const scrollVal = scroll.value = new BScroll(rootRef.value, {
pullUpLoad: true,
observeDOM: true,
click: true
})
scrollVal.on('pullingUp', pullingUpHandler)
async function pullingUpHandler () {
// "初次加载" 还未加载完的时候, 阻止isPullUpLoad值的变化
if (preventPullUpLoad.value) {
scrollVal.finishPullUp()
return
}
isPullUpLoad.value = true
await requestData()
scrollVal.finishPullUp()
scrollVal.refresh()
isPullUpLoad.value = false
}
})
onUnmounted(() => {
scroll.value.destroy()
})
return { scroll, rootRef, isPullUpLoad }
}
第六步:
实现点击歌曲数据的逻辑交互, 这里需要根据业务需求来操作
我们这里需要做的事情就是:
1. 添加点击事件, 点击之后 emit 出去父组件需要的数据(毕竟 suggest 是一个公共业务组件)
2.父组件监听到做出对应的业务需求
第七步:
实现点击歌手数据的逻辑交互
我们这里需要做的事情就是:
1. 添加点击事件, 点击之后 emit 出去父组件需要的数据
2.父组件监听到做出对应的业务需求