前言
实战篇内容参考:
1、小程序开发实战:https://coding.imooc.com/class/chapter/251.html#Anchor
2、博主:ssc在路上
1、使用Promise获取多个异步方法的结果
const detail = bookModel.getDetail(bid);
const comments = bookModel.getComments(bid);
const likeStatus = bookModel.getLikeStatus(bid);
// Promise.race(xx,xx,xx)返回最先有结果的值。 【竞争】
Promise.all([detail, comments, likeStatus])
.then(res=>{
wx.hideLoading();
this.setData({
book:res[0],
comments:res[1],
likeStatus:res[2].like_status,
likeCount:res[2].fav_nums
})
})
2、高阶组件-search
2.1 search组件的基本结构
2.1.1 search组件的骨架index.wxml文件
<!--components/search/index.wxml-->
<view class="container">
<view class="header">
<view class="search-container">
<image class="icon" src="images/search.png"></image>
<input placeholder-class="in-bar" placeholder="书籍名" class="bar" auto-focus="true"></input>
<image class="cancel-img" src="images/cancel.png"></image>
</view>
<view bind:tap="onCancel" class="cancel">取消</view>
</view>
<view class="history">
<view class="title">
<view class="chunk"></view>
<text>历史搜索</text>
</view>
</view>
<view class="history hot-search">
<view class="title">
<view class="chunk"></view>
<text>热门搜索</text>
</view>
</view>
</view>
2.1.2 search组件的样式index.wxss文件
/* 搜索组件样式 */
.container {
display: flex;
flex-direction: column;
align-items: center;
height: 120rpx;
width: 100%;
}
.header {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
height: 100rpx;
width: 750rpx;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
background-color: #ffffff;
z-index: 99;
}
.search-container{
display: inline-flex;
flex-direction: row;
align-items: center;
background-color: #f5f5f5;
border-radius: 50px;
margin-left: 20rpx;
}
.history{
display: flex;
flex-direction: column;
margin: 40rpx 0 20rpx 0;
margin-top: 160rpx;
width: 690rpx;
font-size: 28rpx;
}
.title {
display: flex;
flex-direction: row;
align-items: center;
line-height: 30rpx;
}
.hot-search {
/* 覆盖父样式 */
margin-top: 40rpx;
}
.in-bar {
color: #999;
}
.bar {
border-top-right-radius: 15px;
border-bottom-right-radius: 15px;
display: inline-block;
height: 68rpx;
width: 500rpx;
font-size: 28rpx;
}
.icon {
width: 28rpx;
height: 28rpx;
margin-left: 24rpx;
margin-right: 16rpx;
}
.cancel {
line-height: 68rpx;
width: 120rpx;
text-align: center;
display: inline-block;
border: none;
}
.cancel-img {
width: 28rpx;
height: 28rpx;
margin-right: 20rpx;
}
.chunk {
height: 15px;
width: 5px;
display: inline-block;
margin-right: 10px;
background-color: #000;
}
.tags {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 24rpx;
padding-left: 15px;
width: 630rpx;
}
.tags v-tag {
margin-right: 20rpx;
margin-bottom: 20rpx;
}
2.1.3 search组件的基本业务逻辑index.js
(1)组件search 捕获点击事件,然后以自定义事件穿的给页面book。
search的取消操作事件,通过自定义事件传递给book页面。控制v-search组件显示的searching布尔型变量变成false,进而不显示v-search组件。
/**
* 组件的方法列表
*/
methods: {
// 点击搜索取消
onCancel(event){
this.triggerEvent('cancel',{},{})
}
}
组件页面中给header中的“取消”文本添加tap事件,命名为“onCancel”。
<!-- 头部搜索栏 -->
<view class="header">
<view class="search-container">
...
</view>
<view bind:tap="onCancel" class="cancel">取消</view>
</view>
(2)页面book 在v-search中监听自定义事件,定义searching变量来决定是否渲染search组件。
<v-search more="{{more}}" bind:cancel="onCancel" wx:if="{{searching}}" />
// page book.js
onSearching(event){
this.setData({
searching: true
})
},
onCancel(event){
this.setData({
searching: false
})
},
2.1.4 search组件的业务逻辑
首先明确一点search组件一直是处于page的book页面当中,只有点击搜索框的时候会渲染出来。
2.1.4.1 历史搜索和热门搜索的显示
- 历史搜索和热门搜索都是以tag显示的,区别在于历史搜索是在本地缓存中保存、获取,而热门搜索是在search组件attached初始化时,向服务器进行请求的。
- 封装keywords模块。
封装模块keywords.js
import { HTTP } from "../util/http-p";
class KeywordModel extends HTTP{
key = 'q';
maxLength = 10;
getHistory(){
const words = wx.getStorageSync(this.key);
if(!words){
return [];
}
return words;
}
getHot(){
return this.request({
url: '/book/hot_keyword'
})
}
// 队列方式存储历史信息
addToHistory(keyword){
let words = this.getHistory();
const has = words.includes(keyword);
if(!has){
const length = words.length;
// 删除末尾Word
if(length >= this.maxLength){
words.pop()
}
// 添加到首部
words.unshift(keyword);
wx.setStorageSync(this.key, words);
}
}
}
export {KeywordModel}
历史搜索和热门搜索具体显示逻辑index.js。
组件显示时,调用keywords模板方法,从本地缓存中获取历史搜索,并且发送请求给服务器获取热点搜索。attached默认的组件加载时执行的方法。
attached(){
const historyWords = keywordModel.getHistory();
const hotWords = keywordModel.getHot();
this.setData({
historyWords
})
hotWords.then(res=>{
this.setData({
hotWords:res.hot
})
})
},
历史搜索和热门搜索页面渲染index.wxml
<!-- 搜索记录 -->
<view wx:if="{{!searching}}">
<view class="history">
<view class="title">
<view class="chunk"></view>
<text>历史搜索</text>
</view>
<view class="tags">
<block wx:key="*this" wx:for="{{historyWords}}">
<v-tag bind:tapping="onConfirm" text="{{item}}" />
</block>
</view>
</view>
<view class="history hot-search">
<view class="title">
<view class="chunk"></view>
<text>热门搜索</text>
</view>
<view class="tags">
<block wx:key="*this" wx:for="{{hotWords}}">
<v-tag bind:tapping="onConfirm" text="{{item}}" />
</block>
</view>
</view>
</view>
2.1.4.2 显示搜索的内容—书籍
searching布尔型变量,控制显示搜索tag还是搜索结果。
<!-- 显示搜索内容,本组件内显示 -->
<view wx:if="{{searching}}" class="books-container">
<block wx:for="{{dataArray}}" wx:key="{{item.id}}">
<v-book book="{{item}}" />
</block>
</view>
搜索并返回结果的onConfirm方法。
将searching变量置为true,表示显示book搜索结果,隐藏历史搜索和热门搜索。
// 用户搜索方法
onConfirm(event){
// 1、敲击回车,标签隐藏,可以显示book
this.setData({
searching: true
})
// 2、取得用户输入
const word = event.detail.value || event.detail.text;
// text是tag携带的文本数据
if(word == undefined && word == ''){
return;
}
// 测试:暂时直接加入到缓存中
keywordModel.addToHistory(word);
// 3、按输入搜索书籍
bookModel.search(0, word)
.then(res => {
this.setData({
dataArray: res.books,
searchInput : word
})
// 防止历史搜索保存无效关键词,db中没有的
keywordModel.addToHistory(word);
})
},
给search组件的输入框回车绑定onConfirm事件,给点击tag也绑定onConfirm事件。也就是index.wxml中进行绑定。
tag使用tapping事件,是因为tag的点击,是要从组件tag中响应点击事件tap,然后通过自定义事件传递给 使用tag的search组件的。
<input bind:confirm="onConfirm" placeholder-class="in-bar" placeholder="书籍名" value="{{searchInput}}" class="bar" auto-focus="true"></input>
...
<view class="tags">
<block wx:key="*this" wx:for="{{historyWords}}">
<v-tag bind:tapping="onConfirm" text="{{item}}" />
</block>
</view>
...
<view class="tags">
<block wx:key="*this" wx:for="{{hotWords}}">
<v-tag bind:tapping="onConfirm" text="{{item}}" />
</block>
</view>
2.1.4.3 search组件内部的分页逻辑
首先明确search组件的上拉加载,其实是book页面的上拉加载。然后相当于是页面与组件进行通信,页面发生上拉事件,就可以通过properties中的变量传递下拉的状态,传递给组件。组件根据变量进行获取数据、数据绑定等操作。
//book页面中监听上拉事件
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
this.setData({
more: random(16)
})
},
页面 ===> 组件 properties
random生成16位的随机字符串,为了保证每次传递的more参数都会发生变化。然后就可以在search组件的properties使用observer函数进行监听处理。
book页面传递more参数:
<v-search more="{{more}}" bind:cancel="onCancel" wx:if="{{searching}}" />
search组件:
/**
* 组件的属性列表
*/
properties: {
// more发生改变就会触发observer函数
more:{
type:String,
observer: '_load_more'
}
},
_load_more(){
let input = this.data.searchInput;
if(!input){
return;
}
// loading变量相当于互斥变量,控制每次发生一次请求
if(_isLocked()){
return;
}
// this._locked(); 先锁住会导致在加载完最后一次数据后,不能释放锁。
// 下一次加载别的书籍时,就无法获取互斥变量,也就无法加载数据。
if(this.hasMore()){
this._locked();
bookModel.search(this.getCurrentStart(), input)
.then(res => {
this.setMoreData(res.books);
this._unLocked();
},()=>{
// 避免死锁,在请求失败时释放锁!
this._unLocked();
})
}
}
2.1.4.4 search组件分页事件优化
细节问题:
- 每次上拉到底部,无论是否有新的数据出现,都会向服务器发送请求。
- 在上拉到底部时,还没加载出新数据的过程中不断上拉,会出现重复的数据。
- 对下拉操作加锁前提下,在请求失败时(网络中断),会出现加载不了新数据。
- 在对下拉操作加锁的位置出现问题时(在判断hasMore,有更多数据加载的判断之前,对互斥变量进行加锁。),可能先获取lock之后,发现加载完数据了,就不会主动释放锁,导致加载不了其他书籍的数据。
- 没使用this.data.dataArray = [],total = null对数据初始化,或者直接使用改变变量的方式初始化,会导致页面还有上一次渲染的数据。
- search组件中的load_more()和onConfirm()函数很冗长。
2.1.4.4.1 细节问题一、二
每次上拉触底都会生成随机数,observer函数都会响应无法改变。但是可以对每次的加载操作“加锁”,申请一个互斥变量loading,true代表锁上,false代表没锁可以使用。
问题一,对服务器是否有剩余数据进行判断,对服务器传递的total变量和dataArray的长度比较。(或者没有total变量传递,在请求成功后判断返回的res.books是否为空)
问题二,先判断服务器还有剩余数据,然后每次给服务器发送请求上锁,请求结束时释放锁
2.1.4.4.2 细节问题三
在bookModel中的search函数的失败回调中释放锁资源。
bookModel.search(this.getCurrentStart(), input)
.then(res => {
this.setMoreData(res.books);
this._unLocked();
},()=>{
// 避免死锁,在请求失败时释放锁!
this._unLocked();
})
2.1.4.4.3 细节问题四
先判断服务器还有无剩余数据,再加锁。
// this._locked(); 先锁住会导致在加载完最后一次数据后,不能释放锁。
// 下一次加载其他书籍时就没有数据
if(this.hasMore()){
this._locked();
bookModel.search(this.getCurrentStart(), input)
.then(res => {
this.setMoreData(res.books);
this._unLocked();
},()=>{
// 避免死锁,在请求失败时释放锁!
this._unLocked();
})
}
2.1.4.4.4 细节问题五
在抽取出来的behavior中添加initialize()方法,对dataArray[]和total进行初始化。以setData()方式,对页面进行渲染。
initialize(){
// this.data.dataArray = [],
this.data.total = null
this.setData({
dataArray:[],
noneResult: false
});
}
2.1.4.4.5 细节问题六
将对显示、隐藏书籍页面封装成私有函数_showResult()、_closeResult();将对锁的操作封装成函数;对加载动画的函数进行封装_showLoadingCenter()、_closeLoadingCenter();对dataArray数组进行分页的逻辑专门封装成一个behavior。
// 显示搜索结果
_showResult(){
this.setData({
searching: true
})
},
// 隐藏搜索结果
_closeResult(){
this.setData({
searching: false
})
},
_locked(){
// this.data.loading = true;
this.setData({
loading: true
})
},
_unLocked(){
this.setData({
loading: false
})
},
_isLocked(){
return this.data.loading;
}
封装分页逻辑
const pagingBev = Behavior({
data:{
dataArray:[],
total: null,
noneResult: false
},
methods:{
setMoreData(dataArray){
const tempArray = this.data.dataArray.concat(dataArray);
this.setData({
dataArray: tempArray
})
},
getCurrentStart(){
return this.data.dataArray.length;
},
setTotal(total){
this.data.total = total
if(total == 0){
this.setData({
noneResult: true
})
}
},
hasMore(){
if(this.data.dataArray.length >= this.data.total){
return false
}else{
return true
}
},
initialize(){
// this.data.dataArray = [],
this.data.total = null
this.setData({
dataArray:[],
noneResult: false
});
}
}
})
export {pagingBev}
2.1.4.4.6 loading组件应用
(1)主要是在搜索结果进行渲染前,给予用户一定的反馈。
(2)在第一次点击搜索栏加载数据时显示loading组件,还有在上拉刷新的时候显示loading组件。
点击搜索栏时,使用LoadingCenter变量来控制显示;上拉加载时,使用已有的Loading变量来控制显示,不过需要setData()来控制。
<!-- 加载动画 -->
<v-loading class="loading-center" wx:if="{{loadingCenter}}" />
<v-loading class="loading-center" wx:if="{{loading}}" />
_showLoadingCenter(){
this.setData({
loadingCenter: true
})
},
_hideLoadingCenter(){
this.setData({
loadingCenter: false
})
},