components/like
思路:css
like 默认为 false,显示空心小爱心html
触摸执行tap:onLike
方法,由于 this.setData({count:count,like:!like})
是异步的,先执行count = like ? count - 1 : count + 1
,这时like仍是false,执行count+1。而后在执行this.setData()方法,将like变为true,显示实心小爱心。vue
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接口。android
刚开始实心就调用'like/cancel'接口,空心就调用'like'接口css3
components/navi
思路:git
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}}"/>
githubsrc显示图片规则:若是是最新的期刊,就显示向左禁用状态disLeftSrc箭头;若是不是最新一期的期刊,就显示向左可用状态leftSrc箭头web
为图片绑定触摸事件onLeft,在
navi/index.js
中:json在 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 }
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) } }) }
处理是否点赞小爱心组件的缓存问题:他不须要缓存,须要实时获取最新数据
//编写一个获取点赞信息的方法,从服务器获取最新点赞信息的数据 // 获取点赞信息 getClassicLikeStatus(artID, category, cb) { this.request({ url: `classic/${category}/${artID}/favor`, success: cb }) }
//设置私有数据初始值 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 }) })
解决切换期刊时,其余期刊也都是播放状态的问题。应该是,切换期刊时音乐就中止播放,回到默认不播放状态
利用组件事件的通讯机制,小程序中只有父子组件
在 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 更好。生命周期不会从新执行。
解决切换期刊时音乐能够当作背景音乐一直播放,而其余的期刊是默认是不播放状态
//为了保证期刊在切换时,背景音乐能够一直播放,就要去除掉 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() },
//定义帧动画用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开启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硬件加速会致使大量消耗设备电量,下降电池寿命等问题。
//为图片加上播放就旋转的类,不播放 就就为空字符串 <image class="classic-img {{playing?'rotation':''}}" src="{{img}}"></image>
用 slot 插槽,解决在公用组件中能够加入其余修饰内容问题。其实就是,在定义公用组件时,用 slot 命名插槽占位,在父组件调用时能够传递须要的内容补位。和Vue的指令 v-slot 类似。
//在 Component 中加入 // 启用slot options: { multipleSlots: true },
//定义几个命名插槽,供父元素占位使用 <view class="container tag-class"> <slot name="before"></slot> <text>{{text}}</text> <slot name="after"></slot> </view>
//注册并使用组件 { "usingComponents": { "v-tag": "/components/tag/index" } }
//使用组件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 评论前两条显示两种颜色的作法:/* 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; }
定义外部样式方法,像父子组件传递属性同样,传递样式类
/* 定义外部样式 */ .ex-tag1 { background-color: #fffbdd !important; } .ex-tag2 { background-color: #eefbff !important; }
/*将自定义的样式类经过属性传值的方式传递给自定义子组件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>
//将外部传进来的样式写在Component中,声明一下 // 外部传进来的css,样式 externalClasses: ['tag-class'],
// 把父组件传递过来的类 tag-calss 写在 class 类上 <view class="container tag-class"> <slot name="before"></slot> <text>{{text}}</text> <slot name="after"></slot> </view>
缘由:
是由于服务器返回的原始数据 是\\n
,通过转义就变成\n
而
\n
在text文本标签中默认转义为换行
解决方法:
WXS:WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML
,能够构建出页面的结构。和Vue 中的 Vue.filter(过滤器名,过滤器方法) 很类似。WXS 与 JavaScript 是不一样的语言,有本身的语法,并不和 JavaScript 一致。因为运行环境的差别,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上两者运行效率无差别。
// 定义过滤器函数,处理服务器返回的数据,将 \\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
//引入 <wxs src="../../utils/filter.wxs" module="util"/> //在须要过滤的数据中使用 <text class="content">{{util.format(book.summary)}}</text>
//对须要缩进的段落前加如下的类,但这时只有第一段缩进 .content { text-indent: 58rpx; font-weight: 500; }
//用转义字符 做为空格,但这时小程序会以 样式输出,不是咱们想要的效果 var format = function(text) { if (!text) { return } var reg = getRegExp('\\\\n', 'g') return text.replace(reg, '\n ') } module.exports.format = format
//加入属性 decode="{{true}}",首行缩进问题解决 <text class="content" decode="{{true}}">{{util.format(book.summary)}}</text>
//添加一个限制短评长度的过滤器,并导出 // 限制短评的长度的过滤器 var limit = function(array, length) { return array.slice(0, length) } module.exports = { format: format, limit: limit };
<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>
// 因为 <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}}">
用户提交评论内容:
点击标签向服务器提交评论内容:
//为短评组件绑定出没事件 onTap <view bind:tap="onTap" class="container tag-class"> <slot name="before"></slot> <text>{{text}}</text> <slot name="after"></slot> </view>
// 当触摸短评小标签时,触发一个自定义事件,将短评内容传进去,公父组件调用自定义事件tapping methods: { // 触摸短评小标签时,触发的事件,触发一个自定义事件 onTap(event) { this.triggerEvent('tapping', { text: this.properties.text }) } }
//在父组件中调用子组件的自定义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>
//调取新增短评的接口 // 新增短评 postComment(bid, comment) { return this.request({ url: '/book/add/short_comment', method: 'POST', data: { book_id: bid, content: comment } }) }
// 触摸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 }) }) },
// input有本身的绑定事件bindconfirm,会调用手机键盘完成按键 <input bindconfirm="onPost" type="text" class="post" placeholder="短评最多12个字"/>
点击标签向服务器提交评论内容完成:
// 触摸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 }) }) },
若是没有短评显示问题:
//在短评后加上尚未短评标签,若是没有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
将解析为迭代中找到的第一个值。简单来讲就是:不论成功仍是失败的回调,哪个快就执行哪一个。
//用了 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下,供整个项目调取使用写业务逻辑,若是只是作个项目建议写在这里不会乱。
历史搜索:将历史搜索关键字写入缓存中,在从缓存中获取历史搜索关键字。热门搜索:调取服务器API
GET /book/hot_list
//首先从缓存中获取历史搜索关键字数组,判断获取的数组是否为空,若是为空,为了防止报错就返回空数组;若是不为空就直接返回获取的数组。 //将搜索关键字写入缓存中,先从缓存中获取历史关键字的数组,判断是否包含这次输入的关键字,若是没有此关键字,若是获取的长度大于最大长度,就将数组的最后一项删除;若是获取数组的长度小于最大长度,就将这次输入的关键字加到数组的第一位,而且设置到缓存中。 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}
//为input输入框绑定onConfirm事件 <input bind:confirm="onConfirm" type="text" class="bar" placeholder-class="in-bar" placeholder="书籍名" auto-focus="true"/>
// onConfirm事件执行,调用将输入的内容添加到缓存中的方法Keywordmodel.addToHistory(word),就能够将历史关键字添加到缓存中 methods: { // 点击取消将搜索组件关掉,有两种方法:一是,在本身的内部建立一个变量控制显隐,不推荐,由于万一还有其余操做扩展性很差。二是,建立一个自定义事件,将自定义事件传给父级,让父级触发 onCancel(event) { this.triggerEvent('cancel', {}, {}) }, // 在input输入框输入完成将输入的内容加到缓存中 onConfirm(event) { const word = event.detail.value Keywordmodel.addToHistory(word) } }
//将历史搜索的内容从缓存中取出来 data: { historyWords: [] //历史搜索关键字 }, // 组件初始化时,小程序默认调用的生命周期函数 attached() { const historywords = Keywordmodel.getHistory() this.setData({ historyWords: historywords }) },
//注册引用小标签 tag 组件,组件中也能够引入其余组件 "usingComponents": { "v-tag": "/components/tag/index" }
// 遍历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>
// 引入本身封装的API请求方法 import { HTTP } from '../utils/http-promise' // 获取热门搜素搜关键字 getHot() { return this.request({ url: '/book/hot_keyword' }) }
//定义组件初始值,经过调用传进来的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 }) }) },
//将从服务器获取到的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组件具有了复用性
//定义search函数,封装向服务器发送请求功能 // 书籍搜索 search(start, q) { return this.request({ url: '/book/search?summary=1', data: { q: q, start: start } }) }
// 导入并实例化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) }) }
解析获得的搜索数据,并遍历呈现到页面中:
//搜索获得的数据和热门搜索,历史搜索是不能一块儿显示的,一个显示,另外一个就得隐藏,搜索获得的结果页面是默认不显示的,须要定义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>
//在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) }) }
// 在 components\search\index.wxml 中,为 x 图片绑定触摸时触发的 onDelete 事件<image bind:tap="onDelete" class="cancel-img" src="images/cancel.png"/> // 触摸搜索图片里的x回到原来输入搜索的页面 onDelete(event){ this.setData({ searching: false }) },
// 在 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) }) }
//经过数据绑定给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。:
//在 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很类似。
//将私有数据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 }
// 和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:
//封装一个公用行为和方法的类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 }
// 先导入封装的公用行为方法,再进一步改写_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时,清空本次搜索的数据也就是Behavior里面的数据状态 ,上一次搜索的数据才不会影响本次搜索
//加入清空数据,设置初始值的方法 initialize() { this.data.dataArray = [] this.data.total = null }
//在触发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) }) }
//多封装一些小的函数 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 }, } })
解决方法:
//只要在请求失败的回调函数里加上解锁就能够了 if (this.hasMore()) { this._addLocked() bookmodel.search(this.getCurrentStart(), this.data.q).then(res => { this.setMoreData(res.books) this._unLocked() }, () => { this._unLocked() }) }
先建立一个loading公共组件,只需写简单的样式效果就行,在search组件中注册并使用。
// 在 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类似。
而直接赋值,只是在内存中改变的状态,并无更新到页面中
//在公共行为中加入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 }
//加入空搜索显示的结果结构 <text wx:if="{{ noneResult}}" class="empty-tip">没有搜索到书籍</text>
// 触摸搜索图片里的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() }) },
//在 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>
//把在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:
小程序之间的跳转:这两个小程序都必须关联同一个公众号
==============================================================
在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
中:点击的左右小三角要足够大,用户触摸时才能点击到。两种方法,第一种是再切图时,切得大一些;第二种是,本身编写代码控制操做区域
完成效果展现: