本节主要讲如何引入搜索组件,以及如何定制搜索组件。
# 发现页与视频页面添加标题栏
细心的小伙伴早就可能发现了首页与视频页面并没有头部。这一块头部的添加是相关联搜索的,需要特殊处理一下,因此放到这一节来说。

发现页

由于发现页面布局的改变,因此原有的代码也是需要进行稍微的调整,改为:头部标题栏 + 滚动浏览区域。
发现页相关代码:
<view>
<!-- 头部 -->
<uni-nav-bar fixed status-bar>
<block slot="left"><image class="top-img" src="/static/image/search/6.png"></image></block>
<view class="top-search flex-box">
<image class="search-icon" src="/static/image/search/2.png"></image>
{{ searchTxt }}
</view>
<!-- #ifdef APP-PLUS || H5 -->
<block slot="right"><image class="top-img" src="/static/image/mine/r.png"></image></block>
<!-- #endif -->
</uni-nav-bar>
<!-- 滚动区域 -->
<view class="page-content">
<mescroll-uni ref="mescroll" :fixed="false" :down="downOption" :up="upOption" @down="downCallback" @up="upCallback">
滚动区域内容
</mescroll-uni>
</view>
</view>
.page-content {
position: fixed;
top: 64px;
left: 0;
right: 0;
bottom: 0px;
/* #ifdef H5 || MP-WEIXIN */
top: 44px;
/* #endif */
}
在发现页与视频页面上的搜索框并不是 input 表单控件,是直接由 view 模拟的。
上面介绍了页面标题栏,那么就是确定了搜索的入口了。我们要实现的是点击首页的模拟搜索框,然后搜索页出现的交互。接下来我们开始开发这个组件。
# 开发搜索组件
类似这样的窗口类的组件页面,我们在定义的时候,就要控制显示及隐藏。
<template>
<view class="search-page" v-if="isShow">
主体内容
</view>
</template>
data() {
return {
isShow: false
}
},
methods: {
close () {
this.$emit('close') // 分发关闭事件给父组件监听
this.isShow = false
// 业务逻辑
},
open () {
this.isShow = true
// 业务逻辑
},
}
这样就可以在父页面进行引入调用,控制搜索组件显示及隐藏了。
<search ref="search" @close="closeSearch"></search>
import search from '@/components/search.vue';
export default {
components: {
search
},
methods: {
// 打开搜索
openSearch() {
this.$refs.search.open()
},
// 监听关闭搜索
closeSearch() {
// 业务逻辑
}
}
}
说完搜索组件的调用,接下来讲解一下如何写实现页面

发现页
这个组件的布局相关代码:
<template>
<view class="search-page" v-if="isShow">
<!-- 头部 -->
<uni-nav-bar fixed :isShowLeft="false" :status-bar="true" @clickLeft="goCloud" @clickRight="goCloud">
<view class="search-box flex-box">
<view class="top-search flex-box flex-item">
<image class="search-icon" src="/static/image/search/2.png"></image>
<input class="flex-item" :focus="isFocus" type="text" v-model="searchTxt" @confirm="addSearch" confirm-type="search">
</view>
<view class="cancle" @click="close">取消</view>
</view>
<!-- #ifdef APP-PLUS || H5 -->
<block slot="right"><image class="top-img" src="/static/image/search/1.png"></image></block>
<!-- #endif -->
</uni-nav-bar>
<!-- 滚动区域 -->
<scroll-view class="page-content">
内容区域
</scroll-view>
</view>
</template>
关键代码:
<input class="flex-item" :focus="isFocus" type="text" v-model="searchTxt" @confirm="addSearch" confirm-type="search">
:focus="isFocus"获取焦点,是否唤起键盘;v-model="searchTxt"监听输入值;confirm-type="search"键盘右下角按钮显示为搜索(仅在 type="text" 时生效);@confirm="addSearch"点击右下角搜索执行addSearch事件。
整个交互逻辑是从发现页点击入口加载这个页面,然后输入框自动聚焦,唤起软键盘。输入框输入文字后点击软键盘的搜索完成一次搜索。软键盘弹出后,点击非置焦区域可收起软键盘。点击输入框旁边的取消关闭搜索组件。
由于当前开发的输入框不是 app 平台的原生输入框,存在一个问题是当 input 获得焦点,键盘弹起,然后不点击键盘的完成,就无法关闭键盘。官方作出解释:
App 平台在 iOS 上,webview 中的软键盘弹出时,默认在软键盘上方有一个横条,显示着:上一项、下一项和完成等按钮。如不想显示这个横条,可以配置 softinputNavBar: 'none'。
配置方式,在 pages.json 中配置 style
"app-plus": {
"softinputNavBar": "none"
}
在 pages.json 单个页面的设置:
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "发现",
"navigationStyle": "custom",
"app-plus":{
"titleNView": false,
"bounce" : "none", // 取消iOS回弹,避免和下拉刷新冲突
"disableScroll": true, // Android小程序卡顿
"softinputNavBar": "none"
}
}
}
原生输入框在iOS上不会有软键盘上方的横条。
# 使用 storage 存储搜索记录

这一块的信息一般就是由前端的存储机制去做了,在这使用 Uniapp 的 uni.setStorageSync() ,uni.getStorageSync() 进行存取搜索历史。
// 获取搜索历史
getSearchList () {
try {
const value = uni.getStorageSync('searchlist');
if (value) {
this.historyList = JSON.parse(value)
}
} catch (e) {
// error
}
},
// 清除历史
clearSearch () {
uni.showModal({
title: '',
content: '确认清除全部历史记录?',
success: res => {
if (res.confirm) {
uni.setStorageSync('searchlist', '')
this.historyList = []
} else if (res.cancel) {
// console.log('用户点击取消');
}
}
});
},
// 添加搜索
addSearch () {
const txt = this.searchTxt.trim()
if (txt.length == 0) { // 空字符不添加
return false
}
const tempIndex = this.historyList.indexOf(txt)
if (tempIndex !== -1) { // 如果存在相同的删除旧的
this.historyList.splice(tempIndex, 1)
}
// 插入数组前
this.historyList.unshift(txt)
if (this.historyList.length > 7) { // 数组长度7
this.historyList = this.historyList.slice(0, 7)
}
uni.setStorageSync('searchlist', JSON.stringify(this.historyList))
},
# 使用防抖功能优化请求
防抖(debounce):单位时间内多次触发只会执行一次,即如果短时间内大量触发同一事件,只会执行一次函数。
这个在前端开发的过程中比较容易遇到,经常需要绑定一些持续触发的事件,如 resize、scroll、mousemove、change 等等,但我们并不希望在事件持续触发的过程中那么频繁地去执行函数。防止高频率的触发请求后台服务器。
我们要做的优化策略是当事件被触发时,设定一个周期延迟执行搜索动作,若期间又被触发,则重新设定周期,直到周期结束,执行搜索动作。这样搜索的请求控制在一定时间内。
防抖代码:
// 防抖
function debounce(fn, wait = 300) {
var timeout = null;
return function() {
if(timeout !== null) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}
代码使用:
<input type="text" v-model="searchTxt" confirm-type="search" @input="inputSearch">
methods: {
// 输入搜索
inputSearch: debounce(() => {
// 未搜索或者写入空格的情况下不显示搜索建议
if (!that.searchTxt.trim().length) {
that.suggestList = []
return false
}
const par = {
type: 'mobile',
keywords: that.searchTxt.trim(),
}
// 请求后台数据
apiSearchSuggest(par).then(res => {
that.suggestList = res.result.allMatch
})
})
}
像很多抢票、抢购这类操作很多都加了防抖优化策略,为了避免用户大量点击造成服务器压力,当固定时间内,不论点击多少次,只执行一次抢的动作。
感兴趣的小伙伴可以扩展了解一下与防抖实现的效果类似的节流。
业界最标准的当属 lodash 库的两个方法,debounce (opens new window),throttle (opens new window),有兴趣的可以查看源码。
当前的搜索组件的主要逻辑就是这么多,引入到发现页与视频页即可。
# 小结
- 类似窗口类的组件页面,我们在定义的时候,就要暴露方法给父组件控制显示及隐藏;
- 一些不用后台处理的信息可以放到前端去处理,这样可以避免与后台不必要的交互,防止资源浪费;
- 防抖是有效解决高频率触发请求后台服务器的方法。
- 本章代码 uni-course-实战开发搜索页 (opens new window)。