微信小程序之组件的集合(五)

  这个是学习复杂的组件的封装的,在课程中,主要实现的是书单上方的搜索功能组件的开发,这个应该是较之前的组件是有一定难度的,但是现在学到现在,感觉前端的内容和后端的内容比较起来,还是比较容易的,而且好多内容,其实在后端的开发中是很成熟的,所以学起来并不是很难以理解,这也是我们的一个优势吧,毕竟选择后端的同学应该是不错的啊,哈哈哈!这种组件其实是有一个特别的名字的,那就是高阶组件,来,把这个东西尽快学习掌握!

一、新建search组件

首先还是新建search组件的各个文件,这里微信开发者工具的功能很好用,直接就新建一个search目录,然后在目录中新建一个名字为index的component组件,这样就建好了四个与组件相关的文件

微信小程序之组件的集合(五)

 二、search组件开发

1、基础的结构搭建

(1)样式文件的代码index.wxml文件

 1 <view class="container">
 2   <view class="header">
 3     <view class="search-container">
 4       <image class="icon" src="images/search.png"></image>
 5       <input placeholder-class="in-bar" placeholder="书籍名" class="bar" auto-focus="true"></input>
 6       <image class="cancel-img" src="images/cancel.png"></image>
 7     </view>
 8     <view class="cancel" bindtap="onCancel">取消</view>
 9   </view>
10   <view>
11     <view class="history">
12       <view class="title">
13         <view class="chunk"></view>
14         <text>历史搜索</text>
15       </view>
16     </view>
17     <view class="history hot-search">
18       <view class="title">
19         <view class="chunk"></view>
20         <text>热门搜索</text>
21       </view>
22     </view> 
23   </view>
24 </view>

(2)样式文件 index.wxss代码

  1 .container {
  2   display: flex;
  3   flex-direction: column;
  4   align-items: center;
  5   width: 100%;
  6   /* padding-left:15px; *//* padding-right:15px; */
  7 }
  8 
  9 .history {
 10   width: 690rpx;
 11   margin: 40rpx 0 20rpx 0;
 12   display: flex;
 13   font-size: 14px;
 14   margin-top:160rpx;
 15   flex-direction: column;
 16 }
 17 
 18 .hot-search{
 19   margin-top:70rpx;
 20 }
 21 
 22 .title {
 23   line-height: 15px;
 24   display: flex;
 25   flex-direction: row;
 26   align-items: center;
 27   /* margin-left:100px; */
 28 }
 29 
 30 .search-container {
 31   display: inline-flex;
 32   flex-direction: row;
 33   align-items: center;
 34   background-color: #f5f5f5;
 35   border-radius: 50px;
 36   margin-left: 20rpx;
 37   /* margin-left: */
 38 }
 39 
 40 .books-container book-cmp {
 41   margin-bottom: 25rpx;
 42 }
 43 
 44 .cancel-img {
 45   width: 14px;
 46   height: 14px;
 47   margin-right: 10px;
 48 }
 49 
 50 .books-container {
 51   width: 570rpx;
 52   margin-top:100rpx;
 53   display: flex;
 54   flex-direction: row;
 55   flex-wrap: wrap;
 56   padding: 0 90rpx 0 90rpx;
 57   justify-content: space-between;
 58 }
 59 
 60 .loading {
 61   margin: 50rpx 0 50rpx 0;
 62 }
 63 
 64 .loading-center {
 65   position: absolute;
 66   top: 50%;
 67   left: 50%;
 68 }
 69 
 70 .empty-tip {
 71   display: inline-block;
 72   width: 100%;
 73   text-align: center;
 74   position: absolute;
 75   top: 50%;
 76   /* left: 275rpx; */
 77 }
 78 
 79 .icon {
 80   width: 14px;
 81   height: 14px;
 82   margin-left: 12px;
 83   margin-right: 8px;
 84 }
 85 
 86 .in-bar {
 87   color: #999;
 88 }
 89 
 90 .cancel {
 91   line-height: 34px;
 92   width: 60px;
 93   /* margin-left:10px; */
 94   text-align: center;
 95   display: inline-block;
 96   border: none;
 97 }
 98 
 99 .chunk {
100   height: 15px;
101   width: 5px;
102   background-color: #000;
103   display: inline-block;
104   margin-right: 10px;
105 }
106 
107 .tags {
108   /* padding-left:15px; */
109   display: flex;
110   flex-direction: row;
111   flex-wrap: wrap;
112   /* justify-content: flex-start;  */
113   margin-top: 24rpx;
114   padding-left: 15px;
115   width: 630rpx;
116 }
117 
118 .tags tag-cmp {
119   margin-right: 10px;
120   margin-bottom: 10px;
121   /* padding-bottom: 10px; *//* margin-right:6px; */
122 }
123 
124 .header {
125   background-color: #ffffff;
126   position:fixed;
127   height: 100rpx;
128   border-top: 1px solid #f5f5f5;
129   border-bottom: 1px solid #f5f5f5;
130   display: flex;
131   flex-direction: row;
132   width: 750rpx;
133   align-items: center;
134   z-index:99;
135   /* padding-left:15px;  *//* padding-right:5px; */
136 }
137 
138 .bar {
139   border-top-right-radius: 15px;
140   border-bottom-right-radius: 15px;
141   display: inline-block;
142   height: 34px;
143   /* width:100%; */
144   width: 500rpx;
145   font-size: 14px;
146 }
147 
148 .test {
149   background-color: #000;
150 }

(3)基础的业务逻辑处理 index.js

这个主要是将取消操作交给page中页面进行处理,将组件中的取消事件传递给page页面中

1   /**
2    * 组件的方法列表
3    */
4   methods: {
5     // 搜索取消事件
6     onCancel(event){
7         this.triggerEvent(‘cancel‘,{},{});
8     }
9   }

page中book页面进行组件的展示以及业务逻辑处理的代码:

book.wxml文件中添加事件以及显示代码

 1 <view class="container" wx:if="{{!searching}}">
 2   <view class="header">
 3     <view class="box" bindtap="onSearching">
 4       <image src="/images/icon/search.png"></image>
 5       <text>搜索书籍</text>
 6     </view>
 7   </view>
 8   <view class="sub-container">
 9     <image src="/images/book/quality.png" class="head-img"></image>
10     <view class="books-container">
11       <block wx:key="id" wx:for="{{books}}">
12         <v-book book="{{item}}" />
13       </block>
14     </view>
15   </view>
16 </view>
17 <!-- search组件的使用 -->
18 <v-search bind:cancel="onCancel" wx:if="{{searching}}"></v-search>

book.js中添加部分处理方法

 1 // 添加searching属性  
 2 data: {
 3     // 服务器请求的数据 book的集合
 4     books:[],
 5     searching:false
 6   },
 7 
 8   // 搜索框的点击事件
 9   onSearching(event){
10     this.setData({
11       searching:true
12     })
13   },
14 
15   // 搜索框取消事件
16   onCancel(event){
17     this.setData({
18       searching:false
19     })
20   },

2、组件中的业务代码

 现在不知道从哪里开始写起了,昨天自己动手写了一下,简单的实现了搜索的功能,把前两天学习的内容简单的记录一下。

(1)搜索记录的标签显示

这个是分为两种搜索标签的,一种是历史搜索,一种是热门搜索,这两种实现方式是不同的,历史搜索是从缓存中加载保存的用户搜索记录,这个是有总数限制的,很值得学习一下这种方案的处理思路,热门搜索就是从服务器加载热门搜索记录,这个就有灰色空间了,如果数据量非常大的时候,这个时候会用到排序算法了,之前学习过,具体怎么实现,现在也是没有记住,总体的思路还是在大脑中有点的,看看这两种的实现:

 1   <view wx:if="{{!searching}}">
 2     <view class="history">
 3       <view class="title">
 4         <view class="chunk"></view>
 5         <text>历史搜索</text>
 6       </view>
 7       <view class="tags">
 8         <block wx:for="{{historyWords}}" wx:key="">
 9           <v-tag text="{{item}}" bind:tapping="onConfirm" />
10         </block>
11       </view>
12     </view>
13     <view class="history hot-search">
14       <view class="title">
15         <view class="chunk"></view>
16         <text>热门搜索</text>
17       </view>
18       <view class="tags">
19         <block wx:for="{{hotWords}}" wx:key="">
20           <v-tag text="{{item}}" bind:tapping="onConfirm" />
21         </block>
22       </view>
23     </view>
24   </view>

上面是页面展示的实现,下面看一下具体的逻辑实现:

这个是新建的keyword.js文件,在models文件夹下面,主要是有几个相关的方法,重点是关注一下addToHistroy方法的

 1 import {HTTP} from ‘../util/http-p.js‘
 2 class KeywordModel extends HTTP{ 
 3   key = "q"; // 缓存中的key
 4   maxLength = 10; // 历史搜索展示的条数
 5   // 获取历史搜索方法
 6   getHistory(){
 7     const words =  wx.getStorageSync(this.key);
 8     if(!words){
 9       return [];
10     }
11     return words;
12   }
13 
14   // 获取热门的方法 
15   getHot(){
16     return this.request({
17       url:‘/book/hot_keyword‘
18     })
19   }
20 
21   // 将搜索关键字写入缓存中 
22   addToHistory(keyword){
23     // 注意缓存中是一组数据
24     let words = this.getHistory(this.key);
25     const has = words.includes(keyword);
26     if(!has){
27       const length = words.length;
28       // 删除末尾的Word
29       if(length >= this.maxLength){
30         words.pop();
31       }
32       words.unshift(keyword);
33       wx.setStorageSync(this.key, words);
34     }
35   }
36 }
37 
38 export { KeywordModel }

下面是search组件中的index.js文件中的具体逻辑实现,主要就是在search组件加载的时候,初始化这个历史搜索与热门搜索的标签,这个是在attached函数中,这个attached方法是小程序中的默认的组件加载时执行的方法

 1   /**
 2    * 组件的初始数据
 3    */
 4   data: {
 5     historyWords:[],
 6     hotWords:[],
 7     dataArray:[],
 8     searching:false,
 9     q:""
10   },
11   
12 // 组件初始化时候调用的方法
13   attached(){
14     this.setData({
15       historyWords: keywordModel.getHistory()
16     })
17 
18     keywordModel.getHot().then(res => {
19       this.setData({
20         hotWords:res.hot
21       })
22     })
23   },

(2)书籍信息的显示

这个现在只是实现了书籍信息的简单展示,没有实现分页的操作,后续的会实现这个功能

1   <!-- 书籍展示 -->
2   <view class="books-container" wx:if="{{searching}}">
3     <block wx:for="{{dataArray}}" wx:key="{{item.id}}">
4       <v-book book="{{item}}" class="book"></v-book>
5     </block>
6   </view>

这个页面展示的代码就比较简单了,我们只是复用了一下book组件,所以这里实现起来就比较简单了,下面是逻辑代码,主要就是调用接口加载数据,还有就是对数据的一些处理,以及一些具体细节的处理,这个细节的处理很容易被忽视的,但是这些东西才是体现一个项目的好坏,一个开发者好坏的真正的东西

 1     // 用户搜索的方法
 2     onConfirm(event){
 3       this.setData({
 4         searching:true
 5       })
 6       const word = event.detail.value || event.detail.text;
 7       bookModel.search(0, word).then(res => {
 8         console.log(res);
 9         this.setData({
10           dataArray:res.books,
11           q: word
12         })
13         keywordModel.addToHistory(word);
14       })
15     },

 (3)搜索结果的分页加载

这个业务场景是当用户搜索结果展示出来的时候,之前只是展示若干条数据,无法全部显示搜索结果,这种做法当然无可厚非,但是我们要进一步完善这个功能,那就有必要来实现分页功能了,当用户下滑到底部的时候,如果还有数据,那么我们需要加载出来,那么这个如何实现,哈哈

具体思路:

(1)在page中小程序是有事件来实现这个下拉触发动作的,那就是onReachBottom事件,如何将这个动作的通知传递到组件中,让组件接收到这个通知,实现具体的逻辑

(2)可以通过组件的properties属性来传递这个通知,属性中监听函数observer来实现处理逻辑,这里observer监听 函数只有当属性值改变的时候才会触发,所以,我们的解决办法是每次传递一个随机数给属性,让每一次通知都能被组件接收

(3)剩下的就是具体的逻辑处理了,这里面有好多细节需要处理的,具体看代码

首先,看一下page中 book.wxml以及book.js中的代码

 1 // wxml中代码 简写省略其他
 2 <!-- search组件的使用 -->
 3 <v-search more="{{more}}" bind:cancel="onCancel" wx:if="{{searching}}"></v-search>
 4 
 5 // js中代码
 6   data: {
 7     // 服务器请求的数据 book的集合
 8     books:[],
 9     searching:false,
10     more:‘‘ // 是否加载更多数据
11   },
12 
13   onReachBottom: function() {
14     // console.log("aaaa");
15     // 加载更多数据
16     this.setData({
17       more:random(16)
18     })
19   },

这里有一个产生随机数的方法,random(16) 产生16位的随机数,很简单,不贴代码了

看一下search组件中的相关代码,主要是增加了一个属性,增加了loading ,这个充当的是锁的角色,这个方法还有待优化,

说一下这里面的细节处理:

loading这个锁的引入,防止用户下拉触发事件过于频繁,向服务器发送过多请求,导致的信息加载出现重叠的问题,影响服务器的性能,引入loading锁之后,只有一个请求发送完毕之后,接下来的请求才能继续发送,这个锁的概念在多线程中应用的很广泛,作为后端开发,这个问题很容易理解!

 1   properties: {
 2     more:{
 3       type:String,
 4       observer:‘_load_more‘
 5     }
 6   },
 7 
 8   data: {
 9     loading:false
10   },
11 
12     // 加载更多数据
13     _load_more(){
14       console.log(123123);
15       if(!this.data.q){
16         return;
17       }
18       // loading在这里扮演的是锁的角色
19       if(this.data.loading){
20         return;
21       }
22       const length = this.data.dataArray.length;
23       this.data.loading = true
24       bookModel.search(length,this.data.q).then(res => {
25         const tempArray = this.data.dataArray.concat(res.books);
26         this.setData({
27           dataArray:tempArray
28         })
29         this.data.loading = false
30       })
31     },

 (4)搜索代码的优化

这个优化主要是设计到两方面,一方面是代码的抽离,一方面是代码的可读性

先看看代码的抽离如何来优化,主要是将分页的相关的代码抽离成behavior行为,然后直接在组件中引用behavior中的方法,新建一个behaviors文件夹,创建一个pagination.js文件

看一下pagination.js中的代码:

 1 const paginationBev = Behavior({
 2   data: {
 3     dataArray: [], // 分页数据
 4     total: null
 5   },
 6   methods: {
 7     setMoreData(dataArray) {
 8       const tempArray = this.data.dataArray.concat(dataArray);
 9       this.setData({
10         dataArray: tempArray
11       })
12     },
13     // 获取当前开始的index值
14     getCurrentStart() {
15       return this.data.dataArray.length;
16     },
17 
18     setTotal(total) {
19       this.data.total = total;
20     },
21 
22     // 是否还有数据需要加载
23     hasMore() {
24       if (this.data.dataArray.length >= this.data.total) {
25         return false;
26       } else {
27         return true;
28       }
29     },
30     initialize(){
31       this.data.dataArray = [];
32       this.data.total = null;
33     }
34   }
35 })
36 
37 export {
38   paginationBev
39 }

看一下在组件中如何使用:

  1 import {
  2   KeywordModel
  3 } from ‘../../models/keyword.js‘
  4 
  5 import {
  6   BookModel
  7 } from ‘../../models/book.js‘
  8 
  9 import {
 10   paginationBev
 11 } from ‘../behaviors/pagination.js‘
 12 
 13 Component({
 14   // 引入 组件中behaviors属性 
 15   behaviors: [paginationBev],
 16   /**
 17    * 组件的属性列表
 18    */
 19   properties: {
 20     more: {
 21       type: String,
 22       observer: ‘loadMore‘
 23     }
 24   },
 25 
 26   /**
 27    * 组件的方法列表
 28    */
 29   methods: {
 30     // 加载更多数据
 31     loadMore() {
 32       if (!this.data.q) {
 33         return;
 34       }
 35       // loading在这里扮演的是锁的角色
 36       if (this._isLocked()) {
 37         return;
 38       }
 39       
 40       if (this.hasMore()){
 41         this._locked();
 42         bookModel.search(this.getCurrentStart(), this.data.q).then(res => {
 43           this.setMoreData(res.books);
 44           this._unLocked();  
 45         },()=>{
 46           // 避免死锁 在请求失败的时候也需要释放锁
 47           this._unLocked();
 48         })
 49       }
 50     },
 51     // 搜索取消事件
 52     onCancel(event) {
 53       this.triggerEvent(‘cancel‘, {}, {});
 54     },
 55     // 用户搜索的方法
 56     onConfirm(event) {
 57       // 控制搜索结果的显示
 58       this._showResult();
 59       // 初始化behavior中的数据
 60       this.initialize();
 61       const word = event.detail.value || event.detail.text;
 62       bookModel.search(0, word).then(res => {
 63         this.setMoreData(res.books);
 64         this.setTotal(res.total);
 65         this.setData({
 66           q: word
 67         })
 68         keywordModel.addToHistory(word);
 69       })
 70     },
 71     // X的图标取消事件
 72     onDelete(event) {
 73       this._closeResult();
 74     },
 75     // 显示搜索结果
 76     _showResult(){
 77       this.setData({
 78         searching: true
 79       })
 80     },
 81     // 隐藏搜索结果
 82     _closeResult(){
 83       this.setData({
 84         searching: false
 85       })
 86     },
 87     // 判断是否有锁
 88     _isLocked(){
 89       this.data.loading?true:false;
 90     },
 91     // 加锁
 92     _locked(){
 93       this.data.loading = true;
 94     },
 95     // 释放锁
 96     _unLocked(){
 97       this.data.loading = false;
 98     }
 99   },
100 })

注意:带有下划线的方法是理论上的私有方法,姑且这么说吧,其实本质上和其他方法是一致的,这些方法的优化是增加代码的可读性,使得代码更加容易让人理解,这里其实由很多细节需要注意的,包括锁,为了避免死锁,需要在请求失败的时候同时将锁释放,以及confirm方法中主要将之前的数据清空,否则会造成dataArray中数据是重复数据,还有就是在加锁的时候需要在判断是否还有更多数据之后进行,如果在这之前进行,那么会造成数据不会加载的情况,等等,之后会完善一下加载图标,哈哈,感觉越来越完美

 (5)loading组件的开发与应用

这个就直接从网上找一个loading图标的样式就行,看看loading组件的代码

 1 // index.wxml代码
 2 <view class="spinner">
 3   <view class="double-bounce1"></view>
 4   <view class="double-bounce2"></view>
 5 </view>
 6 
 7 // 样式代码 index.wxss
 8 .spinner {
 9   width: 40rpx;
10   height: 40rpx;
11   position: relative;
12   /* margin: 100px auto; */
13 }
14  
15 .double-bounce1, .double-bounce2 {
16   width: 100%;
17   height: 100%;
18   border-radius: 50%;
19   background-color: #3063b2;
20   opacity: 0.6;
21   position: absolute;
22   top: 0;
23   left: 0;
24    
25   -webkit-animation: bounce 2.0s infinite ease-in-out;
26   animation: bounce 2.0s infinite ease-in-out;
27 }
28  
29 .double-bounce2 {
30   -webkit-animation-delay: -1.0s;
31   animation-delay: -1.0s;
32 }
33  
34 @-webkit-keyframes bounce {
35   0%, 100% { -webkit-transform: scale(0.0) }
36   50% { -webkit-transform: scale(1.0) }
37 }
38  
39 @keyframes bounce {
40   0%, 100% {
41     transform: scale(0.0);
42     -webkit-transform: scale(0.0);
43   } 50% {
44     transform: scale(1.0);
45     -webkit-transform: scale(1.0);
46   }
47 }

看一下loading组件的应用:

主要是在搜索结果展示之前,以及加载更多的时候进行loading组件的显示,在其他时候是无需显示的

1   <!-- loading图标显示 -->
2   <v-loading class="loading-center" wx:if="{{loadingCenter}}" />
3   <v-loading class="loading" wx:if="{{loading}}" />

看一下如何控制显示隐藏的

loadingCenter主要是在onConfirm方法中进行控制的,这个不多说,看一下loading的控制,就是在加锁和释放锁的时候进行控制就行了

 1     // 加锁
 2     locked() {
 3       // this.data.loading = true;
 4       this.setData({
 5         loading: true
 6       })
 7     },
 8     // 释放锁
 9     unLocked() {
10       // this.data.loading = false;
11       this.setData({
12         loading: false
13       })
14     }

这里还有代码的优化,以及在没有搜索结果的时候进行友好的提示,以及在没有更多的数据的时候进行友好的提示,以及在取消的时候进行数据的初始化操作,很多细节的东西,这里就不想写了,很琐碎的东西,但是在我看来是很值得付出时间去完善的一部分,细节决定成败,大家同样是一个功能,最能看出一个人水平的是谁能把细节注意到,并且能够做好,在以后的工作中,这是自己需要提升的一个领域,专注细节,布局整个系统

微信小程序之组件的集合(五)

上一篇:本地存储模块storage.js


下一篇:小程序框架 列表渲染