这个是学习复杂的组件的封装的,在课程中,主要实现的是书单上方的搜索功能组件的开发,这个应该是较之前的组件是有一定难度的,但是现在学到现在,感觉前端的内容和后端的内容比较起来,还是比较容易的,而且好多内容,其实在后端的开发中是很成熟的,所以学起来并不是很难以理解,这也是我们的一个优势吧,毕竟选择后端的同学应该是不错的啊,哈哈哈!这种组件其实是有一个特别的名字的,那就是高阶组件,来,把这个东西尽快学习掌握!
一、新建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 }
这里还有代码的优化,以及在没有搜索结果的时候进行友好的提示,以及在没有更多的数据的时候进行友好的提示,以及在取消的时候进行数据的初始化操作,很多细节的东西,这里就不想写了,很琐碎的东西,但是在我看来是很值得付出时间去完善的一部分,细节决定成败,大家同样是一个功能,最能看出一个人水平的是谁能把细节注意到,并且能够做好,在以后的工作中,这是自己需要提升的一个领域,专注细节,布局整个系统