乐趣区

企业级微信小程序实战详解

项目地址:https://github.com/wudiufo/We…

完成效果展示:https://www.bilibili.com/vide…

小爱心是否点赞组件 components/like

思路:
like 默认为 false,显示空心小爱心
触摸执行 tap:onLike 方法,因为 this.setData({count:count,like:!like})是异步的,先执行 count = like ? count – 1 : count + 1, 这时 like 还是 false,执行 count+1。然后在执行 this.setData()方法,将 like 变为 true,显示实心小爱心。
let behavior = like ? ‘like’ : ‘cancel’
// 自定义事件
this.triggerEvent(‘like’, {
behavior: behavior
}, {})
自定义事件 like,当 like 为真时,behavior 为 like,在 models/like.js 中,let url = behavior === ‘like’ ? ‘like/cancel’ : ‘like’, 因为 behavior === ‘like’ 为真,就调用服务器接口 ’like/cancel’,相反就调用 like 接口。
刚开始实心就调用 ’like/cancel’ 接口,空心就调用 ’like’ 接口

底部左右切换组件 components/navi

思路:
在 navi/index.js 中:
先定义哪些数据是外部传来的数据,哪些数据是私有数据
properties: {// 外部传来的数据
title: String,
first: Boolean, // 如果是第一期向右的箭头就禁用,默认是 false
latest: Boolean // 如果是最新的一期向左的箭头就禁用,默认是 false
},
data: {// 私有数据
disLeftSrc: ‘./images/triangle.dis@left.png’,
leftSrc: ‘./images/triangle@left.png’,
disRightSrc: ‘./images/triangle.dis@right.png’,
rightSrc: ‘./images/triangle@right.png’
},

左箭头:

在 navi/index.wxml 中 <image bind:tap=”onLeft” class=”icon” src=”{{latest?disLeftSrc:leftSrc}}”/>
src 显示图片规则:如果是最新的期刊,就显示向左禁用状态 disLeftSrc 箭头;如果不是最新一期的期刊,就显示向左可用状态 leftSrc 箭头
为图片绑定触摸事件 onLeft,在 navi/index.js 中:
在 methods 中:如果不是最新的期刊,就继续绑定自定义事件 left
onLeft: function(event) {// 不是最新一期
if (!this.properties.latest) {
this.triggerEvent(‘left’, {}, {})
}

},

右箭头:

在 navi/index.wxml 中 <image bind:tap=”onRight” class=”icon” src=”{{first?disRightSrc:rightSrc}}”/>
src 显示图片规则:如果是第一期的期刊,就显示向右禁用状态 disRightSrc 箭头;如果不是第一期的期刊,就显示向右可用状态 rightSrc 箭头
为图片绑定触摸事件 onRight,在 navi/index.js 中:
在 methods 中:如果不是第一期的期刊,就继续绑定自定义事件 right
onRight: function(event) {// 不是第一期
if (!this.properties.first) {
this.triggerEvent(‘right’, {}, {})
}

}

在 pages/classic 中:
1: 在 classic.json 中,注册使用 navi 自定义组件
{
“usingComponents”: {
“v-like”: “/components/like/index”,
“v-movie”: “/components/classic/movie/index”,
“v-episode”: “/components/episode/index”,
“v-navi”: “/components/navi/index”
}
}
2: 在 classic.wxml 中:绑定自定义事件 left,获取当前一期的下一期;绑定自定义事件 right,获取当前一期的上一期
<v-navi bind:left=”onNext” bind:right=”onPrevious” class=”nav” title=”{{classic.title}}” first=”{{first}}” latest=”{{latest}}”/>

3:在 classic.js 中:
// 获取当前一期的下一期,左箭头
onNext: function(evevt) {this._updateClassic(‘next’)
},
// 获取当前一期的上一期,右箭头
onPrevious: function(evevt) {this._updateClassic(‘previous’)
},
// 重复代码过多,利用函数封装的思想,新建一个函数抽取公共代码
// 发送请求,获取当前页的索引,更新数据
_updateClassic: function(nextOrPrevious) {
let index = this.data.classic.index
classicModel.getClassic(index, nextOrPrevious, (res) => {
// console.log(res)
this.setData({
classic: res,
latest: classicModel.isLatest(res.index),
first: classicModel.isFirst(res.index)
})
})
},

4:在 models/classic.js 中:
// 当前的期刊是否为第一期,first 就变为 true,右箭头就显示禁用
isFirst(index) {
return index === 1 ? true : false
}
// 当前的期刊是否为最新的一期,latest 就变为 TRUE,左箭头就显示禁用
// 由于服务器数据还会更新,确定不了最新期刊的索引,所以就要利用缓存机制,将最新期刊的索引存入到缓存中,如果外界传进来的索引和缓存的最新期刊的索一样,latest 就变为 TRUE,左箭头就显示禁用
isLatest(index) {
let latestIndex = this._getLatestIndex()
return latestIndex === index ? true : false
}
// 将最新的期刊 index 存入缓存
_setLatestIndex(index) {
wx.setStorageSync(‘latest’, index)
}

// 在缓存中获取最新期刊的 index
_getLatestIndex() {
let index = wx.getStorageSync(‘latest’)
return index
}
优化缓存。解决每次触摸左右箭头都会频繁向服务器发送请求,这样非常耗性能,用户体验极差。解决方法,就是把第一次发送请求的数据都缓存到本地,再次触摸箭头时,会先查找本地缓存是否有数据,有就直接从缓存中读取数据,没有就在向服务器发送请求,这样利用缓存机制大大的提高了用户的体验。(但也有一部分是需要实时更新的,比如是否点赞的小爱心组件,需要每次都向服务器发送请求获取最新数据)
在 models/classic.js 中:
1:
// 设置缓存中的 key 的样式,classic- 1 这种样式
_getKey(index) {
let key = `classic-${index}`
return key
}
2:
// 因为 getPrevious,getNext 实现代码相似,所以为了简化代码可以合并为一个函数
// 缓存思路:在缓存中寻找 key,找不到就发送请求 API,将 key 写入到缓存中。解决每次都调用 Api 向服务器发请求,耗费性能
// 在缓存中,确定 key
getClassic(index, nextOrPrevious, sCallback) {
//0: 是 next,触摸向左箭头获取下一期,触摸向右箭头否则获取上一期
let key = nextOrPrevious === ‘next’ ? this._getKey(index + 1) : this._getKey(index – 1)
//1: 在缓存中寻找 key
let classic = wx.getStorageSync(key)
//2: 如果缓存中找不到 key, 就调用服务器 API 发送请求获取数据
if (!classic) {
this.request({
url: `classic/${index}/${nextOrPrevious}`,
success: (res) => {
// 将获取到的数据设置到缓存中
wx.setStorageSync(this._getKey(res.index), res)
// 再把获取到的数据返回,供用户调取使用
sCallback(res)
}
})
} else {//3:如果在缓存中有找到 key,将缓存中 key 对应的 value 值,返回给用户,供用户调取使用
sCallback(classic)
}

}
——————————————————————————–

// 获取最新的期刊利用缓存机制进一步优化
// 获取最新的期刊
getLatest(cb) {
this.request({
url: ‘classic/latest’,
success: (res) => {
// 将最新的期刊 index 存入缓存, 防止触摸向左箭头时,没有设置 latest 的值,左箭头会一直触发发送请求找不到最新的期刊报错
this._setLatestIndex(res.index)
// 再把获取到的数据返回,供用户调取使用
cb(res)
// 将最新的期刊设置到缓存中,先调取 this._getKey() 方法,为最新获取的期刊设置 key 值,调用微信设置缓存方法将 key,和对应的 value 值 res 存进去
let key = this._getKey(res.index)
wx.setStorageSync(key, res)

}
})
}
处理是否点赞小爱心组件的缓存问题:他不需要缓存,需要实时获取最新数据
在 models/like.js 中:
// 编写一个获取点赞信息的方法,从服务器获取最新点赞信息的数据
// 获取点赞信息
getClassicLikeStatus(artID, category, cb) {
this.request({
url: `classic/${category}/${artID}/favor`,
success: cb
})
}

在 pages/classic/classic.js 中:
// 设置私有数据初始值
data: {
classic: null,
latest: true,
first: false,
likeCount: 0,// 点赞的数量
likeStatus: false // 点赞的状态

},

// 在 classic.wxml 中:<v-like class=”like” bind:like=”onLike” like=”{{likeStatus}}” count=”{{likeCount}}”/>

// 编写一个私有方法获取点赞信息
// 获取点赞信息
_getLikeStatus: function(artID, category) {
likeModel.getClassicLikeStatus(artID, category, (res) => {
this.setData({
likeCount: res.fav_nums,
likeStatus: res.like_status
})
})
},

// 生命周期函数 – 监听页面加载
onLoad: function(options) {
classicModel.getLatest((res) => {
console.log(res)
// this._getLikeStatus(res.id, res.type) // 不能这样写,会多发一次 favor 请求,消耗性能
this.setData({
classic: res,
likeCount: res.fav_nums,
likeStatus: res.like_status
})
})

在 classic/music/index.js 中:
解决切换期刊时,其他期刊也都是播放状态的问题。应该是,切换期刊时音乐就停止播放,回到默认不播放状态
利用组件事件的通信机制,小程序中只有父子组件
在 components/classic/music/inddex.js 中:
方案一:

// 利用组件生命周期,只有 wx:if 才可以从头掉起组件生命周期
// 组件卸载的生命周期函数
// 组件卸载音乐停止播放,但这时不生效是因为,在 classic.wxml 中用的是 hidden,应改为 if
detached: function(event) {
mMgr.stop()
},
// 在 pages/classic/classic.wxml 中
// <v-music wx:if=”{{classic.type===200}}” img=”{{classic.image}}” content=”{{classic.content}}” src=”{{classic.url}}” title=”{{classic.title}}”/>

知识点补充:
wx:if vs hidden,和 Vue 框架的 v -if 和 v -show 指令一样:wx:if》他是惰性的,如果初始值为 false 框架什么也不做,如果初始值为 true 框架才会局部渲染。true 或 false 的切换就是从页面中局部加入或移除的过程。wx:if 有更高的切换消耗,如果在运行时条件不大可能改变则 wx:if 较好。生命周期会重新执行。hidden》组件始终会被渲染,只是简单的控制显示与隐藏。hidden 有更高的初始渲染消耗。如果需要频繁切换的情景下,用 hidden 更好。生命周期不会重新执行。
方案二:(推荐使用)
解决切换期刊时音乐可以当做背景音乐一直播放,而其他的期刊是默认是不播放状态
在 components/classic/music/inddex.js 中:
// 为了保证期刊在切换时,背景音乐可以一直播放,就要去除掉 mMgr.stop() 事件方法
detached: function(event) {
// mMgr.stop() // 为了保证背景音乐的持续播放就不能加 stop
},

// 监听音乐的播放状态,如果当前页面没有播放的音乐,就设置 playing 为 false。如果当前页面的音乐地址 classic.url 和当前正在播放的音乐的地址一样,就让播放状态为 true
_recoverStatus: function() {
if (mMgr.paused) {
this.setData({
playing: false
})
return
}
if (mMgr.src === this.properties.src) {

this.setData({
playing: true
})

}
},

// 监听播放状态,总控开关就可以控制播放状态,结局总控开关和页面不同步问题
_monitorSwitch: function() {
console.log(‘monitorSwitch 背景音频 ’, ‘ 触发 3 ’)
// 监听背景音频播放事件
mMgr.onPlay(() => {
this._recoverStatus()
console.log(‘onPlay ‘ + this.data.playing)
})
// 监听背景音频暂停事件
mMgr.onPause(() => {
this._recoverStatus()
console.log(‘onPause ‘ + this.data.playing)
})
// 关闭音乐控制台,监听背景音频停止事件
mMgr.onStop(() => {
this._recoverStatus()
console.log(‘onStop ‘ + this.data.playing)
})
// 监听背景音频自然播放结束事件
mMgr.onEnded(() => {
this._recoverStatus()
console.log(‘onEnded ‘ + this.data.playing)
})
},

// 调用生命周期函数,每次切换都会触发 attached 生命周期
// 在组件实例进入页面节点树时执行
// hidden,ready,created 都触发不了生命周期函数
attached: function(event) {
console.log(‘attach 实例进入页面 ’, ‘ 触发 1 ′)
this._monitorSwitch()
this._recoverStatus()

},
播放动画旋转效果制作:
在 components/classic/music/index.wxss 中:
// 定义帧动画用 CSS3
.rotation {
-webkit-transform: rotate(360deg);
animation: rotation 12s linear infinite;
-moz-animation: rotation 12s linear infinite;
-webkit-animation: rotation 12s linear infinite;
-o-animation: rotation 12s linear infinite;
}

@-webkit-keyframes rotation {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}

补充 css3 知识点:
》使用 CSS3 开启 GPU 硬件加速提升网站动画渲染性能:为动画 DOM 元素添加 CSS3 样式 -webkit-transform:transition3d(0,0,0)或 -webkit-transform:translateZ(0);,这两个属性都会开启 GPU 硬件加速模式,从而让浏览器在渲染动画时从 CPU 转向 GPU,其实说白了这是一个小伎俩,也可以算是一个 Hack,-webkit-transform:transition3d 和 -webkit-transform:translateZ 其实是为了渲染 3D 样式,但我们设置值为 0 后,并没有真正使用 3D 效果,但浏览器却因此开启了 GPU 硬件加速模式。》这种 GPU 硬件加速在当今 PC 机及移动设备上都已普及,在移动端的性能提升是相当显著地,所以建议大家在做动画时可以尝试一下开启 GPU 硬件加速。》适用情况通过 -webkit-transform:transition3d/translateZ 开启 GPU 硬件加速的适用范围:
使用很多大尺寸图片 (尤其是 PNG24 图) 进行动画的页面。页面有很多大尺寸图片并且进行了 css 缩放处理,页面可以滚动时。使用 background-size:cover 设置大尺寸背景图,并且页面可以滚动时。(详见:https://coderwall.com/p/j5udlw) 编写大量 DOM 元素进行 CSS3 动画时(transition/transform/keyframes/absTop&Left) 使用很多 PNG 图片拼接成 CSS Sprite 时》总结 通过开启 GPU 硬件加速虽然可以提升动画渲染性能或解决一些棘手问题,但使用仍需谨慎,使用前一定要进行严谨的测试,否则它反而会大量占用浏览网页用户的系统资源,尤其是在移动端,肆无忌惮的开启 GPU 硬件加速会导致大量消耗设备电量,降低电池寿命等问题。

在 components/classic/music/index.wxml 中:
// 为图片加上播放就旋转的类,不播放 就就为空字符串
<image class=”classic-img {{playing?’rotation’:”}}” src=”{{img}}”></image>

用 slot 插槽,解决在公用组件中可以加入其他修饰内容问题。其实就是,在定义公用组件时,用 slot 命名插槽占位,在父组件调用时可以传递需要的内容补位。和 Vue 的指令 v-slot 相似。
在 components/tag/index.js 中:
// 在 Component 中加入
// 启用 slot
options: {
multipleSlots: true
},
在定义的公共组件 components/tag/index.wxml 中:
// 定义几个命名插槽,供父元素占位使用
<view class=”container tag-class”>
<slot name=”before”></slot>
<text>{{text}}</text>
<slot name=”after”></slot>
</view>
在 pages/detail/detail.json 中:
// 注册并使用组件
{
“usingComponents”: {
“v-tag”: “/components/tag/index”
}
}
在 pages/detail/detail.wxml 中:
// 使用组件 v -tag,补位命名插槽
<v-tag tag-class=”{{index===0?’ex-tag1′:”||index===1?’ex-tag2′:”}}” text=”{{item.content}}”>
<text class=”num” slot=”after”>{{‘+’+item.nums}}</text>
</v-tag>

在 pages/detail/detail 中,解决评论内容自定义组件 v-tag 评论前两条显示两种颜色的做法:
第一种方法:(推荐使用)
在 pages/detail/detail.wxss 中:
/* v-tag 是自定义组件,不能使用 css3, 在微信小程序中,只有内置组件才可以用 css3 */
/* 用 CSS hack 方式给自定义组件加样式 */
.comment-container>v-tag:nth-child(1)>view {
background-color: #fffbdd;
}

.comment-container>v-tag:nth-child(2)>view {
background-color: #eefbff;
}
第二种方法:
定义外部样式方法,像父子组件传递属性一样,传递样式类
在 detail.wxss 中:
/* 定义外部样式 */

.ex-tag1 {
background-color: #fffbdd !important;
}

.ex-tag2 {
background-color: #eefbff !important;
}
在 detail.wxml 中:
/* 将自定义的样式类通过属性传值的方式传递给自定义子组件 v -tag */
<v-tag tag-class=”{{index===0?’ex-tag1′:”||index===1?’ex-tag2’:”}}” text=”{{item.content}}”>
<text class=”num” slot=”after”>{{‘+’+item.nums}}</text>
</v-tag>
在 components/tag/index.js 中:
// 将外部传进来的样式写在 Component 中,声明一下
// 外部传进来的 css, 样式
externalClasses: [‘tag-class’],

在 components/tag/index.wxml 中:
// 把父组件传递过来的类 tag-calss 写在 class 类上
<view class=”container tag-class”>
<slot name=”before”></slot>
<text>{{text}}</text>
<slot name=”after”></slot>
</view>

解决服务器返回的内容简介有 n 换行符的问题:
原因:
是因为服务器返回的原始数据 是 \\n , 经过转义就变成 \n 而 \n 在 text 文本标签中默认转义为换行

解决方法:
WXS:WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。和 Vue 中的 Vue.filter(过滤器名,过滤器方法) 很相似。WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。
在 utils/filter.wxs 中:
// 定义过滤器函数,处理服务器返回的数据,将 \\n 变成 \n
// 会打印两次,undefined 和请求得到的数据,因为第一次初始时 text 为 null,发送请求得到数据后调用 setData 更新数据一次
var format = function(text) {
console.log(text)

if (!text) {
return
}
var reg = getRegExp(‘\\\\n’, ‘g’)
return text.replace(reg, ‘\n’)
}

module.exports.format = format
在 pages/detail/detail.wxml 中:
// 引入
<wxs src=”../../utils/filter.wxs” module=”util”/>
// 在需要过滤的数据中使用
<text class=”content”>{{util.format(book.summary)}}</text>

解决解决服务器返回的内容简介首行缩进的问题:
在 pages/detail/detail.wxss 中:
// 对需要缩进的段落前加以下的类,但这时只有第一段缩进
.content {
text-indent: 58rpx;
font-weight: 500;
}
在 utils/filter.wxs 中:
// 用转义字符 &nbsp; 作为空格,但这时小程序会以 &nbsp; 样式输出,不是我们想要的效果
var format = function(text) {
if (!text) {
return
}
var reg = getRegExp(‘\\\\n’, ‘g’)
return text.replace(reg, ‘\n&nbsp;&nbsp;&nbsp;&nbsp;’)
}

module.exports.format = format
在 pages/detail/detail.wxml 中:
// 加入属性 decode=”{{true}}”,首行缩进问题解决
<text class=”content” decode=”{{true}}”>{{util.format(book.summary)}}</text>

解决短评过多让其只显示一部分的问题:
在 utils/filter.wxs 中:
// 添加一个限制短评长度的过滤器,并导出
// 限制短评的长度的过滤器
var limit = function(array, length) {
return array.slice(0, length)
}

module.exports = {
format: format,
limit: limit
};
在 pages/detail/detail.wxml 中:
<wxs src=”../../utils/filter.wxs” module=”util”/>

<view class=”sub-container”>
<text class=”headline”> 短评 </text>
<view class=”comment-container”>
<block wx:for=”{{util.limit(comments,10)}}” wx:key=”content”>
<v-tag tag-class=”{{index===0?’ex-tag1′:”||index===1?’ex-tag2′:”}}” text=”{{item.content}}”>
<text class=”num” slot=”after”>{{‘+’+item.nums}}</text>
</v-tag>
</block>
</view>
</view>

在 pages/detail/detail.wxml 中:进一步优化
// 由于 <v-tag tag-class=”{{index===0?’ex-tag1′:”||index===1?’ex-tag2’:”}}” text=”{{item.content}}”> 过于乱,改写成 wxs 形式:

// 先定义 wxs 过滤器
<wxs module=”tool”>
var highlight = function(index){
if(index===0){
return ‘ex-tag1’
}
else if(index===1){
return ‘ex-tag2’
}
return ”
}
module.exports={
highlight:highlight
}
</wxs>

// 改写为:
<v-tag tag-class=”{{tool.highlight(index)}}” text=”{{item.content}}”>

详情最底部短评的实现:
用户提交评论内容:
点击标签向服务器提交评论内容:
在 componentstagindex.wxml 中:
// 为短评组件绑定出没事件 onTap
<view bind:tap=”onTap” class=”container tag-class”>
<slot name=”before”></slot>
<text>{{text}}</text>
<slot name=”after”></slot>
</view>
在 componentstagindex.js 中:
// 当触摸短评小标签时,触发一个自定义事件,将短评内容传进去,公父组件调用自定义事件 tapping
methods: {
// 触摸短评小标签时,触发的事件,触发一个自定义事件
onTap(event) {
this.triggerEvent(‘tapping’, {
text: this.properties.text
})
}
}
在 pagesdetaildetail.wxml 中:
// 在父组件中调用子组件的自定义 tapping 事件,并且触发事件 onPost
<v-tag bind:tapping=”onPost” tag-class=”{{tool.highlight(index)}}” text=”{{item.content}}”>
<text class=”num” slot=”after”>{{‘+’+item.nums}}</text>
</v-tag>
在 modelsbook.js 中:
// 调取新增短评的接口
// 新增短评
postComment(bid, comment) {
return this.request({
url: ‘/book/add/short_comment’,
method: ‘POST’,
data: {
book_id: bid,
content: comment
}
})
}
在 pagesdetaildetail.js 中:
// 触摸 tag 组件会触发,input 输入框也会触发事件 onPost
// 获取用户的输入内容或触发 tag 里的内容, 并且对用户输入的评论做校验,如果评论的内容长度大于 12 就弹出警告不向服务器发送请求
// 如果评论内容符合规范,就调用新增短评接口并将最新的评论插到 comments 数组的第一位,更新数据,并且把蒙层 mask 关闭
onPost(event) {
// 获取触发 tag 里的内容
const comment = event.detail.text
// 对用户输入的评论做校验
if (comment.length > 12) {
wx.showToast({
title: ‘ 短评最多 12 个字 ’,
icon: ‘none’
})
return
}

// 调用新增短评接口并将最新的评论插到 comments 数组的第一位
bookModel.postComment(this.data.book.id, comment).then(res => {
wx.showToast({
title: ‘+1’,
icon: ‘none’
})
this.data.comments.unshift({
content: comment,
nums: 1 // 这是后面的数量显示
})
this.setData({
comments: this.data.comments,
posting: false
})
})
},

在 pagesdetaildetail.wxml 中:
// input 有自己的绑定事件 bindconfirm,会调用手机键盘完成按键
<input bindconfirm=”onPost” type=”text” class=”post” placeholder=” 短评最多 12 个字 ”/>
在 pagesdetaildetail.js 中:
点击标签向服务器提交评论内容完成:
// 触摸 tag 组件会触发,input 输入框也会触发事件 onPost 事件;然后获取触发 tag 里的内容或获取用户 input 输入的内容;对 tag 里的内容或对用户输入的评论做校验并且输入的内容不能为空;最后调用新增短评接口并将最新的评论插到 comments 数组的第一位, 并且把蒙层 mask 关闭

onPost(event) {
const comment = event.detail.text||event.detail.value
console.log(‘comment’+comment)
console.log(‘commentInput’+comment)
if (comment.length > 12|| !comment) {
wx.showToast({
title: ‘ 短评最多 12 个字 ’,
icon: ‘none’
})
return
}
bookModel.postComment(this.data.book.id, comment).then(res => {
wx.showToast({
title: ‘+1’,
icon: ‘none’
})
this.data.comments.unshift({
content: comment,
nums: 1 // 这是后面的数量显示
})
this.setData({
comments: this.data.comments,
posting: false
})
})
},
细节处理:
如果没有短评显示问题:
在 pagesdetaildetail.wxml 中:
// 在短评后加上还没有短评标签,如果没有 comments 短评就不显示还没有短评标签
<text class=”headline”> 短评 </text>
<text class=”shadow” wx:if=”{{!comments.length}}”> 还没有短评 </text>

// 在尽可点击标签 + 1 后加上暂无评论标签,如果没有 comments 短评就不显示暂无评论标签
<text wx:if=”{{!comments.length}}”> 尽可点击标签 +1</text>
<text wx:else> 暂无短评 </text>

由于需要加载的数据较多,为了提高用户体验,需要加一个 loading 提示数据正在加载中,数据加载完成后就消失;
由于都是利用 promise 异步加载数据,这时取消 loading 显示应该加到每个 promise 后,显然不符合需求。如果利用回调函数机制,先加载 1 在一的回调函数里在加载 2 依次顺序加载,在最后一个回调函数中写取消 loading 操作,这样的方式虽然可以实现,但非常耗时间,请求是串行的,假如一个请求需要花费 2s 中,发三个请求就要花费 6 秒,非常耗时,而且还会出现回调地狱的现象,不推荐使用。
解决方法:在 Promise 中,有一个 Promise.all()方法就可以解决。
补充知识点:

Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。简单来说就是:只要有一个数组里的 promise 获取失败就调用 reject 回调,只有全部数组里的 promise 都成功才调用 resolve 回调。Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。race 函数返回一个 Promise,它将与第一个传递的 promise 相同的完成方式被完成。它可以是完成(resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。如果传的迭代是空的,则返回的 promise 将永远等待。如果迭代包含一个或多个非承诺值和 / 或已解决 / 拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。简单来说就是:不论成功还是失败的回调,哪一个快就执行哪个。

在 pagesdetaildetail.js 中:
// 用了 Promise.all(iterable) 方法就不用写三个 Promise 方法分别来更新数据了,可以简写成一个 all 方法再返回的成功的 promise 中调用 setData(), 更新请求回的数据
onLoad: function(options) {
// 数据加载时显示 loading 效果
wx.showLoading()
const bid = options.bid
console.log(bid)
// 获取书籍详细信息
const detail = bookModel.getDetail(bid)
// 获取书籍点赞情况
const likeStatus = bookModel.getLikeStatus(bid)
// 获取书籍短评
const comments = bookModel.getComments(bid)

// 数据加载完成时取消显示 loading 效果
Promise.all([detail, comments, likeStatus]).then(res => {
console.log(res)
this.setData({
book: res[0],
comments: res[1].comments,
likeStatus: res[2].like_status,
likeCount: res[2].fav_nums
})
wx.hideLoading()
})

},

图书的搜索:
高阶组件:如果一个组件里面的内容比较复杂,包含大量的业务
知识点补充:
工作中我们通常把业务处理逻辑写在 models 中:可以写在单个公用组件里,只供自己写业务逻辑调取使用;
可以写在 components 中,只供 components 内的组件调取使用,如果想把 components 发布出去给其他项目用或者提供给其他开发者使用;
可以写在项目根目录 models 下,供整个项目调取使用写业务逻辑,如果只是做个项目建议写在这里不会乱。

在 componentssearchindex 中:
处理历史搜索和热门搜索:
历史搜索:将历史搜索关键字写入缓存中,在从缓存中获取历史搜索关键字。热门搜索:调取服务器 API
GET /book/hot_list

将业务逻辑写在 modelskeyword.js 中:
// 首先从缓存中获取历史搜索关键字数组,判断获取的数组是否为空,如果为空,为了防止报错就返回空数组;如果不为空就直接返回获取的数组。
// 将搜索关键字写入缓存中,先从缓存中获取历史关键字的数组,判断是否包含此次输入的关键字,如果没有此关键字,如果获取的长度大于最大长度,就将数组的最后一项删除;如果获取数组的长度小于最大长度,就将此次输入的关键字加到数组的第一位,并且设置到缓存中。
class KeywordModel {
constructor() {
// 把 key 属性挂载到当前实例上,供实例调取使用
this.key = ‘q’,
this.maxLength = 10 // 搜索关键字的数组最大长度为 10
}

// 从缓存中,获取历史搜索关键字数组,如果缓存中没有直接返回空数组
getHistory() {
const words = wx.getStorageSync(this.key)
if (!words) {
return []
}
return words
}

// 将历史搜索关键字写入缓存中。先从缓存中获取历史关键字的数组,判断数组中是否已经有此关键字。如果没有,并且获取数组的长度大于最大长度,就将数组最后一项删除。获取数组的长度小于最大长度就将此次输入的关键字加到数组第一位,并且设置到缓存中;
addToHistory(keyword) {
let words = this.getHistory()
const has = words.includes(keyword)
if (!has) {
const length = words.length
if (length >= this.maxLength) {
words.pop()
}
words.unshift(keyword)
wx.setStorageSync(this.key, words)
}

}

// 获取热门搜素搜关键字
getHot() {

}
}

export {KeywordModel}
在 componentssearchindex.wxml 中:
// 为 input 输入框绑定 onConfirm 事件
<input bind:confirm=”onConfirm” type=”text” class=”bar” placeholder-class=”in-bar” placeholder=” 书籍名 ” auto-focus=”true”/>
在 componentssearchindex.js 中:
// onConfirm 事件执行,调用将输入的内容添加到缓存中的方法 Keywordmodel.addToHistory(word),就可以将历史关键字添加到缓存中
methods: {
// 点击取消将搜索组件关掉,有两种方法:一是,在自己的内部创建一个变量控制显隐,不推荐,因为万一还有其他操作扩展性不好。二是,创建一个自定义事件,将自定义事件传给父级,让父级触发
onCancel(event) {
this.triggerEvent(‘cancel’, {}, {})
},

// 在 input 输入框输入完成将输入的内容加到缓存中
onConfirm(event) {
const word = event.detail.value
Keywordmodel.addToHistory(word)
}
}
在 componentssearchindex.js 中:
// 将历史搜索的内容从缓存中取出来
data: {
historyWords: [] // 历史搜索关键字
},

// 组件初始化时,小程序默认调用的生命周期函数
attached() {
const historywords = Keywordmodel.getHistory()
this.setData({
historyWords: historywords
})
},
在 componentssearchindex.json 中:
// 注册引用小标签 tag 组件,组件中也可以引入其他组件
“usingComponents”: {
“v-tag”: “/components/tag/index”
}
在 componentssearchindex.wxml 中:
// 遍历 historyWords 数组中的每一项,呈现在页面中
<view class=”history”>
<view class=”title”>
<view class=”chunk”></view>
<text> 历史搜索 </text>
</view>
<view class=”tags”>
<block wx:for=”{{historyWords}}” wx:key=”item”>
<v-tag text='{{item}}’/>
</block>
</view>
</view>
热门搜索:
在 modelskeyword.js 中:
// 引入自己封装的 API 请求方法
import {
HTTP
} from ‘../utils/http-promise’

// 获取热门搜素搜关键字
getHot() {
return this.request({
url: ‘/book/hot_keyword’
})
}
在 componentssearchindex.js 中:
// 定义组件初始值,通过调用传进来的 getHot 方法获取热门搜索关键字,并更新到初始值 hotWords 中
data: {
historyWords: [], // 历史搜索关键字
hotWords: [] // 热门搜索关键字
},
// 组件初始化时,小程序默认调用的生命周期函数
attached() {
const historywords = Keywordmodel.getHistory()
const hotword = Keywordmodel.getHot()
this.setData({
historyWords: historywords
})

hotword.then(res => {
this.setData({
hotWords: res.hot
})
})
},
在 componentssearchindex.wxml 中:
// 将从服务器获取到的 hotWords 数组遍历,呈现到页面中
<view class=”history hot-search”>
<view class=”title”>
<view class=”chunk”></view>
<text> 热门搜索 </text>
</view>
<view class=”tags”>
<block wx:for=”{{hotWords}}” wx:key=”item”>
<v-tag text='{{item}}’/>
</block>
</view>
</view>
注意点:
由于在 componentssearchindex.js 调用了 Keywordmodel.getHot()方法,这个方法是和服务器相关联的,这样做,会使组件复用性降低。如果要想让 search 组件复用性变高,应该在 componentssearchindex.js 的 properties 中开放一个属性,然后再引用 search 组件的 pages 页面里调用 models 里的方法,再把数据通过属性传递给 search 组件,然后再做数据绑定,这样就让 search 组件具备了复用性

在 modelsbook.js 中:
// 定义 search 函数,封装向服务器发送请求功能
// 书籍搜索
search(start, q) {
return this.request({
url: ‘/book/search?summary=1’,
data: {
q: q,
start: start
}
})
}
在 componentssearchindex.js 中:
// 导入并实例化 BookModel 类,负责向服务器发送搜索图书的请求;在 data 中声明私有变量 dataArray 数组,为搜索图书当 summary=1, 返回概要数据。在用户输入完成点击完成时,调用 bookmodel.search 方法,并更新数据到 dataArray 中。
// 注意点:不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
import {
BookModel
} from ‘../../models/book’
const bookmodel = new BookModel()

data: {
historyWords: [], // 历史搜索关键字
hotWords: [], // 热门搜索关键字
dataArray: [] // 搜索图书当 summary=1, 返回概要数据
},

// 在 input 输入框输入完成将输入的内容加到缓存中
onConfirm(event) {
const word = event.detail.value

// 获取搜索的关键词 q, 调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value
bookmodel.search(0, q).then(res => {
this.setData({
dataArray: res.books
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(word)

})

}
在 componentssearchindex.wxml 中:
解析得到的搜索数据,并遍历呈现到页面中:
// 搜索得到的数据和热门搜索,历史搜索是不能一起显示的,一个显示,另一个就得隐藏,搜索得到的结果页面是默认不显示的,需要定义 searching 一个变量来控制显隐
<view wx:if=”{{searching}}” class=”books-container”>
<block wx:for=”{{dataArray}}” wx:key=”{{item.id}}”>
<v-book book=”{{item}}” class=”book”></v-book>
</block>
</view>

在 componentssearchindex.js 中:
// 在 data 私有属性中定义 searching 变量来控制显隐,默认为 false;在触发 onConfirm 事件中,为了用户体验好,应该点击完立即显示搜索页面,并将 searching 改为 true,让其搜索的内容显示到页面上
data: {
historyWords: [], // 历史搜索关键字
hotWords: [], // 热门搜索关键字
dataArray: [], // 搜索图书当 summary=1, 返回概要数据
searching: false // 控制搜索到的图书数据的显隐,默认不显示
},

// 在 input 输入框输入完成将输入的内容加到缓存中
onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this.setData({
searching: true
})

// 获取搜索的关键词 q, 调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value
bookmodel.search(0, q).then(res => {
this.setData({
dataArray: res.books
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(q)

})

}
实现 搜索框里的 x 按钮功能:
在 componentssearchindex.js 中:
// 在 components\search\index.wxml 中,为 x 图片绑定触摸时触发的 onDelete 事件 <image bind:tap=”onDelete” class=”cancel-img” src=”images/cancel.png”/>

// 触摸搜索图片里的 x 回到原来输入搜索的页面
onDelete(event){
this.setData({
searching: false
})
},

实现 用户点击历史搜索和热门搜索里的标签也能跳转到相应的搜索到的结果显示页面:只要监听到用户点击标签的事件就可以实现
在 componentssearchindex.js 中:
// 在 components\search\index.wxml 中:绑定 v -tag 组件自定事件 tapping 触发 onConfirm 事件:`<v-tag bind:tapping=”onConfirm” text='{{item}}’/>`

// 在 input 输入框输入完成将输入的内容加到缓存中
onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this.setData({
searching: true
})

// 获取搜索的关键词 q: 一种是用户输入的内容或是通过调用 tag 组件的自定义事件 tapping,里面有携带的 text 文本;调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value || event.detail.text
bookmodel.search(0, q).then(res => {
this.setData({
dataArray: res.books
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(q)

})

}
解决再点击 tag 标签搜索时应该在 input 输入框中显示书名的问题:
在 componentssearchindex.js 中:
// 通过数据绑定给 input 输入框绑定 value=”{{q}}”
//`<input value=”{{q}}” bind:confirm=”onConfirm” type=”text” class=”bar” placeholder-class=”in-bar” placeholder=” 书籍名 ” auto-focus=”true”/>`

// 先在 data 中定义私有数据 q: ” 代表输入框中要显示的内容,当数据请求完成后把点击标签的内容 q 赋值给私有数据 q 并更新
data: {
historyWords: [], // 历史搜索关键字
hotWords: [], // 热门搜索关键字
dataArray: [], // 搜索图书当 summary=1, 返回概要数据
searching: false, // 控制搜索到的图书数据的显隐,默认不显示
q: ” // 输入框中要显示的内容
},
// 在 input 输入框输入完成将输入的内容加到缓存中
onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this.setData({
searching: true
})

// 获取搜索的关键词 q, 调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value || event.detail.text
bookmodel.search(0, q).then(res => {
this.setData({
dataArray: res.books,
q: q
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(q)

})

}
实现数据分页加载功能:
第一种方法:用微信小程序提供的内置组件 scroll-view。
第二种方法:用 pages 里的 页面上拉触底事件的处理函数 onReachBottom。:
在 pagesbookbook.js 中:
// 在 data 里设置私有变量 more 为 false,代表的是是否需要加载更多数据,默认是不加载
data: {
books: [],
searching: false, // 控制搜索框组件 search 显隐,默认不显示
more: false // 是否需要加载更多数据,默认是不加载
},

// 用 pages 里自带的 页面上拉触底事件的处理函数 onReachBottom 监听页面是否到底了,如果到底了就会就会将 more 改变为 true,就可以实现加载更多数据方法
onReachBottom: function() {
console.log(‘ 到底了 ’)
this.setData({
more: true
})
},

// 由于 search 组件不是页面级组件,没有 onReachBottom 函数,就需要通过属性传值的方式将 more 私有变量控制是否加载更多数据传给子组件 search
// 在 pages\book\book.wxml 中:`<v-search more=”{{more}}” bind:cancel=”onCancel” wx:if=”{{searching}}”/>`

// 然后在 search 组件里接收父级传递过来的属性 more,并利用监听函数 observer,只要外界传来的数据改变就会触发此函数执行
properties: {
more: {
type: String,
observer: ‘_load_more’
} // 从 pages/book/book.js 传来的属性,监听滑到底步操作. 只要外界传来的属性改变就会触发 observer 函数执行
},

methods: {
// 只要外界传来的属性改变就会触发 observer 函数执行
_load_more() {
console.log(‘ 监听函数触发到底了 ’)
},
}

但现在存在一个问题就是:observer 只会触发一次,因为下拉到底会把 more 变为 true,之后就都是 true 不会再发生变化了,就不会再触发监听函数 observer 执行。
解决方法:用随机字符串触发 observer 函数,因为 observer 函数的执行必须是监听的数据发生改变才会执行此函数。和 Vue 中的 watch 很相似。

在 pagesbookbook.js 中:
// 将私有数据 data 中的 more 改为空字符串
data: {
books: [],
searching: false, // 控制搜索框组件 search 显隐,默认不显示
more: ” // 是否需要加载更多数据,默认是不加载
},

// 触发 页面上拉触底事件的处理函数,将 more 变为随机数,导入 random 自定义随机处理函数,问题解决
onReachBottom: function() {
console.log(‘ 到底了 ’)
this.setData({
more: random(16)
})
},

// 在 utils\common.js 中:
// 定义随机生成字符串处理函数,n 是生成随机字符串的位数
const charts = [‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’, ‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’, ‘G’, ‘H’, ‘I’, ‘J’, ‘K’, ‘L’, ‘M’, ‘N’, ‘O’, ‘P’, ‘Q’, ‘R’, ‘S’, ‘T’, ‘U’, ‘V’, ‘W’, ‘X’, ‘Y’, ‘Z’]

const random = function generateMixed(n) {
var res = ”
for (var i = 0; i < n; i++) {
var id = Math.ceil(Math.random() * 35)
res += charts[id]
}
return res
}

export {
random
}

在 componentssearchindex.js 中:
实现加载更多数据:
// 和 onConfirm 一样都需要调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
// 先判断已得到的搜索数据的长度, 在调用 search 方法将最新获取的数据和原来的数据拼接到一起更新数据然后呈现到页面中
_load_more() {
console.log(‘ 监听函数触发到底了 ’)
const length = this.data.dataArray.length
bookmodel.search(length, this.data.q).then(res => {
const tempArray = this.data.dataArray.concat(res.books)
this.setData({
dataArray: tempArray
})
})

},

细节完善:
// 如果关键字 q 初始没有值就直接返回什么也不做
_load_more() {
console.log(‘ 监听函数触发到底了 ’)
if (!this.data.q) {
return
}
const length = this.data.dataArray.length
bookmodel.search(length, this.data.q).then(res => {
const tempArray = this.data.dataArray.concat(res.books)
this.setData({
dataArray: tempArray
})
})

},

// 问题:当下拉刷新没有更多数据时,还会继续向服务器发送请求非常耗性能;还有就是用户操作过快没等第一次请求的结果回来,就又发送一次相同的请求,会加载重复的数据,非常耗性能

// 解决:使用锁的概念解决重复加载数据的问题
// 其实就是事件节流操作,在 data 中定义一个 loading:false,表示是否正在发送请求,默认是没有发送请求,在_load_more 中,判断如果正在发送请求就什么也不做,如果没有正在发送请求就将 loading 变为 true,调用 search 方法向服务器发送请求,待请求完毕并返回结果时将 loading 变为 false。
data:{
loading: false // 表示是否正在发送请求,默认是没有发送请求
},
_load_more() {
console.log(‘ 监听函数触发到底了 ’)
if (!this.data.q) {
return
}
// 如果是正在发送请求就什么也不做
if (this.data.loading) {
return
}
const length = this.data.dataArray.length
bookmodel.search(length, this.data.q).then(res => {
const tempArray = this.data.dataArray.concat(res.books)
this.setData({
dataArray: tempArray,
loading: false
})
})

},

进一步封装优化,组件行为逻辑抽象分页行为,顺便解决 是否还有更多数据的问题:
在 components 中,创建并封装一个公用行为和方法的组件 pagination:
在 componentsbehaviorspagination.js 中:
// 封装一个公用行为和方法的类 paginationBev
const paginationBev = Behavior({
data: {
dataArray: [], // 分页不断加载的数据
total: 0 // 数据的总数
},

methods: {
// 加载更多拼接更多数据到数组中;新加载的数据合并到 dataArray 中
setMoreData(dataArray) {
const tempArray = this.data.dataArray.concat(dataArray)
this.setData({
dataArray: tempArray
})
},

// 调用 search 方法时返回起始的记录数
getCurrentStart() {
return this.data.dataArray.length
},

// 获取设置从服务器得到数据的 总长度
setTotal(total) {
this.data.total = total
},

// 是否还有更多的数据需要加载。如果得到数据的长度大于服务器返回的总长度,代表没有更多数据了,就停止发请求
hasMore() {
if (this.data.dataArray.length >= this.data.total) {
return false
} else {
return true
}
}
}

})

export {
paginationBev
}

在 componentssearchindex.js 中:
// 先导入封装的公用行为方法,再进一步改写_load_more 和 onConfirm 方法,将写好的公用方法用上
import {
paginationBev
} from ‘../behaviors/pagination’

_load_more() {
console.log(‘ 监听函数触发到底了 ’)

if (!this.data.q) {
return
}
// 如果是正在发送请求就什么也不做
if (this.data.loading) {
return
}

if (this.hasMore()) {
this.data.loading = true// 必须放在 hasMore()里
bookmodel.search(this.getCurrentStart(), this.data.q).then(res => {
this.setMoreData(res.books)
this.setData({
loading: false
})
})
}

},
onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this.setData({
searching: true
})

// 获取搜索的关键词 q, 调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value || event.detail.text

bookmodel.search(0, q).then(res => {
this.setMoreData(res.books)
this.setTotal(res.total)
this.setData({

q: q
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(q)

})

}
但这时又会出现一个小问题:就是每次点 x 回退到搜索页面时,再次搜索同样的书籍时,会存在以前请求的数据没有清空又会重新向服务器发送请求,就会出现更多的重复数据
解决方法:就是在每次点 x 时,清空本次搜索的数据也就是 Behavior 里面的数据状态,上一次搜索的数据才不会影响本次搜索
在 componentsbehaviorspagination.js 中:
// 加入清空数据,设置初始值的方法
initialize() {
this.data.dataArray = []
this.data.total = null
}
在 componentssearchindex.js 中:
// 在触发 onConfirm 函数时调用 this.initialize()方法先清空上一次搜索的数据在加载
onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this.setData({
searching: true
})
// 先清空上一次搜索的数据在加载
this.initialize()
// 获取搜索的关键词 q, 调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value || event.detail.text

bookmodel.search(0, q).then(res => {
this.setMoreData(res.books)
this.setTotal(res.total)
this.setData({

q: q
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(q)

})

}
搜索代码重构:增强代码可阅读性:
在 componentssearchindex.js 中:
// 多封装一些小的函数
import {
KeywordModel
} from ‘../../models/keyword’
import {
BookModel
} from ‘../../models/book’
import {
paginationBev
} from ‘../behaviors/pagination’

const Keywordmodel = new KeywordModel()
const bookmodel = new BookModel()

// components/search/index.js
Component({
// 组件使用行为需加
behaviors: [paginationBev],

/**
* 组件的属性列表
*/
properties: {
more: {
type: String,
observer: ‘loadMore’
} // 从 pages/book/book.js 传来的属性,监听滑到底步操作. 只要外界传来的属性改变就会触发 observer 函数执行
},

/**
* 组件的初始数据
*/
data: {
historyWords: [], // 历史搜索关键字
hotWords: [], // 热门搜索关键字
// dataArray: [], // 搜索图书当 summary=1, 返回概要数据
searching: false, // 控制搜索到的图书数据的显隐,默认不显示
q: ”, // 输入框中要显示的内容
loading: false // 表示是否正在发送请求,默认是没有发送请求
},

// 组件初始化时,小程序默认调用的生命周期函数
attached() {
// const historywords = Keywordmodel.getHistory()
// const hotword = Keywordmodel.getHot()
this.setData({
historyWords: Keywordmodel.getHistory()
})

Keywordmodel.getHot().then(res => {
this.setData({
hotWords: res.hot
})
})
},

/**
* 组件的方法列表
*/
methods: {
// 只要外界传来的属性改变就会触发 observer 函数执行
loadMore() {
console.log(‘ 监听函数触发到底了 ’)
// 和 onConfirm 一样都需要调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
// 先判断已得到的搜索数据的长度, 在调用 search 方法将最新获取的数据和原来的数据拼接到一起更新数据然后呈现到页面中
if (!this.data.q) {
return
}
// 如果是正在发送请求就什么也不做
if (this._isLocked()) {
return
}
// const length = this.data.dataArray.length

if (this.hasMore()) {
this._addLocked()
bookmodel.search(this.getCurrentStart(), this.data.q).then(res => {
this.setMoreData(res.books)
this._unLocked()
})
}

},

// 点击取消将搜索组件关掉,有两种方法:一是,在自己的内部创建一个变量控制显隐,不推荐,因为万一还有其他操作扩展性不好。二是,创建一个自定义事件,将自定义事件传给父级,让父级触发
onCancel(event) {
this.triggerEvent(‘cancel’, {}, {})
},

// 触摸搜索图片里的 x 回到原来输入搜索的页面
onDelete(event) {
this._hideResult()
},

// 在 input 输入框输入完成将输入的内容加到缓存中
onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this._showResult()
// 先清空上一次搜索的数据在加载
this.initialize()
// 获取搜索的关键词 q, 调取 search 方法返回当 summary=1, 返回概要数据: 并更新数据到 dataArray 中
const q = event.detail.value || event.detail.text

bookmodel.search(0, q).then(res => {
this.setMoreData(res.books)
this.setTotal(res.total)
this.setData({

q: q
})

// 不能用户输入什么都保存在缓存中,只有用户搜索到有效的关键字时才保存到缓存中
Keywordmodel.addToHistory(q)

})

},

// 更新变量的状态, 显示搜索框
_showResult() {
this.setData({
searching: true
})
},

// 更新变量的状态, 隐藏搜索框
_hideResult() {
this.setData({
searching: false
})
},

// 事件节流机制,判断是否加锁
_isLocked() {
return this.data.loading ? true : false
},

// 加锁
_addLocked() {
this.data.loading = true
},

// 解锁
_unLocked() {
this.data.loading = false
},

}
})

小问题:当加载的时候突然断网,数据还没加载完,等在恢复网络的时候,就不能继续向服务器发送请求了。问题存在的原因在于出现死锁,只有请求成功才会解锁继续发送请求,如果请求失败,就不会解锁什么也做不了。
解决方法:
在 componentssearchindex.js 中:
// 只要在请求失败的回调函数里加上解锁就可以了

if (this.hasMore()) {
this._addLocked()
bookmodel.search(this.getCurrentStart(), this.data.q).then(res => {
this.setMoreData(res.books)
this._unLocked()
}, () => {
this._unLocked()
})
}
加入 loading 效果,提升用户体验:
先创建一个 loading 公共组件,只需写简单的样式效果就行,在 search 组件中注册并使用。
在 componentssearchindex.js 中:
// 在 components\search\index.wxml 中:加入两个 loading 组件。第一个在中间显示,获取搜获数据中;第二个在底部显示,数据加载更多时显示
//<v-loading class=”loading-center” wx:if=”{{loadingCenter}}”/>
// <v-loading class=”loading” wx:if=”{{loading}}”/>

// 在 data 中添加一个 loadingCenter 变量控制 loading 效果是否在中间显示,并且加两个私有函数控制 loading 的显隐。在 onConfirm 函数中调用 this._showLoadingCenter()函数,显示 loading 效果,在 数据加载完成,调取 this._hideLoadingCenter(),取消显示 loading 效果,
data: {loadingCenter: false},
// 改变 loadingCenter 的值
_showLoadingCenter() {
this.setData({
loadingCenter: true
})
},

// 改变 loadingCenter 的值
_hideLoadingCenter() {
this.setData({
loadingCenter: false
})
}

onConfirm(event) {
// 为了用户体验好,应该点击完立即显示搜索页面
this._showResult()
// 显示 loading 效果
this._showLoadingCenter()
this.initialize()
const q = event.detail.value || event.detail.text
bookmodel.search(0, q).then(res => {
this.setMoreData(res.books)
this.setTotal(res.total)
this.setData({
q: q
})
Keywordmodel.addToHistory(q)
// 数据加载完成,取消显示 loading 效果
this._hideLoadingCenter()
})

},
知识点补充:
特别注意 setData 与直接赋值的区别:setData: 调用 setData 函数更新的数据会触发页面重新渲染,和 REACT 里的 setState 相似。
而直接赋值,只是在内存中改变的状态,并没有更新到页面中

空搜索结果的处理:
在 componentsbehaviorspagination.js 中:
// 在公共行为中加入 noneResult:false,控制是否显示没有得到想要的搜索结果,在 setTotal 方法中,如果返回的结果为 0,就是没有得到想要的搜索结果。将 noneResult:true 显示出来。在 initialize 设置初始值并清空数据函数,再将 noneResult:false,取消显示。

data: {
dataArray: [], // 请求返回的数组
total: null, // 数据的总数
noneResult: false // 没有得到想要的搜索结果
},

// 获取设置数据的 总长度
// 如果返回的结果为 0,就说明没有得到搜索结果,将提示内容显示出来
setTotal(total) {
this.data.total = total
if (total === 0) {
this.setData({
noneResult: true
})
}
},

// 清空数据,设置初始值,将提示隐藏
initialize() {
this.setData({
dataArray: [],
noneResult: false
})
this.data.total = null
}

在 componentssearchindex.wxml 中:
// 加入空搜索显示的结果结构
<text wx:if=”{{noneResult}}” class=”empty-tip”> 没有搜索到书籍 </text>
在 componentssearchindex.js 中:
// 触摸搜索图片里的 x 回到原来输入搜索的页面,先回到初始值,再将搜索组件隐藏。在 onConfirm 中,不用等数据加载完,输入完成后就把输入的内容显示在输入框中。
onDelete(event) {
this.initialize()
this._hideResult()
},

onConfirm(event) {
this._showResult()
this._showLoadingCenter()
const q = event.detail.value || event.detail.text
// 不用等数据加载完,输入完成后就把输入的内容显示在输入框中。
this.setData({
q: q
})

bookmodel.search(0, q).then(res => {
this.setMoreData(res.books)
this.setTotal(res.total)
Keywordmodel.addToHistory(q)
this._hideLoadingCenter()

})

},

处理一个小问题:就是在热门搜索里搜索王小波,返回的搜索结果页面每本书里会显示有喜欢字样,去掉喜欢字样。
在 componentsbookindex.js 中:
// 在 components\search\index.wxml 中:
// 通过搜索组件搜索显示的书籍都不显示喜欢字样,通过属性传值的方式将喜欢字样去掉,把 false 传递给子组件,子组件通过 showLike 变量接收,通过数据控制显隐将喜欢字样去掉。
<v-book book=”{{item}}” class=”book” show-like=”{{false}}”></v-book>

// 添加一个 showLike 属性,代表每本书里面的喜欢字样是否显示
properties: {
book: Object,
showLike: {// 控制每本书下面有个喜欢字样的显示与隐藏
type: Boolean,
value: true
}
},

// 在 components\book\index.wxml 中:
//showLike 属性的显示和隐藏控制喜欢字样的显示和隐藏
<view class=”foot” wx:if=”{{showLike}}”>
<text class=”footer”>{{book.fav_nums}} 喜欢 </text>
</view>
对 search 组件进一步优化,将锁提取到分页行为中:
在 componentsbehaviorspagination.js 中:
// 把在 components\search\index.js 中的三个锁方法提取到公用行为方法中,在公用行为方法中,在 data 里添加 loading:false 属性。在 initialize 函数中,把 loading:false 也加进去即可

// 事件节流机制,判断是否加锁
isLocked() {
return this.data.loading ? true : false
},

// 加锁
addLocked() {
this.setData({
loading: true
})

},

// 解锁
unLocked() {
this.setData({
loading: false
})
},
两种方法监听移动端触底的操作:
scroll-view 或 Pages 里的 onReachBottom。如果要想用 scroll-view 把 view 组件换成 scroll-view 就可以。

微信 open-data 显示用户信息:https://developers.weixin.qq.com/miniprogram/dev/api/wx.getUserInfo.html
用户授权,需要使用 button 来授权登录。
很多时候我们需要把得到信息保存到我们自己的服务器上去,需要通过 js 代码中操作用户头像等信息。封装一个 image-button 通用组件就可以,改变他的图片,并且可以在不同的方式中调用,只需要改变 open-type 属性就可以。

分享的实现:自定义分享 button:

小程序之间的跳转:这两个小程序都必须关联同一个公众号

==============================================================

bug 解决
在 components/episode/index.js 中
报错 RangeError: Maximum call stack size exceeded:
原因:
// 错误写法
properties: {
index: {// 默认显示为 0
type: String,
observer: function(newVal, oldVal, changedPath) {
let val = newVal < 10 ? ‘0’ + newVal : newVal
this.setData({
index: val
})
}
}
},
// 小程序的 observer,相当于 vue 中的 watch 监听函数,只要监听的 index 数据改变,就会调用 observer 方法,会形成死循环,就会报错 RangeError: Maximum call stack size exceeded
解决:
// 第一种解决方法
this.setData({
_index: val
})

data: {
year: 0,
month: ”,
_index: ”
},
// 第二种解决方法(推荐)

在 components/music/index.js 中:
报错:setBackgroundAudioState:fail title is nil!;at pages/classic/classic onReady function;at api setBackgroundAudioState fail callback function
Error: setBackgroundAudioState:fail title is nil!
原因:
少 title 外传属性
解决:
// 在 `components/music/index.js` 中:
properties: {
src: String,
title: String
},
methods: {
// 为图片绑定触摸播放事件
onPlay: function() {
// 图片切换
this.setData({
playing: true
})
mMgr.src = this.properties.src
mMgr.title = this.properties.title
}
}
—————————————————————————–
// 在 app.json 中加上:
“requiredBackgroundModes”: [
“audio”
],

============================================================================

移动端增加用户体验优化
在 components/navi 中:
点击的左右小三角要足够大,用户触摸时才能点击到。两种方法,第一种是再切图时,切得大一些;第二种是,自己编写代码控制操作区域

完成效果展示:
视频地址

退出移动版