- 微信小程序答题页——swiper渲染优化及swiper分页实现
前言 swiper的加载太多问题,网上资料好像没有一个特别明确的,就拿这个答题页,来讲讲我的解决方案 这里实现了如下功能和细节: 保证swiper-item的数量固定,加载大量数据时,大大优化渲染效率记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验示例动图 [图片] 截图 [图片] [图片] 问题原因 当swiper-item数量很多的时候,会出现性能问题 我实现了一个答题小程序,在一次性加载100个swipe-item的时候,低端手机页面渲染时间达到了2000多ms 也就是说在进入答题页的时候,会卡顿2秒多去加载这100个swiper-item 思考问题 那我们能不能让他先加载一部分,然后滑动以后再去改变item的数据,让swiper一直保持一定量的swiper-item? 注意到官方文档有这么两个属性可以利用,我们可以开启衔接滑动,然后再bindchange方法中去修改data [图片] 1、保证swiper-item的数量固定,加载大量数据时,优化渲染效率 假设我们请求到的数据的为list,实际渲染的数据为swiperList 我们现在给他就固定3个swiper-item,前后滑动的时候去替换数据 正向滑动的时候去替换滑动后的下一页数据,反向滑动的时候去替换滑动后的上一页数据 当我们知道了要替换的条件,我们便可以去替换数据了 但是我们应该考虑到临界值的问题,如果当前页是list第一项和最后一项该怎么办,向左向右滑是不是得禁止啊 这边是判断没数据会让它再弹回去 2、记录上次的位置,页面初次加载不一定非得是第一页,可以是任何页 有很多时候,我们是从某一项直接进来的,比如说上次答题答到了第五题,我这次进来要直接做第六题 那么我们需要去初始化这个swiperList,让它当前页、上一页、下一页都有数据 3、答题卡选择某一index回来以后的数据替换,并去掉swiper切换动画,提升交互体验 从答题卡选择index,那就不仅仅是滑动上下页了,它可以跳转到任何页,所以也采用类似初始化swiperList的方法 swiper切换动画我这边是默认250ms,但是发现有时候从答题卡点击回来,你在答题卡点击的下一项不知道会从左还是从右滑过来 体验真的很差,一开始不知道怎么禁掉动画,其实在跳转到答题卡页的时候把duration设为0就可以了 然后在答题卡页的unload方法中恢复 关键点: 在固定3个swiper-item的同时,要保证我们可以有办法来替代微信自带swiper的current属性和change方法 swiper-limited-load使用方法及说明: 将components中的swiper-limited-load复制到您的项目中在需要的页面引用此组件,并且创建自己的自定义组件item-view在初始化数据时,为你的list的每一项指定index属性具体可以参照项目目录start-swiper-limited-load中的用法说明:其它属性和swiper无异,你们可以自己单独添加你们需要的属性总结 一开始很头疼,为什么微信小程序提供的这个swiper,没去考虑这方面 然后在网上和社区找也没有一个特别好的解决方案。 后来想想,遇到需求就静下来解决吧。 项目地址:https://github.com/pengboboer/swiper-limited-load 如果错误,欢迎指出。 如有新的需求也可以提出来,如果有时间的话,我会帮你们完善。 如果能帮到你们,记得给一个star,谢谢。 ---补充 有很多朋友在评论区提到了分页的需求,抽时间写了一个分页的Demo和大家分享一下。 还是以答题为例,比如我们一共有500条数据,一页20条,可能需要如下功能,乍一看不就加了个分页,挺简单的,其实实现起来挺麻烦的,下面说一下思路和一些需要特别注意的点: 1、从其他页面跳转到答题页时,不光只能默认在第一题,可以是任意一题,比如第80题。 跳转到任意一题,那么需要我们根据index算出该数据在第几页,然后需要请求该页数据,最后显示对应的index。我的思路更注重用户体验,不可能是上滑或者下滑才开始去请求数据,一定是要用户滑动前提前请求好数据。所以起码要保证左右两侧在初始化那一刻都有数据。如果此题和它的上一题下一题都在同一页,那么我们只需要请求一页数据(第15题,那么只需请求第1页数据)。如果此题和它的上一题或者下一题不在同一页,那么我们可能需要请求两页数据。(第20题,那么需要请求第1页和第2页数据) 2、左滑、右滑没数据时,都可以加载新数据。直到滑到第一题或者最后一题。 如果我们初始化时是第24题,那么我们左滑到第21题时,就应该去请求第一页的数据。那么用户在看完21题时,再滑到20题,可能就根本不会感知到通过网络请求了数据。但是如果用户此刻滑动特别快:滑到21题时请求了网络,请求还没成功,就又向左滑了。那么我们需要限制用户的滑动,给用户一个提示:数据正在加载中。 3、从答题卡点击任意一题可以跳转到相应的题目,并且左右滑动显示正常数据 比如我们初始化是跳转到了第80题,不一会点击答题卡又要跳转到200题,一会又跳转到150题。各种无序操作,你也不知道用户要往哪里点。 一开始是想着维护一个主list,点到哪道题往list中添加这道题所在的当页的数据,但是还得判断这一页或者左滑右滑请求新一页的数据得往list的哪个位置添加。这来回来去乱七八糟的判断就很麻烦了,很容易出bug。而且list长度太长了以后insert的性能也不好。 后来就去想,要不答题卡点击任意一题都清空旧的list,然后请求新的数据,左右滑动没数据了再请求新的数据呗。但是这样很浪费资源,并且用户体验也不好,用户已经从第1题答到第200题了,这时用户从答题卡选择了一个25题,还得重新请求网络。而且200道题的数据都没了,那再选个26题,再重新请求网络?网络有延时不说,还浪费资源。 最后转念一想,这时候就需要弄一个缓存了。所以最终的解决方法就出来了:我们维护一个map,在网络请求成功后,在map中保存对应页的数据,同时我们维护一个主list来显示对应的题目。当我们在答题卡选择某一题目,就清空list,然后判断map中有没有该页的数据,如果有就直接拿来,没有就再去网络请求。这个处理方式,写法相对来说简单,不需要乱七八糟的判断,也不浪费资源,用户体验也很不错。 总结 以上就是一些思路和要注意的地方。这个Demo断断续续花了好几天时间写出来的。可能我说的比较啰嗦比较细,只是想让需要用到这个分页Demo的同学能理解我是如何实现的。 如果觉得能帮到你,记得给一个star,谢谢。同时如果这个demo有bug或者你们有新想法,欢迎提出来。
2021-01-07 - 【手把手喂饭】Behavior教程:如何给每个页面混入统一的分享-share?
1、创建一个 js文件,我放在了个根目录下的 utils 里面 ,你也可以放在 behavior文件夹下(官方示例)取名为 share.js [图片] share.js 代码如下: let title = '你的默认分享主题' let imageUrl= '你的默认分享图片地址' let path = 'pages/index/index' // 默认放了个首页 module.exports = Behavior({ methods: { onShareAppMessage() { return { title, path, imageUrl } }, onShareTimeline() { return { title, path, imageUrl } } } }) 有没有很简单?我自己的 还加入了 默认的分享链接,还能知道是谁分享的 (大家可忽略) let userInfo = getApp().globalData.userInfo path = 'pages/index/index?share=' + userInfo._id 具体使用 ,在页面中引入 share.js,再配置一下就好 [图片] import share from "../../utils/share" Page({ // options: { // pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段 // }, behaviors: [share], // 混合 data: { 喂饭完毕~
2023-02-03 - 微信小程序答题页 swiper分页 极简
因为最近要写个做题页面,要求左右滑动切换页面。题目数量三位数会导致swiper卡顿严重,找了下相关文章没有发现合适的,就自己写了下。 实现功能 1、可以加载大量数据 2、跳转任意index 3、实现代码简单,方便复用 [图片] 直接帖代码 // pages/swiper/swiper.js Page({ /** * 页面的初始数据 */ data: { dataList: [], //实际数据 list: [], //展示数据 currentIndex: 0, //真实的index current: 0, //swiper当前的index recordCurrent: 0, //swiper上次的index duration: 300 //动画时常 }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { for (let i = 0; i < 1000; ++i) { this.data.dataList.push(i) } this.setData({ dataList: this.data.dataList }) this.upSwiper(0) }, animationfinish({ detail }) { // 用户滑动 if (detail.source == "touch") { // 滑动后的index this.upSwiper(this.data.currentIndex + detail.current - this.data.recordCurrent) } }, upSwiper(index) { // 防止越界 if (index < 0 || index >= this.data.dataList.length) { return } this.setData({ currentIndex: index }) // 更新数据 let list = [] for (let i = this.data.currentIndex - 1; i <= this.data.currentIndex + 1; ++i) { let item = this.data.dataList[i] if (typeof (item) !== "undefined") { list.push(item) } } let current = 0; // 只要不是第一个页面 current都是1 if (index != 0) { current = 1 } // 取消动画 this.setData({ duration: 0 }) // current 和 list要同时更新,不然数据会闪 this.setData({ current: current, recordCurrent: current, duration: 300, list }) }, tap(ev) { let index = ev.currentTarget.dataset.index this.upSwiper(index) } }) <!--pages/swiper/swiper.wxml--> <swiper id="swiper" current="{{current}}" duration="{{duration}}" bindanimationfinish="animationfinish"> <swiper-item wx:for="{{list}}" wx:for-item="ee"> <view wx:for="{{20}}">{{ee}}</view> </swiper-item> </swiper> <view class="but-z"><button catchtap="tap" data-index="{{0}}">0</button> <button catchtap="tap" data-index="{{5}}">5</button> <button catchtap="tap" data-index="{{dataList.length-1}}">{{dataList.length-1}}</button> </view> /* pages/swiper/swiper.wxss */ #swiper { width: 100vw; height: 100vh; } .but-z { position: fixed; bottom: 20px; height: 40px; width: 100vw; display: flex } button { background-color: red; width: 100rpx !important; text-align: center; }
2021-12-14 - 利用css圆锥渐变实现环形进度条
[图片] 锥形渐变的正式语法如下: conic-gradient( [ from <angle> ]? [ at <position> ]?, <angular-color-stop-list> ) 可以看出锥形渐变由3部分组成: 起始角度 中心位置 角渐变断点 其中起始角度和中心位置都是可以省略的 这里只用了角渐变断点 background-image: conic-gradient(green 80%,#fff 80% 100%); 主要实现就两步 1.利用conic-gradient画一个圆 [图片] 2.利用任意元素做个内圆遮挡 [图片] 最外层的背景色原来是白色的,这里为了便于识别改成了灰色,现在将灰色的背景还原为白色 [图片] 环形进度条完成,就这么简单 通过动态的设置green的值就可以改变进度条的值 补充:还可以实现渐变进度条效果 [图片] 代码片段: 利用css圆锥渐变实现环形进度条
2020-06-22 - 巧用伪元素(:after/before)放大手机端点击区域
问题场景 我们在使用手机的过程中,是不是遇到过一些特别让你累的交互,就如手机端的按钮或图标老是点击不到,要多点击几次才能命中! 如何解决 手机的屏幕普遍比较小且分辨率高,这就导致设计的时候不能过大且要保持美观,严格按钮设计图尺寸开发后在手机端又过小导致点击事件无法触发。 那么怎么解决呢?不按照设计图切图?增加内边距撑大容器?这些方案都有或多或少不合理的地方,我有妙计可骚一把! 解决方案:after/before 通过设置点击元素的:after或:before伪元素大小来放大点击元素的点击区域 示例代码如下: [代码]/* 放大点击区域功能类 */ .nice-focus{ position:relative; } .nice-focus::after{ content:''; position:absolute; width:80px; height:80px; top:50%; left:50%; margin-top:-40px; margin-left:-40px; } /*返回按钮*/ .back{ width:12px; heigh:24px; background-image: url(/static/img/icon-back.png); background-size: 100% auto; } [代码] [代码]<!-- 页面使用 --> <div class="back nice-focus">返回</div> [代码] 总结 很多看似不能解决问题,换个角度就可以轻松解决!此方法也同样适用于小程序开发。
2019-10-28 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。 [代码] my.setPageScrollable({ scrollable: true, success: res => { console.log(res); }, fail: err => { console.log(err); }, complete: res => { console.log(res); } }) [代码] 20250618. API已开放,支付宝小程序测试时发现bug,安卓设置禁止滚动后,弹窗内的可滚动区域也会被禁止,IOS正常,且该问题暂时无法解决。 原因: 由于系统实现层面的差异,安卓与 iOS 对于滚动禁止的层级控制存在区别: 安卓端采用 Webview 级滚动限制(全页面锁定),生效时界面及所有弹层均不可滚动; iOS 端采用组件级滚动限制(局部锁定),当弹层激活时会智能区分层级,仅限制底层页面滚动而保持弹层可滚动。
06-18 - 微信小程序swiper高度动态适配(子元素高度不固定)
示例代码地址 https://github.com/s568774056/swipe.git 对于整页都是swiper的情况下。例如下面这张图: [图片] 则可以使用如下css [代码] [代码] [代码]swiper,swiper-item{[代码] [代码] [代码][代码]height[代码][代码]: [代码][代码]100[代码][代码]vh [代码][代码]!important[代码][代码];[代码][代码]}[代码] [代码] [代码] [代码]或者 [代码] [代码] [代码] [代码][代码] [代码][代码] swiper,swiper-item{ height: calc(100vh - 75rpx) !important; } [代码][代码] [代码] [代码] 对于swiper占据部分高度的情况下。 [图片] 使用如下代码 原理为在[代码]swiper-item[代码][代码][代码]的最上面和最下面插入空view,并利用wx api获取两个之间的高度差,然后设置给[代码]swiper[代码]。 细节方面需要自己调整下。为什么小程序不把这个组件做好呢?还得自己计算- -! <swiper class='hide' bindanimationfinish="swiperChange" style="height:{{swiper_height}};" current="{{isIndex}}"> <swiper-item wx:for="{{roomList}}" wx:for-item='room' wx:for-index="index"> <view id="start{{index}}" class='start-view'></view> <block wx:for="{{imgUrls}}" wx:for-item='path' wx:for-index="img-index"> <image mode="aspectFill" src="{{path}}" /> </block> <view id="end{{index}}" class='start-view'></view> </swiper-item> </swiper> [代码][代码][代码][代码] swiper { margin-top: 45rpx; } Page({ data: { roomList: ['Room1', 'Room2', 'Room3'], imgUrls: [ 'https://images.unsplash.com/photo-1551334787-21e6bd3ab135?w=640', 'https://images.unsplash.com/photo-1551214012-84f95e060dee?w=640', 'https://images.unsplash.com/photo-1551446591-142875a901a1?w=640' ], swiper_height: 0, isIndex:0 }, onLoad: function () { this.autoHeight(); }, changeNavBar: function (e) { this.setData({ isIndex: e.detail }); }, swiperChange: function (e) { this.setData({ isIndex: e.detail.current }); this.autoHeight(); }, autoHeight() { let { isIndex } = this.data; wx.createSelectorQuery() .select('#end' + isIndex).boundingClientRect() .select('#start' + isIndex).boundingClientRect().exec(rect => { let _space = rect[0].top - rect[1].top; _space = _space + 'px'; this.setData({ swiper_height: _space }); }) } }) 参考文章https://developers.weixin.qq.com/community/develop/doc/00008aaf4a473056d1c69a8b253c04
2019-09-04 - 【笔记】云开发用户记录存在更新,不存在新增
方案一 :如果这个表里只存userInfo的信息,将_id值设置为OPENID,直接使用set覆盖,最简单。注意:这种方式会覆盖掉以前的数据,如果有部分其他数据,如注册时间、最后登录时间请使用后面的方式。let userInfo = event.userInfo if (!userInfo) { console.log('无用户信息,更新失败') }else{ db.collection('user').doc(OPENID).set({ data: userInfo }).then(e => { console.log('用户数据更新成功', e) }) } 方案二:如果用户表中存了其他字段,需要实现存在更新,不存在插入,可以先查询,再判断处理。async function updateUser () { let chk = await db.collection('user').where({ _openid: OPENID }).get() if (chk.data.length) { // 存在记录,更新 await db.collection('user').where({ _openid: OPENID }).update(userInfo) } else { // 不存在,新增 await db.collection('user').add(userInfo) } } 方案三:将_id直接设置为openid,然后利用查不到指定_id会报错的特性,在catch里填写更新语句,这样可以少执行一次查询操作。let userInfo = event.userInfo userInfo.lastTime = db.serverDate() //添加最后更新时间 db.collection('user').doc(OPENID).update({ data: userInfo }).catch(err => { //用户不存在会抛出异常,在异常中处理新增 userInfo._id = OPENID //设置OPENID为_id userInfo.createTime = db.serverDate() //添加用户创建时间 db.collection('user').add({ data: userInfo }) })
2020-05-21