本节主要讲如何引入搜索组件,以及如何定制搜索组件。

# 发现页与视频页面添加标题栏

细心的小伙伴早就可能发现了首页与视频页面并没有头部。这一块头部的添加是相关联搜索的,需要特殊处理一下,因此放到这一节来说。

image-20210215155333640

发现页

image-20210215155344079

由于发现页面布局的改变,因此原有的代码也是需要进行稍微的调整,改为:头部标题栏 + 滚动浏览区域。

发现页相关代码:

<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() {
			// 业务逻辑
		}
  }
}

说完搜索组件的调用,接下来讲解一下如何写实现页面

img

发现页

这个组件的布局相关代码:

<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 存储搜索记录

image-20210215155416874

这一块的信息一般就是由前端的存储机制去做了,在这使用 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),有兴趣的可以查看源码。

当前的搜索组件的主要逻辑就是这么多,引入到发现页与视频页即可。

# 小结

  1. 类似窗口类的组件页面,我们在定义的时候,就要暴露方法给父组件控制显示及隐藏;
  2. 一些不用后台处理的信息可以放到前端去处理,这样可以避免与后台不必要的交互,防止资源浪费;
  3. 防抖是有效解决高频率触发请求后台服务器的方法。
  4. 本章代码 uni-course-实战开发搜索页 (opens new window)。
阅读全文