微信小程序|开发实战篇之三

前言

实战篇内容参考:
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组件分页事件优化

细节问题:

  1. 每次上拉到底部,无论是否有新的数据出现,都会向服务器发送请求。
  2. 在上拉到底部时,还没加载出新数据的过程中不断上拉,会出现重复的数据。
  3. 对下拉操作加锁前提下,在请求失败时(网络中断),会出现加载不了新数据。
  4. 在对下拉操作加锁的位置出现问题时(在判断hasMore,有更多数据加载的判断之前,对互斥变量进行加锁。),可能先获取lock之后,发现加载完数据了,就不会主动释放锁,导致加载不了其他书籍的数据。
  5. 没使用this.data.dataArray = [],total = null对数据初始化,或者直接使用改变变量的方式初始化,会导致页面还有上一次渲染的数据。
  6. 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
   })
 },
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值