我们先来看一下最终效果
这样的搜索组件在移动端是很常见的, 大部分需求都是:
1. 搜索框进行搜索关键字
2. 热门搜索
3. 搜索历史
4. 搜索结果(提供上拉加载效果)
上述的基本需求也是我们现在需要去实现的, 先来说一下大致的方向:
1. search 一般都是一个路由组件, 所以先创建 search.vue 组件
2. 拆分 search-input (搜索框业务组件), 实现其需求
3. 拆分 search-suggest (搜索结果业务组件), 实现其需求
4. 拆分 search-list (搜索历史基础组件), 持久化保存用户搜索数据
5. 在 search.vue 父组件中使用上述拆分出去的组件, 根据条件动态渲染热门搜索, 搜索历史或搜索结果组件
第一步:
1. 抽离 search-input.vue 组件
抽离的原因就是为了进行复用, 可能在项目的其他地方会使用到; 这也是 vue 的特色之一
<template>
<div class="search-input">
<!-- 搜索图标 -->
<i class="icon-search"></i>
<!-- input框 -->
<input class="input-inner"/>
<!-- 清空搜索数据图标 -->
<i class="icon-dismiss"></i>
</div>
</template>
<script>
export default {
name: 'search-input'
}
</script>
<style lang="scss" scoped>
.search-input {
display: flex;
align-items: center;
box-sizing: border-box;
width: 100%;
padding: 0 6px;
height: 32px;
background: $color-highlight-background;
border-radius: 6px;
.icon-search {
font-size: 24px;
color: $color-text-d;
}
.input-inner {
flex: 1;
margin: 0 5px;
line-height: 18px;
background: $color-highlight-background;
color: $color-text;
font-size: $font-size-medium;
outline: 0;
&::placeholder {
color: $color-text-d;
}
}
.icon-dismiss {
font-size: 16px;
color: $color-text-d;
}
}
</style>
先来说一下, 这一个组件需要得到什么数据; 返回给父组件什么数据:
1. 返回出去用户输入的关键字信息(父组件拿到发送请求获取数据)
2. 接收到父组件传入的关键字信息, 然后显示到 input 框中(父组件主动修改的情况)
首先完成第一个需求:
1. 在 search-input 组件中定义 v-model 数据
2. watch 监听对应数据的变化, 一旦变化 emit 修改父组件数据(当然需要进行节流操作)
<template>
<div class="search-input">
<i class="icon-search"></i>
<input class="input-inner" v-model="query"/>
<i class="icon-dismiss"></i>
</div>
</template>
<script>
export default {
name: 'search-input',
props: {
modelValue: {
type: String,
default: ''
}
},
data () {
return {
query: this.modelValue
}
},
watch: {
query (newQuery) {
this.$emit('update:modelValue', newQuery.trim())
}
}
}
</script>
<style lang="scss" scoped>
...
</style>
因为监听 query 数据变化的时候, 会用到 debounce 方法; 所以 watch 的监听需要做一些改变
<script>
import { debounce } from 'throttle-debounce'
created () {
this.$watch('query', debounce(300, (newQuery) => {
this.$emit('update:modelValue', newQuery.trim())
}))
}
// watch: {
// query: debounce(3000, function (newQuery) {
// this.$emit('update:modelValue', newQuery.trim())
// })
// }
</script>
再来完成第二个需求:
1. 在父组件中定义 v-model 数据
<template>
<div class="search">
<div class="search-input-wrapper">
<!-- 搜索框组件 -->
<SearchInput v-model="query"></SearchInput>
</div>
</div>
</template>
<script>
import SearchInput from '@/components/search/search-input'
import { ref } from 'vue'
export default {
name: 'searchCom',
components: {
SearchInput
},
setup () {
const query = ref('')
return { query }
}
}
现在整体的父子组件交互需求是完成了, 我们还需要去完善一下其他的需求
1. 点击清空数据按钮, 清空数据
2. 扩展 input 框的 placeholder 属性
<template>
<div class="search-input">
<i class="icon-search"></i>
<input class="input-inner" v-model="query" :placeholder="placeholder" />
<!-- 只有当query有值是才会显示 -->
<i class="icon-dismiss" v-show="query" @click="clear"></i>
</div>
</template>
<script>
props: {
modelValue: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '搜索歌曲、歌手'
}
},
methods: {
clear () {
this.query = ''
}
}
</script>
现在父组件中, search-input 组件封装完毕了; 然后还要在父组件中显示 "热门搜索" 的需求
1. 首先热门搜索的数据是发送请求获取来的(响应式变量进行接收)
2. 拿着数据对模板进行 v-for 遍历
3. 给每一个元素添加点击事件, 触发点击事件时; 会修改掉 query 的数据
<template>
<div class="search">
<!-- 搜索组件 -->
<div class="search-input-wrapper">
<SearchInput v-model="query"></SearchInput>
</div>
<!-- 热门搜索 -->
<div class="search-content">
<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>
</template>
<script>
import { getHotKeys } from '@/api/search'
getHotKeys().then(res => {
hotKeys.value = res.hotKeys
})
// 点击数据修改子组件数据
const addQuery = (key) => {
query.value = key
}
</script>