- 云开发收费,求助如何转到其他免费数据库?
求个免费的数据库平台,以及使用教程
2022-09-01 - 免费ICP备案攻略。不花1分钱拥有一台云服务器并顺利ICP备案。
写在前面: 大家不要将ICP证和ICP备案搞混了。 ICP证指的是【电信增值业务经营许可证】,这个资质需要企业主体至少100万注金,去工信部办理,比较难办理;社交-交友需要ICP证。 而ICP备案,【非经营性互联网信息服务备案核准】仅仅是指企业主体的域名备案,可以简单的按以下步骤免费办理成功,其他社交类目如社区、论坛、笔记等,只需要ICP备案即可。 1、在腾讯云注册一个账号并认证企业主体(不吹不黑,开发小程序当然首选腾讯云,好用)。http://www.qcloud.com/ 如果你是个人主体,就不要往下看了,没必要折腾了。 2、找到腾讯云免费活动页:https://cloud.tencent.com/act/free?from=10107 3、选择一款云服务器,180天免费试用。 云服务器申请成功后,它的使命就完成了,没用了,让它自生自灭吧。 在整个备案过程中,也不需要部署网站(域名都没有备案,哪来的网站?)。 [图片] 云服务器180天到期后,可以自己决定是否续费,每个月也才99元,促销期甚至更低,完全可以接受吧。 备案成功后,该服务器就没什么作用了,让它180天后自然欠费销毁得了。 服务器销毁后会有什么影响?答:没有任何影响。 但是。。。。。 你备案的域名最后还得指向一个网站,因为腾讯云会应工信部的要求定期检查网站是否合规,所以你还是要建一个简单的网站,(备案期间,可以暂时不管网站的事,等将来需要的时候再管理)。 至于有多简单,答,多简单都行。此时你可以在七牛、腾讯云、阿里云租点免费的对象存储空间,做个简单的网站。 4、在进行ICP备案之前,你需要在腾讯云注册你的域名地址,如果你已有域名,但不在腾讯云,建议先将要域名过户到腾讯云的账号上。 5、进入控制台,开始ICP备案,这个流程就不介绍了,因为完全一看就懂。而且现在使用备案小程序后,不需要幕布或现场拍照了,极其方便,大家跟着流程走就一点问题没有,有人脸识别和在线拍一段小视频。另外,大家可以随便作,随便填,填错或者填得不合适也不用怕,会有专门的备案客服打电话告诉你哪哪要改,还会告诉你应该怎么填才更容易通过工信部的审核,客服的态度好得发指。 仅说一点其中的几个小坑: a、人脸识别的时候,白色背景、白色背景、白色背景,笔者在人脸识别的时候,满世界找白墙,结果还被打回来重拍了3次。 b、网站用途一律写:公司官网,好通过工信部审核。 6、腾讯云提交资料到工信部审核。这是一个漫长的让人无语的等待,20-30天。笔者最近两次都是20天才过审;不要幻想会有可能提前完成审核,这是政府部门在审核,提前完成说明某政府人员的工作安排有问题,会犯错误的。 7、备案成功后,会有短信通知你,但是,你需要去工信部网站查询结果,并将结果切屏拷贝下来,因为小程序类目审核需要上传这张图片。http://beian.miit.gov.cn/publish/query/indexFirst.action [图片] 把上面这张图片保存好,小程序类目审核的时候需要上传。收到通知后,如果在这里查不到结果,也别急,据说需要24小时。 8、接下来是小程序上线审核。 因为ICP备案的小程序内容肯定涉及到社交,最后小程序上线时还要提交到工信部审核,还需要7天左右的时间,加上前面ICP备案的时间,加起来怎么也得30-40天。大家估计时间,别影响小程序上线。这7天也是政府部门在审核,不要幻想会提前。 9、计算一下时间: 腾讯云注册账号和认证:1-3天; 域名备案:腾讯云环节:1-3天; 域名备案:工信部环节:20-30天; 小程序添加服务类目:社交类目审核:1-3天; 小程序上线审核:腾讯环节:1-2天; 小程序上线审核:工信部环节:7+天; 总天数:30-40天; 10、节省时间的一些建议: 在开发小程序之前,就开始备案工作,小程序可以同时开发,相互不影响; 在开发完成之前一、两星期之内,先发布一版小程序,别管功能是不是完整,能通过审核就行,这样会有7天的等待类目审核的时间,这个时间里,小程序可以照常开发,不影响进度; 只要是社交类,基本需要有文字和图片安全检查功能,别忘了加上,别到时审核通过不了。 11、结束。 [图片]
2021-01-19 - 「笔记」个人主体迁移到企业主体小程序踩坑
前言 作为开发者,相信很多人都是个人开发小程序,想着等以后做大了或者有公司后再迁移。 迁移 小程序的迁移过程并不复杂,根据后台提示进行操作,提交相关资料就可以了。 迁移中的注意事项 如果迁移的企业和原主体是同一个管理员的话可以选择不变更小程序管理员,申请函中填同一个人的手机号或者只填一个就可以,但是小程序后台填资料的时候一番操作后会告诉你目标主体和原主体不能使用同一个手机号码,必须使用不同的号码(大部分人都有多个手机号,所以这个也不是什么大问题),迁移过程中审核人员会分别拨打2个号码进行确认,而且是上一个号码拨打完立马就拨打下一个,所以如果是同一个人的话最好手机都在身边。 迁移成功后 收到迁移成功后可能以为就万事大吉了,但是你登录后台的时候会发现小程序的主体信息变更了,但是微信认证状态是[代码]未认证[代码],这时候我不知道其他人是什么想法,反正我看到第一时间可能是很多头羊驼在奔腾,如果需要认证的话还需要走一遍认证流程。 在迁移后未认证状态的小程序大部分功能使用,比如[代码]业务域名[代码]可以直接配置,目前不知道对实际审核会有什么影响。 迁移后可复用公众号资质认证 经过上面的误解,最后发现通过公众号后台的小程序管理里点开迁移过来的小程序详情可以[代码]复用公众号资质认证[代码]进行免费认证,切记不要在小程序后台里走认证流程,那个还是需要支付300。 [图片] 本文仅代表个人观点,希望大家不要花冤枉钱。
2020-06-11 - 小程序消息推送,订阅消息的实现,借助云开发云函数实现定时推送订阅消息功能
我在云开发基础课程里给大家讲过小程序消息推送功能的实现,等下会给大家回顾下。但是有时候我们如果想实现定时推送的功能该怎么做呢 一,普通订阅消息的发送 我们先来看下订阅消息的官方简介。 [图片] 接下来我们就来借助云开发,来快速实现小程序消息推送的功能。 1-1,获取模板 ID 这一步和我们之前的模板消息推送是一样的,也是先添加模板,然后拿到模板id [图片] 首先是开通订阅消息功能,很简单,如下图 [图片] [图片] 由于长期性订阅消息,目前仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。仅就线下公共服务这一点,长期性订阅消息就和大部分开发者无缘了。 所以我们这里只能以使用一次性订阅消息为例。 [图片] 如上图,我们从公共模板库里选择一个一次性订阅的模板。然后编辑模板如下图 [图片] 下图就是我们添加好的模板,下图的模板id就是我们需要的。 [图片] 1-2,请求用户授权 我们做订阅消息授权时,只能是用户点击或者支付完成后才可以调起来授权弹窗,官方是这么要求的: [图片] 我们这里用到了wx.requestSubscribeMessage这个方法,来获取用户的授权。 1,编写index.wxml代码 [图片] 2,编写index.js代码,实现点击获取授权 [图片] 这一步tmplIds里的一串字符,就是我们自己添加的模板id [图片] 3,点击按钮运行效果如下 开发者工具模拟器上点击授权弹窗是这样的: [图片] 手机上的授权弹窗是这样的: [图片] 可以看到,这里显示的就是我们添加的 ‘上课提醒’的模板。 细心的同学可以看到, 真机上多了一个 ‘总是保持以上选择,不再询问’ 其实,你自己仔细多品一些。也能明白,我们正常订阅消息授权时,用户允许的话,你只能推送一次消息。也就是用户允许一次,我们就可以推送一条消息给用户,并且这个允许不存在过期。所以我们可以让用户尽量多的点击允许,这样我们就可以尽量多的给用户发送消息了。 这里用户允许后,我们就可以给用户推送消息了,接下来我们来借助云开发的云函数来实现消息推送功能。 1-3,获取用户的opneid 先来看官方爸爸是怎么说的。 [图片] 可以看出官方提供了两种方式,我们这里使用云调用。说白了就是在云函数里调用推送功能。 推送所需参数 [图片] 可以看到我这里用来openapi功能,并且需要用到用户的opneid,关于openid的获取,我之前有写过文章,也录过视频的。文章的话,大家去翻下我历史的文章,视频的话,点击这个即可:《借助云函数获取用户openid》 这里的openid的获取我就不再详细讲解了,把对应云函数的代码给大家贴出来。 [图片] 在使用云开发时,有几点需要注意的 1,需要在project.config.json里创建云函数目录如下图 [图片] 2,需要在app.js里初始化云开发环境 [图片] 至于云开发的环境id从哪里拿,我视频里也讲过很多遍了,直接去看我视频或者翻看我历史文章即可。 《零基础入门云开发视频》 1-4,用云函数实现消息推送 我们只需要创建一个云函数如下,然后填入用户的openid,要跳转的小程序页面链接,模板内容,模板id即可。通常这些数据都应该传进来,简单起见,我就把这里的模板内容写成固定的。 [图片] 注意:我在编写上面的代码时,推送内容的key必须和小程序模板里的key保持一致,否则就会报如下错误。 [图片] 然后看下调用这个云函数的地方 [图片] 如果用户没有授权,我们推送会报如下错误 [图片] 如果用户授权过,我们就可以成功推送了,推送后的打印日志如下 [图片] 还记得我们真机上的授权吗,如果用户只是点击了允许,没有选择一直允许,那我我们在推送成功一次后,如果再次推送,就需要用户重新授权。否则,还是会报这个错误的 [图片] 所以我们用户点击一次允许,我们就可以推送一次消息,比如,我点击了4次允许那么我就可以成功的推送4次 [图片] 效果图 [图片] 可以看到,我们成功的收到 上课提醒的模板消息,点击进去,就是我们具体的推送内容 [图片] 其实我这是连续收到了4条消息,因为我点击了4次允许推送,所以就可以成功的推送4次。 到这里我们就完整的实现模板消息推送功能了,下面我把主要代码贴给大家,大家也可以私信我获取完整源码。 index.wxml [代码]<button bindtap="shouquan" type='primary'>获取订阅消息授权</button> <button bindtap="getOpenid">获取用户的openid并推送消息</button> [代码] index.js [代码]//编程小石头wechat:2501902696 Page({ //获取授权的点击事件 shouquan() { wx.requestSubscribeMessage({ tmplIds: ['CFeSWarQLMPyPjwmiy6AV4eB-IZcipu48V8bFLkBzTU'], //这里填入我们生成的模板id success(res) { console.log('授权成功', res) }, fail(res) { console.log('授权失败', res) } }) }, //获取用户的openid getOpenid() { wx.cloud.callFunction({ name: "getopenid" }).then(res => { let openid = res.result.openid console.log("获取openid成功", openid) this.send(openid) }).catch(res => { console.log("获取openid失败", res) }) }, //发送模板消息到指定用户,推送之前要先获取用户的openid send(openid) { wx.cloud.callFunction({ name: "sendMsg", data: { openid: openid } }).then(res => { console.log("推送消息成功", res) }).catch(res => { console.log("推送消息失败", res) }) } }) [代码] 推送对应的云函数 [代码]//编程小石头wechat:2501902696 const cloud = require('wx-server-sdk') cloud.init() exports.main = async(event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ touser: event.openid, //要推送给那个用户 page: 'pages/index/index', //要跳转到那个小程序页面 data: {//推送的内容 thing1: { value: '小程序入门课程' }, thing6: { value: '杭州浙江大学' }, thing7: { value: '第一章第一节' } }, templateId: 'CFeSWarQLMPyPjwmiy6AV4eB-IZcipu48V8bFLkBzTU' //模板id }) console.log(result) return result } catch (err) { console.log(err) return err } } [代码] 后面我会分享更多小程序相关的知识出来,请持续关注。 注意:授权一次,只能发送一条消息。 二,定时发送消息 我们上面用户授权和发送消息都需要手动点击才可以实现发送。但是有时候我们需要定时提醒用户,比如做的闹钟小程序,要定时提醒用户,该怎么做呢,接下来我们就来实现定时发送消息的功能。 注意 当然了这里还是要先授权才可以发送消息的,同样也是授权一次可以发送一条消息,所以这里要尽量先多授权几次 2-1,什么是定时触发器 我们实现定时发送的功能就是要用到云函数里的定时触发器,官方介绍如下。 [图片] 大家有时间可以自己去仔细读下 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html [图片] 官方已经教我们怎么写定时触发器了 2-2,定时触发器时间设置规则 建议大家仔细去读下官方文档。 [图片] 下面是官方给出的一些示例 [图片] 我这里就取用每隔5秒通过该定时触发器调用下我们的云函数,实现订阅消息的发送。 2-3,添加定时触发器 添加步骤如下图,我们需要新建一个云函数timer [图片] 我们要在timer云函数里调用我们的fasong云函数来实现发送功能 [图片] 然后在timer文件夹下新建一个config.json文件 [图片] [图片] 然后给config.json做如下配置 [图片] 注意json里不能有注释,配置好的触发器如下 [图片] 2-4,部署定时触发器 添加好以后,记得部署触发器 [图片] 2-5,定时发送效果 首先看定时触发器是不是每隔5秒执行了一次 [图片] 然后看手机是否接到了消息 [图片] 可以看出我们手机上每隔5秒也接到了消息。这里还是要记得多授权才可以多接消息。 当然了,我们不可能这样每隔5秒给客户发条消息,这样骚扰到客户,很容易被封的,所以可以停止触发器 2-6,停止触发器 [图片] 到这里我们的定时发送消息功能也实现了,当然了我们要发给指定用户,就要先去获取用户openid,并且得让用户多授权。
2022-02-28 - 云开发轮询实现定时推送订阅消息
前言 受众:已有小程序和云开发经验(没有的话照着流程和官方文档也应该可以实现) 关于小程序的消息推送,我了解到的有以下几种实现方式 1、模板消息,已于2020 年 1 月 10 日下线 2、通过服务端的统一服务消息下发推送,[代码]因为模板消息现已下线,现只支持公众号[代码]。统一服务消息官方文档 2、通过关注公众号通过公众号实现长期的消息推送 3、订阅消息,包含一次性订阅消息和长期订阅消息 订阅消息官方文档 关于技术实现的选择 关于小程序的消息推送的几种实现方式,先简单说一下各自的优缺点: 1、统一服务消息 优点:可以长期多次发送 缺点:因为模板消息现已下线,现只支持公众号 2、通过公众号实现 优点:可以长期多次发送 缺点:需要引导关注公众号,没有公众号还得注册一个,以下还有一些注意事项 1、公众号和小程序需要在同一个微信开放平台下,保证拿到相同的UnionID 2、如果需要在消息模板上加上小程序的入口,需要微信公众号和小程序做关联 3、小程序和公众号都必须是认证过的 4、小程序需要提前知道公众号的appid和appsecret 5、发送消息之前需要拿到用户对应于公众号的openid 3、订阅消息实现 订阅消息,包含一次性订阅消息和长期订阅消息,可惜[代码]长期订阅消息只对指定类目开放[代码] 一次性订阅消息 优点:服务端,云开发都可以实现推送 缺点:每次需要授权,每次授权同意只推送一次 哎,没什么可挑的,[代码]最终选择的是订阅消息的一次性订阅消息[代码],下面是微信官方介绍 小程序订阅消息 功能介绍 消息能力是小程序能力中的重要组成,我们为开发者提供了订阅消息能力,以便实现服务的闭环和更优的体验。 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面 [图片] 消息类型 1. 一次性订阅消息 一次性订阅消息用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 2. 长期订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 [代码]目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。[代码] 3. 设备订阅消息 设备订阅消息是一种特殊类型的订阅消息,它属于长期订阅消息类型,且需要完成「设备接入」才能使用。 设备订阅消息用于在设备触发某些需要人工介入的事件时(例如设备发生故障、设备耗材不足等),向用户发送消息通知。详见设备订阅消息文档。 使用说明 步骤一:获取模板 ID 在微信公众平台手动配置获取模板 ID: 登录 https://mp.weixin.qq.com 获取模板,如果没有合适的模板,可以申请添加新模板,审核通过后可使用。 [图片] 步骤二:获取下发权限 一次性订阅消息、长期订阅消息,详见接口 wx.requestSubscribeMessage 设备订阅消息,详见接口 wx.requestSubscribeDeviceMessage 步骤三:调用接口下发订阅消息 一次性订阅消息、长期订阅消息,详见服务端接口 subscribeMessage.send 设备订阅消息,详见服务端接口 hardwareDevice.send 注意事项 用户勾选 “总是保持以上选择,不再询问” 之后,下次订阅调用 wx.requestSubscribeMessage 不会弹窗,保持之前的选择,修改选择需要打开小程序设置进行修改。 实现 设计思路 我这里的需求是纪念日的推送,到用户设置的纪念日当天或者前几天,需要给用户推送一条消息,提醒用户纪念日已到。因为是一次性授权,首先要考虑授权的时机,因此考虑是在用户新增或者编辑纪念日信息那里,设置一个开关,开启时判断通知权限,无权限提醒到设置页开启通知,有权限请求授权,授权成功开关开启,失败或者未开启通知权限开关关闭,最后提交信息到数据库,推送的接口会每天轮询一次,根据这个状态和是否到纪念日提醒时间判断是否进行推送,推送成功重置状态。推送之后用户再次开启推送开关可实现下一次推送,形成一个闭环。 [图片] 实现流程 首先要去小程序后台选择一个模板 然后在新增编辑页面实现设计的逻辑(包含通知权限的判断,请求一次性订阅,失败关闭开关状态,保存数据到数据库) 云开发轮询推送消息(先查出需要推送的数据,然后对应模板的数据格式发送推送) 具体实现 1、选择模板 登录小程序开发后台 - 订阅消息 - 公共模板库搜索选择一个合适的模板,然后在我的模板那里可以查看模板详情,[代码]模板id[代码]和[代码]详情里的字段[代码]在云开发那里需要使用 [图片] [图片] [图片] 2、编辑页代码实现 首先先把开关相关的页面和逻辑实现,这里就不细说。 然后是判断通知权限和请求一次性订阅 2.1、判断通知权限 判断通知权限使用的是wx.getSetting [代码]wx.getSetting({ withSubscriptions: true, success (res) { console.log(res.authSetting) // res.authSetting = { // "scope.userInfo": true, // "scope.userLocation": true // } console.log(res.subscriptionsSetting) // res.subscriptionsSetting = { // mainSwitch: true, // 订阅消息总开关 // itemSettings: { // 每一项开关 // SYS_MSG_TYPE_INTERACTIVE: 'accept', // 小游戏系统订阅消息 // SYS_MSG_TYPE_RANK: 'accept' // zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: 'reject', // 普通一次性订阅消息 // ke_OZC_66gZxALLcsuI7ilCJSP2OJ2vWo2ooUPpkWrw: 'ban', // } // } } }) [代码] 2.2、请求一次性订阅 请求一次性订阅使用的是wx.requestSubscribeMessage [代码]wx.requestSubscribeMessage({ tmplIds: [''], // 模板id success (res) { } }) [代码] 2.3、把数据存储到云数据库 每个项目数据结构不一样,这里也不展开说 核心代码 [代码] // 是否提醒 onSwitchChange: function (event) { this.setData({ ["dict.isPush"]: event.detail.value }) if (!event.detail.value) { this.setData({ ["dict.pushTime"]: '' }) } if (event.detail.value) { this.checkAndRequestSubscribeMessage() } }, // 检查订阅消息权限,未开启提示前往开启,已开启请求订阅消息 checkAndRequestSubscribeMessage() { let that = this wx.getSetting({ withSubscriptions: true, success(res) { console.log(res.subscriptionsSetting) // 订阅消息总开关是否开启 if (!res.subscriptionsSetting.mainSwitch) { that.subscriptionFailed() wx.showModal({ title: '提示', content: '当前暂未开启接消息提醒,是否前往设置页开启?', success(res) { if (res.confirm) { wx.openSetting() } } }) } else { let templateId = 'c64Gp5-89xyD55rnDr0oBWQNphWlNm_l4MX-Sduuj2c' // 模板ID wx.requestSubscribeMessage({ tmplIds: [templateId], success(res) { console.log(res) // 申请订阅成功,将订阅信息调用云函数存入云开发数据 if (res.errMsg === 'requestSubscribeMessage:ok') { // res[templateId]: 'accept'、'reject'、'ban'、'filter' if (res[templateId] == 'accept') {} else { that.subscriptionFailed() } } }, fail(err) { console.log(err) that.subscriptionFailed() wx.showToast({ title: '订阅失败', icon: 'none' }) } }) } } }) }, // 订阅失败 subscriptionFailed() { this.setData({ ["dict.isPush"]: false }) }, [代码] 3、云开发轮询推送 这里才是重点!!!首先在项目中选择云函数目录右键新建一个云函数,然后就需要在这个云函数中实现轮询推送的代码了,我这里建的云函数是[代码]push[代码] [图片] 3.1、推送 推送使用的是服务端的subscribeMessage.send方法。 这里是的是云调用进行推送,这样可以使用云开发 [代码]subscribeMessage.send[代码]使用需要在云函数代码的[代码]config.json[代码]文件中配置 [代码]subscribeMessage.send[代码] API 的权限,详情 配置如下 [代码] "permissions": { "openapi": ["subscribeMessage.send"] }, [代码] [图片] 请求参数 属性 类型 默认值 必填 说明 touser string 是 接收者(用户)的 openid templateId string 是 所需下发的订阅模板id page string 否 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。 data Object 是 模板内容,格式形如 { “key1”: { “value”: any }, “key2”: { “value”: any } } miniprogramState string 否 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 lang string 否 进入小程序查看”的语言类型,支持zh_CN(简体中文)、en_US(英文)、zh_HK(繁体中文)、zh_TW(繁体中文),默认为zh_CN 官方请求示例 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) exports.main = async (event, context) => { try { const result = await cloud.openapi.subscribeMessage.send({ "touser": 'OPENID', "page": 'index', "lang": 'zh_CN', "data": { "number01": { "value": '339208499' }, "date01": { "value": '2015年01月05日' }, "site01": { "value": 'TIT创意园' }, "site02": { "value": '广州市新港中路397号' } }, "templateId": 'TEMPLATE_ID', "miniprogramState": 'developer' }) return result } catch (err) { return err } } [代码] [代码]miniprogramState[代码]在正式环境要换成[代码]formal[代码]或注释掉这行 3.2、轮询 轮询使用的云开发的定时触发器 [图片] 定时触发器官方介绍 该功能需开发者工具 1.02.1811270 及以上版本方可使用 从开发者工具 1.02.1910182 开始,新上传的定时触发器内支持使用云调用 如果云函数需要定时 / 定期执行,也就是定时触发,我们可以使用云函数定时触发器。配置了定时触发器的云函数,会在相应时间点被自动触发,函数的返回结果不会返回给调用方。 在需要添加触发器的云函数目录下新建文件 [代码]config.json[代码],格式如下: [代码]{ // triggers 字段是触发器数组,目前仅支持一个触发器,即数组只能填写一个,不可添加多个 "triggers": [ { // name: 触发器的名字,规则见下方说明 "name": "myTrigger", // type: 触发器类型,目前仅支持 timer (即 定时触发器) "type": "timer", // config: 触发器配置,在定时触发器下,config 格式为 cron 表达式,规则见下方说明 "config": "0 0 2 1 * * *" } ] } [代码] 官方示例 下面展示了一些 Cron 表达式和相关含义的示例: */5 * * * * * * 表示每5秒触发一次 0 0 2 1 * * * 表示在每月的1日的凌晨2点触发 0 15 10 * * MON-FRI * 表示在周一到周五每天上午10:15触发 0 0 10,14,16 * * * * 表示在每天上午10点,下午2点,4点触发 0 */30 9-17 * * * * 表示在每天上午9点到下午5点内每半小时触发 0 0 12 * * WED * 表示在每个星期三中午12点触发 [代码]triggers[代码]中的[代码]config[代码]字段可以控制触发的频率,具体开发测试时我使用的是50秒调用一次 [代码] "config": "*/50 * * * * * *" [代码] [代码]这里有个坑,如果代码实现上传并部署云函数之后,左等右等在日志中看不到日志,因为少了一个步骤,在上传并部署云函数之后,需要右键云函数上传触发器,这样才生效,想关闭可以删除触发器[代码] 轮询推送完整代码 config.json [代码]{ "permissions": { "openapi": ["subscribeMessage.send"] }, "triggers": [{ "name": "myTimer", "type": "timer", "config": "0 0 8 * * * *" }] } [代码] index.js [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') // 初始化 cloud cloud.init({ // API 调用都保持和云函数当前所在环境一致 env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command const $ = db.command.aggregate const kTableName = '换成自己的表名' // 云函数入口函数 exports.main = async (event, context) => { try { // 从云开发数据库中查询等待发送的消息列表 const msgArr = await db .collection(kTableName) // 查询条件,已开启推送,并且提醒时间为今天 .where({ A_IsPush: true, A_PushTime: timeStampToTime(new Date().getTime(), '{y}/{m}/{d}') }) .get() // 循环消息列表 const sendPromises = msgArr.data.map(async msgData => { try { // 发送订阅消息 await cloud.openapi.subscribeMessage.send({ touser: msgData._openid, // 要发送用户的openid page: 'pages/home/home', // 用户通过消息通知点击进入小程序的页面 lang: 'zh_CN', templateId: 'c64Gp5-89xyD55rnDr0oBWQNphWlNm_l4MX-Sduuj2c', // 订阅消息模板ID // 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 // miniprogramState: 'developer', // 要发送的数据,要和模板一致 data: { // 纪念日名称 thing5: { value: msgData.A_Title }, // 纪念日时间 time2: { value: msgData.A_Time }, // 备注 thing4: { value: msgData.A_Remarks ? msgData.A_Remarks : '无' }, } }) // 发送成功后将数据状态重置 return db .collection(kTableName) .doc(msgData._id) .update({ data: { A_IsPush: false, A_PushTime: '', A_NextTime: '', }, }) } catch (e) { return e } }) return Promise.all(sendPromises) } catch (err) { console.log(err) return err } } function timeStampToTime(time, cFormat) { if (arguments.length === 0) { return null } const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' let date if (typeof time === 'object') {} else { if (('' + time).length === 10) time = parseInt(time) * 1000 date = new Date(time) } const formatObj = { y: date.getFullYear(), m: date.getMonth() + 1, d: date.getDate(), h: date.getHours(), i: date.getMinutes(), s: date.getSeconds(), w: date.getDay() } const time_str = format.replace(/{(y|m|d|h|i|s|w)+}/g, (result, key) => { let value = formatObj[key] if (key === 'w') { return ['日', '一', '二', '三', '四', '五', '六'][value] } if (result.length > 0 && value < 10) { value = '0' + value } return value || 0 }) return time_str } // 定时触发器 // https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/functions/triggers.html // 50秒一次 // "config": "*/50 * * * * * *" // 每天上午8点一次 // "config": "0 0 8 * * * *" [代码] package.json [代码]{ "name": "push", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.6.1" } } [代码] 3.3、上传并部署云函数 所有代码实现之后,可以先把项目环境设置为开发环境,然后右键上传并部署云函数,然后上传触发器 [图片] 然后打开云函数控制台,选择云函数-日志,进行查看状态,如果成功并且有需要推送的数据,手机会收到推送消息,失败的话根据日志进行修改。 [图片] 至此结束 最后推荐一下我的小程序,[代码]我的纪念日小助手[代码] [图片]
2022-05-16 - 云函数触发定时器的时间可以动态设置吗?
动态传递一个时间给定时器应该怎么做?
2020-07-18 - 小程序eval/Function终极替代方案:eval5
由于小程序内部禁用了eval、Function导致在一些场景下无法动态执行脚本,小程序又只支持JavaScript开发,如果想要在在前端动态执行脚本就得实现对于的脚本解释器。 eval5是完全基于JavaScript编写的JavaScript解释器,支持ECMA5语法 项目地址:https://github.com/bplok20010/eval5 实现原理: 使用acorn或esprima编译器对JavaScript代码进行编译并得到抽象语法树(AST)用JavaScript解析语法树并得到计算结果例如:1+1的解析 一、使用acorn编译后得到的语法树,语法树由不同的节点组成各个节点的type代表着不同的语句或表达式类型: { "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Literal", "value": 1, "raw": "1" }, "right": { "type": "Literal", "value": 1, "raw": "1" } } } ], "sourceType": "script" } 二、得到语法树后,我们可以根据不同的节点类型实现不同的处理函数,例如: function handleBinaryExpression(node) { switch( node.operator ) { case '+': return node.left.value + node.right.value; case '-': return node.left.value - node.right.value; } } 如何使用 一、使用npm安装: npm install --save eval5 二、使用打包好的eval5.js 使用示例: // npm install --save eval5 import {Function,evaluate} from 'eval5'; //or 'path/eval5.js' const sum = new Function('a', 'b', 'return a+b'); sum(100,200) evaluate('1+1') eval5基于小程序编写的示例:eval5-wx-demo github地址 [图片]
2020-02-19 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 新富文本组件
mp-html小程序富文本组件 news欢迎加入 QQ 交流群:699734691示例小程序添加获取组件包功能[图片] 功能介绍 支持在多个平台使用 支持丰富的标签(包括 table、video、svg 等) 支持丰富的事件效果(自动预览图片、链接处理等) 支持锚点跳转、长按复制等丰富功能 支持大部分 html 实体 丰富的插件(关键词搜索、内容编辑等) 效率高、容错性强且轻量化使用方法1. npm 方式 在项目根目录下执行 npm install mp-html 开发者工具中勾选 使用 npm 模块 并点击 工具 - 构建 npm 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "mp-html" } } 在需要使用页面的 wxml 文件中添加 <mp-html content="{{html}}" /> 在需要使用页面的 js 文件中添加 Page({ onLoad() { this.setData({ html: 'Hello World!' }) } }) 2. 源码方式 将源码中的代码包(dist/mp-weixin)拷贝到 components 目录下,更名为 mp-html 在需要使用页面的 json 文件中添加 { "usingComponents": { "mp-html": "/components/mp-html/index" } } 后续步骤同上 获取github 链接:https://github.com/jin-yufeng/mp-html npm 链接:https://www.npmjs.com/package/mp-html 文档链接:https://jin-yufeng.gitee.io/mp-html
2022-03-04 - weapp-qrcode-canvas-2d在微信小程序中生成二维码,新版canvas-2d接口
weapp-qrcode-canvas-2d weapp-qrcode-canvas-2d 是使用新版canvas-2d接口在微信小程序中生成二维码(外部二维码)的js包。canvas 2d 接口支持同层渲染且性能更佳,建议切换使用,可大幅提升生成图片的速度。 仓库地址 weapp-qrcode-canvas-2d【码云gitee】 weapp-qrcode-canvas-2d【github】 [图片] 测试环境 微信小程序基础库版本:2.10.4 开发者工具版本:Stable 1.03.2101150 Usage 先在 wxml 文件中,创建绘制的 [代码]canvas[代码],并定义好 [代码]width[代码], [代码]height[代码], [代码]id[代码] , [代码]type[代码] ,其中type的值必须为[代码]2d[代码] [代码]<canvas type="2d" style="width: 260px; height: 260px;" id="myQrcode"></canvas> [代码] 安装方法1:直接引入 js 文件 直接引入 js 文件,使用 [代码]drawQrcode()[代码] 绘制二维码 [代码]// 将 dist 目录下,weapp.qrcode.esm.js 复制到项目中。路径根据实际引用的页面路径自行改变 import drawQrcode from '../../utils/weapp.qrcode.esm.js' [代码] 安装方法2:npm安装 [代码]npm install weapp-qrcode-canvas-2d --save [代码] // 然后需要在小程序开发者工具中:构建npm [代码]import drawQrcode from 'weapp-qrcode-canvas-2d' [代码] 安装完成后调用 例子1:没有使用叠加图片 [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node // 调用方法drawQrcode生成二维码 drawQrcode({ canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, background: '#ffffff', foreground: '#000000', text: 'abc', }) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ canvasId: 'myQrcode', canvas: canvas, x: 0, y: 0, width: 260, height: 260, destWidth: 260, destHeight: 260, success(res) { console.log('二维码临时路径:', res.tempFilePath) }, fail(res) { console.error(res) } }) }) [代码] 例子2:使用叠加图片(在二维码中加logo) [代码]const query = wx.createSelectorQuery() query.select('#myQrcode') .fields({ node: true, size: true }) .exec((res) => { var canvas = res[0].node var img = canvas.createImage(); img.src = "/image/logo.png" img.onload = function () { // img.onload完成后才能调用 drawQrcode方法 var options = { canvas: canvas, canvasId: 'myQrcode', width: 260, padding: 30, paddingColor: '#fff', background: '#fff', foreground: '#000000', text: '123456789', image: { imageResource: img, width: 80, // 建议不要设置过大,以免影响扫码 height: 80, // 建议不要设置过大,以免影响扫码 round: true // Logo图片是否为圆形 } } drawQrcode(options) // 获取临时路径(得到之后,想干嘛就干嘛了) wx.canvasToTempFilePath({ x: 0, y: 0, width: 260, height: 260, destWidth: 600, destHeight: 600, canvasId: 'myQrcode', canvas: canvas, success(res) { console.log('二维码临时路径为:', res.tempFilePath) }, fail(res) { console.error(res) } }) }; }) [代码] API drawQrcode([options]) options Type: Object 参数 必须 说明 示例 canvas 必须 画布标识,传入 canvas 组件实例 canvasId 非 绘制的[代码]canvasId[代码] [代码]'myQrcode'[代码] text 必须 二维码内容 ‘123456789’ width 非 二维码宽度,与[代码]canvas[代码]的[代码]width[代码]保持一致 260 padding 非 空白内边距 20 paddingColor 非 内边距颜色 默认与background一致 background 非 二维码背景颜色,默认值白色 [代码]'#ffffff'[代码] foreground 非 二维码前景色,默认值黑色 [代码]'#000000'[代码] typeNumber 非 二维码的计算模式,默认值-1 8 correctLevel 非 二维码纠错级别,默认值为高级,取值:[代码]{ L: 1, M: 0, Q: 3, H: 2 }[代码] 1 image 非 在 canvas 上绘制图片,层级高于二维码,v1.1.1+版本支持。具体使用见:例子2 [代码]{imageResource: '', width:80, height: 80, round: true}[代码]
2023-04-02 - 微信小程序中字符串数学式子怎么直接执行
后台返的数学公式,通过代码把值放进去,在微信小程序中怎么计算 eval在微信小程序中不支持 求解
2019-04-17 - 云开发http请求的两种写法
对于简单的GET表单请求 可以直接将参数封装在url中 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') // 云函数入口函数 exports.main = async (event, context) => { //qz return new Promise((resolve, reject) => { request({ url: event.URL, method: "POST",//GET json: true, headers: { "content-type": "application/json", "token": event.token }, }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] 对于POST请求 参数不好封装的 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { //这里写普通话成绩查询方式 return new Promise((resolve, reject) => { request({ url: event.url, method: "POST", json: true, headers: { "content-type": "application/json", "token":event.token }, body: event.body }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] body中填写需要的参数 body是json形式 [代码]{ xxx:xxx } [代码] 请求头可以根据自己的需要进行修改。
2019-05-28 - 小程序 vtabs组件滑动跳转混乱?
使用官方提供的vtabs 示例中的代码,开发垂直滑动选项卡。 右侧内容项中的高度是根据内容的大小随机设定的,然后就会出现选项卡切换混乱的问题,当给右侧内容项设定固定高度时,又没有问题。 请问存在什么bug呢?
2020-10-14 - 小程序粘性布局组件实现
一、前言 开发中,我们经常会遇需要让组件在屏幕范围内时,按照正常布局排列,而组件滚出屏幕范围时,让其始终固定在屏幕顶部的情况,也就是常说的粘性布局。今天我们就一起用小程序来实现一个适用于不同场景下的粘性布局组件。 二、demo演示 如图,实现的组件主要适用于以下几种场景: 吸顶页面最上方; 吸顶与页面有固定距离的位置; 在指定容器内吸顶; 嵌套在scroll-view中吸顶。 [图片] 三、代码演示 其中,粘性组件通过<weimob-sticky></weimob-sticky>调用,参数信息用法如下: 参数 说明 类型 默认值 offset-top 吸顶时与顶部的距离,单位px number 0 z-index 吸顶时的 z-index number 99 container 一个函数,返回容器对应的 NodesRef 节点 function - scroll-top 当前滚动区域的滚动位置,非 null 时会禁用页面滚动事件的监听 number - 滚动时触发scroll函数,其中isFixed为是否吸顶,scrollTop为距离顶部的位置。详细代码如下。 3.1 页面代码 3.1.1 基础用法 [代码]<view class="weimob-block"> <view class="weimob-title">基础用法</view> <view class="weimob-body"> <weimob-sticky> <!-- 需要粘性的部分 --> <button class="margin-left-base" size="mini"> 基础用法 </button> </weimob-sticky> </view> </view> [代码] 3.1.2 吸顶距离 [代码]<view class="weimob-block"> <view class="weimob-title">吸顶距离</view> <view class="weimob-body"> <!-- 吸顶时与顶部的距离,单位px --> <weimob-sticky offset-top="{{ 50 }}"> <!-- 需要粘性的部分 --> <button class="margin-left-top" type="primary" size="mini"> 吸顶距离 </button> </weimob-sticky> </view> </view> [代码] 3.1.3 指定容器 [代码]<view class="weimob-block"> <view class="weimob-title">指定容器</view> <view class="weimob-body"> <!-- 这里需要固定高度 --> <view id="container" style="height: 300rpx;background-color: #fff"> <weimob-sticky container="{{ container }}"> <button size="mini" class="margin-left-special"> 指定容器 </button> </weimob-sticky> </view> </view> </view> [代码] 3.1.4 嵌套在scroll-view使用 [代码]<view class="weimob-block"> <view class="weimob-title">嵌套在 scroll-view 内使用</view> <!-- 这里需要固定高度,scroll-view里的元素高度需要大于其高度 --> <scroll-view bind:scroll="onScroll" scroll-y id="scroller" style="height: 400rpx; background-color: #fff;margin-top: 40rpx;" > <view style="height: 800rpx"> <weimob-sticky scroll-top="{{ scrollTop }}" offset-top="{{ offsetTop }}" > <button size="mini" class="margin-left-scoll"> 嵌套在 scroll-view 内 </button> </weimob-sticky> </view> </scroll-view> </view> [代码] 页面js [代码]Page({ data: { container: null, //一个函数,返回容器对应的 NodesRef 节点 scrollTop: 60, // 当前滚动区域的滚动位置,非null时会禁用页面滚动事件的监听 offsetTop: 0 // 吸顶时与顶部的距离,单位px }, onReady() { // 页面渲染完,获取节点信息 this.setData({ container: () => wx.createSelectorQuery().select('#container'), }); }, onScroll(event) { // 容器滚动时获取节点信息 wx.createSelectorQuery() .select('#scroller') .boundingClientRect((res) => { this.setData({ scrollTop: event.detail.scrollTop, offsetTop: res.top, }); }) .exec(); } }); [代码] 3.2 组件代码 组件wxml [代码]<wxs src="./index.wxs" module="computed" /> <view class="weimob-sticky" style="{{ computed.containerStyle({ fixed, height, zIndex }) }}" > <view class="{{ fixed ? 'weimob-sticky-wrap--fixed' : ''}}" style="{{ computed.wrapStyle({ fixed, offsetTop, transform, zIndex }) }}" > <slot /> </view> </view> [代码] 组件wxs 这里使用使用小程序的wxs对吸顶元素的transform,top,height,z-index元素进行实时渲染,ios设备在滚动监听时性能会优于在js 2-20倍,androd设备效率暂无差异。 [代码]function wrapStyle(data) { var style = ""; if (data.transform) { style += 'transform: translate3d(0, ' + data.transform + 'px, 0);' } if (data.fixed) { style += 'top: ' + data.offsetTop + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } function containerStyle(data) { var style = ""; if (data.fixed) { style += 'height: ' + data.height + 'px;' } if (data.zIndex) { style += 'z-index: ' + data.zIndex + ';' } return style; } module.exports = { wrapStyle: wrapStyle, containerStyle: containerStyle } [代码] 组件js [代码]import pageScrollMixin from "./page-scroll"; const ROOT_ELEMENT = ".weimob-sticky"; Component({ options: { multipleSlots: true }, properties: { zIndex: { type: Number, value: 99 }, offsetTop: { type: Number, value: 0, observer: "onScroll" }, disabled: { type: Boolean, observer: "onScroll" }, container: { type: null, observer: "onScroll" }, scrollTop: { type: null, observer(val) { this.onScroll({ scrollTop: val }); } } }, data: { height: 0, fixed: false, transform: 0 }, behaviors: [pageScrollMixin(function pageScrollMixinCallback(event) { // 非null时会禁用页面滚动事件的监听 if (this.data.scrollTop != null) { return; } this.onScroll(event); })], lifetimes: { attached() { this.onScroll(); } }, methods: { onScroll({ scrollTop } = {}) { const { container, offsetTop, disabled } = this.data; if (disabled) { this.setDataAfterDiff({ fixed: false, transform: 0 }); return; } this.scrollTop = scrollTop || this.scrollTop; if (typeof container === "function") { // 情况一:指定容器下时,吸顶距离+吸顶元素高度>容器高度+容器距顶部距离,随页面滚动; // 情况二:指定容器下时,吸顶距离>吸顶元素高度,元素固定; // 情况三:元素初始化。 // this.getRect获取节点ROOT_ELEMENT相对于显示区域的top,height等信息,通过root获取 // this.getContainerRect获取父容器相对于显示区域的top,height等信息,通过container获取 Promise.all([this.getRect(ROOT_ELEMENT), this.getContainerRect()]).then( ([root, container]) => { if (offsetTop + root.height > container.height + container.top) { this.setDataAfterDiff({ fixed: false, transform: container.height - root.height }); } else if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height, transform: 0 }); } else { this.setDataAfterDiff({ fixed: false, transform: 0 }); } }); return; }else{ this.getRect(ROOT_ELEMENT).then(root => { // 吸顶时与顶部的距离小于可视区域的top距离时,随着滚动条滚动,否则吸顶 if (offsetTop >= root.top) { this.setDataAfterDiff({ fixed: true, height: root.height }); this.transform = 0; } else { this.setDataAfterDiff({ fixed: false }); } return Promise.resolve(); }); } }, setDataAfterDiff(data) { // 比较数据是否与上次相同,不同则触发父组件scroll事件更新isFixed,scrollTop。 wx.nextTick(() => { const diff = Object.keys(data).reduce((prev, key) => { const prevCopy = prev; if (data[key] !== this.data[key]) { prevCopy[key] = data[key]; } return prevCopy; }, {}); this.setData(diff); this.triggerEvent("scroll", { scrollTop: this.scrollTop, isFixed: data.fixed || this.data.fixed }); }); }, getContainerRect() { const nodesRef = this.data.container(); return new Promise(resolve => nodesRef.boundingClientRect(resolve).exec()); }, getRect(selector) { return new Promise(resolve => { wx.createSelectorQuery().in(this).select(selector).boundingClientRect(rect => { resolve(rect); }).exec(); }); } } }); [代码] page-scroll.js 滚动事件在页面进入和离开时共享的pageScrollMixin函数。 [代码]function getCurrentPage() { const pages = getCurrentPages(); return pages[pages.length - 1] || {}; } function onPageScroll(event) { const { weimobPageScroller = [] } = getCurrentPage(); weimobPageScroller.forEach(scroller => { if (typeof scroller === "function" && event) { // @ts-ignore scroller(event); } }); } const pageScrollMixin = scroller => Behavior({ attached() { const page = getCurrentPage(); if (Array.isArray(page.weimobPageScroller)) { page.weimobPageScroller.push(scroller.bind(this)); } else { page.weimobPageScroller = typeof page.onPageScroll === "function" ? [page.onPageScroll.bind(page), scroller.bind(this)] : [scroller.bind(this)]; } page.onPageScroll = onPageScroll; }, detached() { const page = getCurrentPage(); page.weimobPageScroller = (page.weimobPageScroller || []).filter(item => item !== scroller); } }); export default pageScrollMixin; [代码] 总结 最后,我将上述代码放在了代码片段中供大家使用了解,https://developers.weixin.qq.com/s/qiym3wmr7znx ,希望能够帮到小伙伴们,欢迎评论区建议或指教哦~
2021-01-26 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 如何单页面设置navigationStyle: custom,使二级页面打开后退出功能不被遮挡?
在对主页设置时,将navigationStyle的值设为custom后,打开二级页面后箭头退出的地方被挤到顶部隐藏起来了,请教有没有好一点的方法单页面设置这个custom的功能
2020-06-29 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 云数据库聚合查询,利用$.setUnion或$.setDifference对数组型字段的元素去重
一、查遍文档也没找到去重的办法,最后发现这两个函数可以曲线救国 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.setUnion.html https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/database/command/aggregate/AggregateCommand.setDifference.html 假设记录中存在一个数组型字段suppliers,里面存在重复的元素,我们在输出时希望去重,给前台一份干净的数据。 suppliers: ['a', 'a', 'b', 'c'] 二、先试试setUnion,函数的作用是输出两个数组的并集,那么加入一个空数组,相当于去重,示例如下: await db.collection('orders') .aggregate() .addFields({ suppliersDistinct: $.setUnion(['$suppliers', []]) }).end() 输出结果为: suppliersDistinct: ['a', 'b', 'c'] 三、再试试setDifference,函数作用是输出仅存在第一个数组中的值,可以用于先排除一个指定值,假如想排除c,示例如下 await db.collection('orders') .aggregate() .addFields({ suppliersDistinct: $.setDifference(['$suppliers', ['c']]) }).end() 输出结果为: suppliersDistinct: ['a', 'b']
2020-08-03 - 微信小程序如何去掉checkbox前面的方框?
微信小程序如何去掉checkbox前面的方框,我只想要这个复习框的功能,但是我不想要这个复选框前面的方框,应该怎么做呀 checkbox-group bindchange="checkboxChange"> <label class="checkbox" wx:for="{{items}}" wx:key="item"> <checkbox value="{{item.name}}" checked="{{item.checked}}">{{item.value}}</checkbox> </label> </checkbox-group>
2019-09-18 - 微信小程序查看原生组件的代码结构
大家在开发过程中大多数有遇到过想修改微信小程序的原生组件样式的时候吧 比如修改复选框的样式如下: [图片] 这几个 css 类官方明明没有提供,在网上百分之九十九的文章都是直接告诉你这么做,但是没人告诉你为啥这么做 有咩有和我一样第一次看到一脸懵逼的举个手😄 本着不抛弃不放弃的思想,终于在我不断探索下(其实是胡乱一蒙)找到了方法 进入复选框官方文档[图片] 2. 鼠标右键 审查示例代码(这里是个iframe) [图片] 3. 审查示例代码里的的复选框 [图片] 嘻嘻😁就能看到代码结构了 然后通过类名还能找到样式 完成!!!!
2020-03-30 - 云存储图片超过52张连续显示,为什么只显示第一张?
52张云存储图片连续显示,为什么只显示第一张?取不同的图片也有类似的问题,这里做代码片段方便取了相同图片。 [图片] 少于52张云存储图片连续显示,就正常了。或者云函数里通过cloud.getTempFileURL换取临时链接显示,也正常。 [图片]
2020-04-21 - 小程序云开发时效数据刷新和存储 (access_token等)
小程序云开发时效数据刷新和存储 (access_token等) 1.问题描述 小程序中经常有需要进行OCR识别,或者使用外部api例如百度AI识别等接口,请求调用这些接口需要令牌,即一些具有时效性的数据。本文以小程序云开发使用百度API接口为例,介绍access_token定时刷新和请求机制。 下面是百度调用身份证识别的一段需求,需要传的参数需要有access_token。 请求示例 HTTP 方法:[代码]POST[代码] 请求URL: [代码]https://aip.baidubce.com/rest/2.0/ocr/v1/idcard[代码] URL参数: 参数 值 access_token 通过API Key和Secret Key获取的access_token,参考“Access Token获取” access_token 是具有时效性的数据,每次请求一次接口就进行一次请求刷新,显然是对计算机资源的极大浪费,且影响效率。 2.问题解决方案 [图片] 2.1.云数据库配置 新建一个云数据库名为setconfig。作为配置型信息存储数据库,类似access_token的数据都可以向其中存储。 对access_token配置下列字段: _openid (你的openid* 必填) config_name (配置名,填access_token) value (access_token的值,默认为null) id会自动生成,配置完效果如下(这个value是已经更新后的值) [图片] 2.2 定时云函数配置 阅读access_token获取的文档,可知,我们需要请求一个地址来获取access_token的值。 获取Access_Token 请求URL数据格式 向授权服务地址[代码]https://aip.baidubce.com/oauth/2.0/token[代码]发送请求(推荐使用POST),并在URL中带上以下参数: grant_type: 必须参数,固定为[代码]client_credentials[代码]; client_id: 必须参数,应用的[代码]API Key[代码]; client_secret: 必须参数,应用的[代码]Secret Key[代码]; 例如: [代码]https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=Va5yQRHlA4Fq5eR3LT0vuXV4&client_secret=0rDSjzQ20XUj5itV6WRtznPQSzr5pVw2&[代码] 实现 我们需要在云函数中模拟请求,并根据返回结果刷新云数据库中的access_token值。 想要运行通过该程序,需要开发者自己去百度创建账号并创建应用。 云函数index.js [代码]// 云函数入口文件 index.js const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() var request = require('request') // 定时器 exports.main = async(event, context) => { const appkey = '填写你的百度AppKey'; const appsecret = '填写你的百度AppSecret'; var url = 'https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=' + appkey + '&client_secret=' + appsecret; return new Promise((resolve, reject) => { request({ url: url, method: "POST", json: true, headers: { "content-type": "application/json", }, }, function(error, response, body) { if (!error && response.statusCode == 200) { console.log('通行证为' + body.access_token) resolve(body.access_token) //记得修改.doc('xxx') 中的内容 db.collection('setconfig').doc('aaaf5a56-1dd9-4e50-974b-34688fa47b20').update({ data: { value: body.access_token } }).then(res => { console.log('调用完成') console.log(res) }) } }) }) } [代码] docid是setconfig生成的,每个人不同注意修改 还有一种更新写法,不过更推荐使用上面的方法,效率更高,且稳定。 [代码]db.collection('setconfig').where({ config_name:'access_token' }).update({ data: { value: body.access_token } }) [代码] 云函数config.json(定时触发器功能实现) [代码]{ // triggers 字段是触发器数组,目前仅支持一个触发器,即数组只能填写一个,不可添加多个 "triggers": [ { // name: 触发器的名字,规则见下方说明 "name": "myTrigger", // type: 触发器类型,目前仅支持 timer (即 定时触发器) "type": "timer", // config: 触发器配置,在定时触发器下,config 格式为 cron 表达式, //现在为每天凌晨两点触发 "config": "0 0 2 * * * *" } ] } [代码] 云函数整体结构为: [图片] 然后上传并部署(云端安装依赖)。 2.3 小程序端获取Access_token 在小程序进入相应界面的时候,请求云数据库,获取access_token [代码]onLoad: function (options) { //页面初始化 var that = this; db.collection('setconfig').where({ config_name:'access_token' }).get({ success(res){ that.setData({ access_token:res.data[0].value }) //console.log(res.data[0]) }, fail(res){ wx.showToast({ title: '请求失败,无法通过扫描填充数据', }) } }) }, [代码] 3. 参考资料 [1]百度AI鉴权认证机制 [2]微信小程序云开发数据库update函数更新不了数据的问题 [3]小程序云开发定时触发器
2020-03-06 - 小程序云函数调用http或https请求外部数据
我们使用小程序云开发的时候,难免会遇到在云函数里做http获取https请求外部数据,然后再通过云函数返回给我们的小程序。今天就来教大家如何在云函数里做http和https请求。 老规矩,先看效果图 [图片] 通过上图,可以看到我们在云函数里成功的访问到了百度的数据。下面就来讲下实现步骤。 一,定义云函数 关于云函数如何创建,这里我就不多说了。不知道如何创建的同学可以去看下我的云开发基础视频:https://study.163.com/course/courseMain.htm?courseId=1209499804 二,使用npm安装request-promise库 使用npm命令行之前,我们需要先安装node.js,node的安装网上搜一下就行。 下面我就来讲下在小程序里使用npm安装类库的步骤。 1, 右键我们的云函数,然后点击在终端中打开 [图片] 2,在打开的终端中输入 npm install request-promise [图片] 3, request-promise安装成功的标示如下 [图片] 三,编写我们的云函数代码 [图片] 把代码给大家贴出来,代码很简单,里面也有相应的注释,我们这里以请求百度的数据为例。 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') //引入request-promise用于做网络请求 var rp = require('request-promise'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { let url = 'https://www.baidu.com'; return await rp(url) .then(function (res) { return res }) .catch(function (err) { return '失败' }); } [代码] 到这里我就成功的在云函数里实现了http和https请求了,这里使用的是get请求,至于post请求如何使用,自己去百度下“ request-promise post请求”即可。 再来看下我们请求成功的效果图 [图片] 是不是很简单,有任何关于小程序,云开发相关的问题,都可以留言或者私信我,我看到后会及时解答的。
2019-09-23 - popButtons
本月份的组件分享,事件效果由 wxs事件完成,注释在代码片段里有。 按钮可拖动,子菜单上没有点击事件,是根据touchend位置来判断点击的谁。有bug欢迎反馈 代码片段:https://developers.weixin.qq.com/s/9BNYnhmG7tcl 社区这会儿效果图gif上传不了。。不是我的错 图标iconfont下的,没版权问题吧。。
2019-10-21 - [打怪升级]小程序自定义头部导航栏“完美”解决方案
[图片] 为什么要做这个? 主要是在项目中,智酷君发现的一些问题 一些页面是通过扫码和订阅消息访问后,没有直接可以点击去首页的,需要添加一个home链接 需要添加自定义搜索功能 需要自定义一些功能按钮 [图片] 其实,第一个问题,在最近的微信版本更新中已经优化了,通过 小程序模板消息 过来的,系统会自动加上home按钮,但对于其他的访问方式则没有支持~ 一个不大不小的问题:两边ICON不对齐问题 [图片] 智酷君之前尝试了各种解决方法,发现有一个问题,就是现在手机屏幕太多种多样,有 传统头部、宽/窄刘海屏、水滴屏等等,无法八门,很多解决方案都无法解决特殊头部,系统**“胶囊按钮”** 和 自定义按钮在Android屏幕可能有 几像素不对齐 的问题(强迫症的噩梦)。 下面分享下一个相对比较完善的解决方案: [图片] 小程序代码段DEMO Link: https://developers.weixin.qq.com/s/cuUaCimT72cH ID: cuUaCimT72cH 智酷君做了一个demo代码段,方便大家直接用IDE工具查看源码~ [图片] 页面配置 1、页面JSON配置 [代码]{ "usingComponents": { "NavComponent": "/components/nav/common" //以插件的方式引入 }, "navigationStyle": "custom" //自定义头部需要设置 } [代码] 如果需要自定义头部,需要设置navigationStyle为 “custom” 2、页面代码 [代码]<!-- home 类型的菜单 --> <NavComponent v-title="自定义头部" bind:commonNavAttr="commonNavAttr"></NavComponent> <!-- 搜索菜单 --> <NavComponent is-search="true" bind:commonNavAttr="commonNavAttr"></NavComponent> [代码] 可以在自定义导航标签上添加属性配置来设置功能,具体按照实际需要来 3、目录结构 [代码]│ ├─components │ └─nav │ common.js │ common.json │ common.wxml │ common.wxss │ ├─images │ back.png │ home.png │ └─index index.js index.json index.wxml index.wxss search.js search.json search.wxml search.wxss [代码] 仅供参考 插件对应的JS部分 components/nav/common.js部分 [代码]const app = getApp(); Component({ properties: { vTitle: { type: String, value: "" }, isSearch:{ type: Boolean, value: false } }, data: { haveBack: true, // 是否有返回按钮,true 有 false 没有 若从分享页进入则没有返回按钮 statusBarHeight: 0, // 状态栏高度 navbarHeight: 0, // 顶部导航栏高度 navbarBtn: { // 胶囊位置信息 height: 0, width: 0, top: 0, bottom: 0, right: 0 }, cusnavH: 0, //title高度 }, // 微信7.0.0支持wx.getMenuButtonBoundingClientRect()获得胶囊按钮高度 attached: function () { if (!app.globalData.systeminfo) { app.globalData.systeminfo = wx.getSystemInfoSync(); } if (!app.globalData.headerBtnPosi) app.globalData.headerBtnPosi = wx.getMenuButtonBoundingClientRect(); console.log(app.globalData) let statusBarHeight = app.globalData.systeminfo.statusBarHeight // 状态栏高度 let headerPosi = app.globalData.headerBtnPosi // 胶囊位置信息 console.log(statusBarHeight) console.log(headerPosi) let btnPosi = { // 胶囊实际位置,坐标信息不是左上角原点 height: headerPosi.height, width: headerPosi.width, top: headerPosi.top - statusBarHeight, // 胶囊top - 状态栏高度 bottom: headerPosi.bottom - headerPosi.height - statusBarHeight, // 胶囊bottom - 胶囊height - 状态栏height (胶囊实际bottom 为距离导航栏底部的长度) right: app.globalData.systeminfo.windowWidth - headerPosi.right // 这里不能获取 屏幕宽度,PC端打开小程序会有BUG,要获取窗口高度 - 胶囊right } let haveBack; if (getCurrentPages().length != 1) { // 当只有一个页面时,并且是从分享页进入 haveBack = false; } else { haveBack = true; } var cusnavH = btnPosi.height + btnPosi.top + btnPosi.bottom // 导航高度 console.log( app.globalData.systeminfo.windowWidth, headerPosi.width) this.setData({ haveBack: haveBack, // 获取是否是通过分享进入的小程序 statusBarHeight: statusBarHeight, navbarHeight: headerPosi.bottom + btnPosi.bottom, // 胶囊bottom + 胶囊实际bottom navbarBtn: btnPosi, cusnavH: cusnavH }); //将实际nav高度传给父类页面 this.triggerEvent('commonNavAttr',{ height: headerPosi.bottom + btnPosi.bottom }); }, methods: { _goBack: function () { wx.navigateBack({ delta: 1 }); }, bindKeyInput:function(e){ console.log(e.detail.value); } } }) [代码] 解决不同屏幕头部不对齐问题的终极办法是 wx.getMenuButtonBoundingClientRect() 这个方法从微信7.0.0开始支持,通过这个方法我们可以获取到右边系统胶囊的top、height、right等属性,这样无论是水滴屏、刘海屏、异形屏,都能完美对齐右边系统默认的胶囊bar,完美治愈强迫症~ APP.js 部分 [代码]//app.js App({ /** * 加载页面 * @param {*} options */ onShow: function (options) { }, onLaunch: async function () { let self = this; //设置默认分享 this.globalData.shareData = { title: "智酷方程式" } // this.getSysInfo(); }, globalData: { //默认分享文案 shareData: {}, qrCodeScene: false, //二维码扫码进入传参 systeminfo: false, //系统信息 headerBtnPosi: false, //头部菜单高度 } }); [代码] 将获取的参数存储在一个全局变量globalData中,可以减少反复调用的性能消耗。 插件HTML部分 [代码]<view class="custom_nav" style="height:{{navbarHeight}}px;"> <view class="custom_nav_box" style="height:{{navbarHeight}}px;"> <view class="custom_nav_bar" style="top:{{statusBarHeight}}px; height:{{cusnavH}}px;"> <!-- 搜索部分--> <block wx:if="{{isSearch}}"> <input class="navSearch" style="height:{{navbarBtn.height-2}}px;line-height:{{navbarBtn.height-4}}px; top:{{navbarBtn.top+1}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;" maxlength="10" bindinput="bindKeyInput" placeholder="输入文字搜索" /> </block> <!-- HOME 部分--> <block wx:else> <view class="custom_nav_icon {{!haveBack||'borderLine'}}" style="height:{{navbarBtn.height}}px;line-height:{{navbarBtn.height-2}}px; top:{{navbarBtn.top}}px; left:{{navbarBtn.right}}px; border-radius:{{navbarBtn.height/2}}px;"> <view wx:if="{{haveBack}}" class="icon-back" bindtap='_goBack'> <image src='/images/back.png' mode='aspectFill' class='back-pre'></image> </view> <view wx:if="{{haveBack}}" class='navbar-v-line'></view> <view class="icon-home"> <navigator class="home_a" url="/pages/home/index" open-type="switchTab"> <image src='/images/home.png' mode='aspectFill' class='back-home'></image> </navigator> </view> </view> <view class="nav_title" style="height:{{cusnavH}}px; line-height:{{cusnavH}}px;"> {{vTitle}} </view> </block> </view> </view> </view> [代码] 主要是对几种状态的判断和定位的计算。 插件CSS部分 [代码]/* components/nav/test.wxss */ .custom_nav { width: 100%; background: #3a7dd7; position: relative; z-index: 99999; } .custom_nav_box { position: fixed; width: 100%; background: #3a7dd7; z-index: 99999; border-bottom: 1rpx solid rgba(255, 255, 255, 0.3); } .custom_nav_bar { position: relative; z-index: 9; } .custom_nav_box .nav_title { font-size: 28rpx; color: #fff; text-align: center; position: absolute; max-width: 360rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; top: 0; left: 0; right: 0; bottom: 0; margin: auto; z-index: 1; } .custom_nav_box .custom_nav_icon { position:absolute; z-index: 2; display: inline-block; border-radius: 50%; vertical-align: top; font-size:0; box-sizing: border-box; } .custom_nav_box .custom_nav_icon.borderLine { border: 1rpx solid rgba(255, 255, 255, 0.3); background: rgba(0, 0, 0, 0.1); } .navbar-v-line { width: 1px; margin-top: 14rpx; height: 32rpx; background-color: rgba(255, 255, 255, 0.3); display: inline-block; vertical-align: top; } .icon-back { display: inline-block; width: 74rpx; padding-left: 20rpx; vertical-align: top; /* margin-top: 12rpx; vertical-align: top; */ height: 100%; } .icon-home { /* margin-top: 8rpx; vertical-align: top; */ display: inline-block; width: 80rpx; text-align: center; vertical-align: top; height: 100%; } .icon-home .home_a { height: 100%; display: inline-block; vertical-align: top; width: 35rpx; } .custom_nav_box .back-pre, .custom_nav_box .back-home { width: 35rpx; height: 35rpx; vertical-align: middle; } .navSearch { width: 200px; background: #fff; font-size: 14px; position: absolute; padding: 0 20rpx; z-index: 9; } [代码] 总结: 通过微信API: getMenuButtonBoundingClientRect(),结果各类手机屏幕的适配问题 将算好的参数存储在全局变量中,一次计算全局使用,爽YY~ 往期回顾: [填坑手册]小程序PC版来了,如何做PC端的兼容?! [填坑手册]小程序Canvas生成海报(一) [拆弹时刻]小程序Canvas生成海报(二)
2021-09-13 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - 【突然发现】sticky吸顶 IOS安卓都支持了吗?
刚才支付宝纠结 滚动监听动态设置position:fixed 吸顶效果,然后想看看 sticky是否已经支持,测试后发现竟然IOS安卓都支持了。 赶紧跑到微信测试了一发 竟然也都支持了。。 有手机的大佬 可以测试下兼容性如何吗?我这边测试了iPhone系列和几个vivo oppo的机型,都正常 代码片段: https://developers.weixin.qq.com/s/XNa5Pemr7Vew
2020-01-08 - 请问如何实现一个悬浮的圆形按钮,并且可以在屏幕上拖动呢?
请问如何实现一个悬浮的圆形按钮,并且可以在屏幕上拖动呢?
2019-09-19 - 写一个能自定义尺寸、样式的switch
小程序原生的switch不能灵活的修改宽高、样式,很不方便,我这边参考WeUI的开关,写了一个可以自定义尺寸样式的switch组件。 直接上代码:https://gitee.com/piscdong/wechat-switch 效果如下图,可以自定义宽高,可以做成方角的 [图片] 代码分析 这个switch主要的难点就是点击后背景颜色变换的动画,这里用到了css的transition、transform两个属性来实现动画,以及::before和::after两个伪元素。 wxml基本结构为: [代码]<view class="switch"> <view></view> </view> [代码] 父级view是整个switch容器,会用到::before做背景色切换动画,::after做禁用时的灰色遮罩。内部的一个view是来回切换的白点。未选中时默认class是switch,选中时增加一个class:switch_checked。 选中状态到未选中状态背景有一个从中间变大到全部的白色动画,所以需要给父级view设置一个颜色作为背景色。 [代码].switch { ... background: #00c000; position: relative; } [代码] 未选中时::before覆盖整个容器,选中时::before设置[代码]transform: scale(0);[代码],这样选中时白色区域就会缩放到最小,再加上transition实现动画效果。 [代码].switch::before { display: block; content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 9999rpx; background: #fff; transition: all 0.35s cubic-bezier(0.45, 1, 0.4, 1); } .switch_checked::before { transform: scale(0); } [代码] 来回移动的白点,未选中时通过[代码]left: 0;[代码]定位到左侧,选中时将left设置为100%定位到右侧,但是这样白点会完全移出容器范围,所以还需要加上[代码]transform: translateX(-100%);[代码]将白点向左再一定自身宽度的100%,同样加上transition实现动画效果。 [代码].switch view { position: absolute; top: 0; left: 0; width: 60rpx; height: 60rpx; border-radius: 50%; background: #fff; box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.4); transition: all 0.35s cubic-bezier(0.45, 1, 0.4, 1); } .switch_checked view { left: 100%; transform: translateX(-100%); } [代码] 关于“::” 最后搭车说一下“:”和“::”,“:”是伪类,“::”是伪元素。按照我的理解:伪类不会在dom中增加节点,只不过是css选择器的一种特殊效果;伪元素会增加节点,flex布局中会影响到其他元素。 为了保证兼容性,css3是允许伪元素使用单个冒号。
2019-12-24 - 小米手机使用webgl无法获取透明背景,会显示黑色,怎样解决?
组件:webgl 微信版本号:7.0.10 基础版本号:2.9.4 手机型号:红米 k30 需求:在手机上实现3d模型的显示,因为设置了背景图片,所以想让画布显示为透明。 问题描述: 部分机型(如iPhone5)可以实现透明效果,但自己测试的这些机型无法显示透明效果,像Redmi k30、Redmi 4 复现问题的代码片段: https://developers.weixin.qq.com/s/8DO7jamr7Weg 补充: 用的是https://github.com/yannliao/threejs.miniprogram的demo 只修改了model.js的两个地方 修改的地方如图: [图片] 图片分别是:iPhone5s显示效果.jpg 红米k30显示效果.jpg 开发工具.jpg 手机基本信息.jpg [图片] [图片] [图片] [图片]
2020-01-15 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 小程序简单两栏瀑布流效果
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次放入到高度最低的那一栏。 先上代码:https://developers.weixin.qq.com/s/Fgm5s1mz7Wdm 所谓简单,是指只考虑图片,图片之外的其他元素高度固定,不在考虑范围内。 说一下基本的实现思路: 1、加载列表数据 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 我也考虑过在第二步bindload获取到宽高后就直接插入到栏位中,但是会出现小的图片先加载完先出现到页面中,虽然瀑布流不是普通的列表那样的排序,但是也不能小的图片在上面这样太乱顺序,所以就改成了获取宽高先存储,等所有图片加载完成后再往页面上渲染。 来看看实际的代码 不需要渲染到wxml中的数据,我放到了jsData中,主要是两栏的高度和是否在加载数据的标记。 tempPics是第一次加载的数据,临时存放,用于加载图片宽高 columns是两个栏位的实际展示数据 [代码]jsData: { columnsHeight: [0, 0], isLoading: false }, data: { columns: [ [], [] ], tempPics: [] } [代码] 1、加载列表数据 这一步没什么好说的,主要是触发方式,我的代码里是放在页面加载以及拉到页面底部时触发 [代码]onLoad: function() { this.loadData() }, onReachBottom: function() { this.loadData() } [代码] 加载后将列表数据存到tempPics中,用于页面加载获取宽高 2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储 [代码]<view class="hide"> <image wx:for="{{tempPics}}" src="{{item.pic}}" bindload="loadPic" binderror="loadPicError" data-index="{{index}}" /> </view> [代码] 主要是image组件的bindload来获取实际宽高,这里还增加了binderror,防止出现图片加载出错的时候卡死 [代码]loadPic: function(e) { var that = this, data = that.data, tempPics = data.tempPics, index = e.currentTarget.dataset.index if (tempPics[index]) { //以750为宽度算出相对应的高度 tempPics[index].height = e.detail.height * 750 / e.detail.width tempPics[index].isLoad = true } that.setData({ tempPics: tempPics }, function() { that.finLoadPic() }) } [代码] 获取到宽高后,以750为宽度计算出相对应的高度并存储,然后增加一个加载完成的标记。加载出错后就强制高度为750,这样展示的时候就是一个正方形。 单个图片加载完成并存储后调用finLoadPic方法来判断所有图片是否都加载完成。 遍历列表,只要有一个图片没有加载完成的标记,就判断为没有加载完成。 加载完成后进入下一步。 [代码]finLoadPic: function() { var that = this, data = that.data, tempPics = data.tempPics, length = tempPics.length, fin = true for (var i = 0; i < length; i++) { if (!tempPics[i].isLoad) { fin = false break } } if (fin) { wx.hideLoading() if (that.jsData.isLoading) { that.jsData.isLoading = false that.renderPage() } } } [代码] 3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度 这里需要再便利一遍列表,根据当前栏位的高度情况,将图片插入到高度底的那一栏,同时把这一栏高度加上当前图片的高度(不是实际高度,是上一步以750为宽度算出来的高度) [代码]renderPage: function() { var that = this, data = that.data, columns = data.columns, tempPics = data.tempPics, length = tempPics.length, columnsHeight = that.jsData.columnsHeight, index = 0 for (var i = 0; i < length; i++) { index = columnsHeight[1] < columnsHeight[0] ? 1 : 0 columns[index].push(tempPics[i]) columnsHeight[index] += tempPics[i].height } that.setData({ columns: columns, tempPics: [] }) that.jsData.columnsHeight = columnsHeight } [代码] 在wxml中展示的时候image组件的mode要使用widthFix,同时wxss中图片的高度和宽度一样,这样加载出错的图片可以正方形展示 11月21日增加: 根据@杨泉的建议,也尝试了使用wx.getImageInfo来获取图片的宽高(具体代码可以参考评论区),代码也精简了很多。但是实际比较下来速度要比用image组件慢,初步推测原因是[代码]wx.getImageInfo[代码]会返回本地路径,多了写本地临时文件的时间 ps:用到瀑布流的地方,最好能后端直接返回图片的宽高,省去小程序端获取宽高的麻烦 再ps:我个人并不建议小程序端使用瀑布流
2020-01-14 - 图片能显示却报Failed to load local image resourc错误?
图片正常显示[图片] wxml:[图片] 控制台报错: [图片] [图片] 云存储路径: [图片]
2019-11-09 - 小程序顶部导航栏,可滑动,可动态选中放大
最近在研究小程序顶部导航栏时,学到了一个不错的导航栏,今天就来分享给大家。 老规矩,先看效果图 [图片] 可以看到我们实现了如下功能 1,顶部导航栏 2,可以左右滑动的导航栏 3,选中条目放大 原理其实很简单,我这里把我研究后的源码发给大家吧。 wxml文件如下 [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item.name}}</view> </view> </scroll-view> [代码] wxss文件如下 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #fbbd08; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #fbbd08 !important; } [代码] js文件如下 [代码]// pages/test2/test2.js Page({ data: { tabCur: 0, //默认选中 tabs: [{ name: '等待支付', id: 0 }, { name: '待发货', id: 1 }, { name: '待收货', id: 2 }, { name: '待签字', id: 3 }, { name: '待评价', id: 4 }, { name: '五星好评', id: 5 }, { name: '差评订单', id: 6 }, { name: '编程小石头', id: 8 }, { name: '小石头', id: 9 } ] }, //选择条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }) } }) [代码] 代码里注释很明白了,大家自己跟着多敲几遍就可以了。后面会更新更多小程序相关的知识,请持续关注。
2019-11-22 - 如何使用数据库聚合match过滤$.and
我需要在云函数中查询数据库,聚合计算一个人的某月1-31号的消费金额: [代码]let queryFeeSum = await db[代码][代码] [代码][代码].collection([代码][代码]'lunch'[代码][代码])[代码][代码] [代码][代码].aggregate()[代码][代码] [代码][代码].match({[代码][代码] [代码][代码]eno: eno,[代码][代码] [代码][代码]date: $.and([$.gte([[代码][代码]'$date'[代码][代码], dateNumRange.begin]), $.lte([[代码][代码]'$date'[代码][代码], dateNumRange.end])])[代码][代码] [代码][代码]})[代码][代码] [代码][代码].group({[代码][代码] [代码][代码]_id: [代码][代码]null[代码][代码],[代码][代码] [代码][代码]fee: $.sum([代码][代码]'$goodsPrice'[代码][代码])[代码][代码] [代码][代码]})[代码][代码] [代码][代码].end()[代码]date是数值型:它的值是8位整数,如20190701、20190728 上面的 $.and 语句按文档中的写法,聚合这个功能也是官方开发出来不久,找不到相关帮助文档。 运行报错,查看日志提示: {"errorCode":1,"errorMessage":"user code exception caught","stackTrace":"errCode: -502001 database request fail | errMsg: [FailedOperation] (BadValue) unknown operator: $and; "} 请问一下,为什么提示操作失败,无效的值、未知的操作器$and。
2019-07-28 - recycle-view 商品长列表应用
recycle-view 在商品长列表中的应用 参考官方文档 WXML: [代码]<recycle-view class="list" batch="{{batchSetRecycleData}}" id="recycleId" bindscrolltolower="bindscrolltolower" scroll-y="true"> <recycle-item class="record" wx:for="{{recycleList}}" wx:key="id"> <view class="record_image" style="background:url( {{item.images[0] }}&x-oss-process=image/resize,s_320 )"></view> <view class="record_view">{{index}} . {{item.title}}</view> </recycle-item> <view slot="after">加载中...</view> </recycle-view> [代码] JS: [代码]const app = getApp() const createRecycleContext = require('miniprogram-recycle-view'); Page({ pageNum:1,//页码 listobj: Object,//RecycleContext对象 postflg:true,//是否可以加载列表,用户误触控制 windowWidth:0,//系统页面可视宽度 data: {}, onReady: function () { var than = this; //获取系统参数 wx.getSystemInfo({ success: function(res) { than.windowWidth = res.windowWidth; //创建RecycleContext对象来管理 recycle-view 定义的的数据 than.listobj = createRecycleContext({ id: 'recycleId', dataKey: 'recycleList', page: than, itemSize: than.itemSizeFunc, }) than.getlist();//请求接口 }, }) }, //设置item宽高信息,样式所设必须与之相同 itemSizeFunc: function (item, idx) { var than = this; return { width: than.windowWidth * 0.47, height: than.windowWidth * 0.61 } }, //滚动到底部监听,分页加载 bindscrolltolower(e) { console.log('滚动到底部----'); if(this.postflg){ this.postflg = false;//请求完成前不再更改页码请求接口 this.pageNum++; this.getlist(); } }, //数据请求 getlist(){ var than = this; wx.request({ url: 'https://w.taopaitang.com/api/discover?page=1&pagenum=10', data:{ page: than.pageNum, pagenum:10, }, method: 'get', success(res){ console.log('数据请求成功----' + than.pageNum +'---',res); if(res.data.message){ //append RecycleContext 对象提供的方法:在当前的长列表数据上追加list数据 than.listobj.append(res.data.data.items); than.postflg = true; } } }) } }) [代码] 全部代码 -------> 代码片段 <--------- 我想把此列表改成瀑布流展示,求路过的大神指点下!!!!!!
2019-10-07 - 关于swiper组件的current问题导致图片不显示
在使用swiper组件的时候,如果切换循环swiper-item的图片数组的话,会引起数组长度变化。当切换后的数组长度不及切换前的数组长度时,而恰好swiper此时的current记录的位置已经超出了后面的数组的长度,就会报警告。 [图片] 并且图片显示不出来,情况如下: [图片] 修改前代码: wxml: [代码]<swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}"> <block wx:for="{{imgUrls}}"> <swiper-item> <image src="{{item}}" class="slide-image" width="355" height="150"/> </swiper-item> </block> </swiper> <button bindtap="changeArr"></button> [代码] [代码]js: data: { imgUrls: ['img1', 'img2', 'img3'] }, changeArr(){ this.setData({ imgUrls: ['img1', 'img2''] }) } [代码] 解决办法非常简单,只需在swiper上面绑定current属性,然后在改变imgUrls时将current的值变成 0 即可 修改后代码: wxml: [代码]<swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}" current=“{{current}}”> <block wx:for="{{imgUrls}}"> <swiper-item> <image src="{{item}}" class="slide-image" width="355" height="150"/> </swiper-item> </block> </swiper> <button bindtap="changeArr"></button> [代码] js: [代码]data: { imgUrls: ['img1', 'img2', 'img3'], current:0 }, changeArr(){ this.setData({ imgUrls: ['img1', 'img2''], current:0 }) } [代码] 需要注意的是如果改变imgUrls时是需要从后台请求的话,要等到请求完成后再将current的值改成0
2021-08-31 - 如何用小程序实现类原生APP下一条无限刷体验
1.背景 如今信息流业务是各大互联网公司争先抢占的一个大面包,为了提高用户的后续消费,产品想出了各种各样的方法,例如在微视中,用户可以无限上拉出下一条视频;在知乎中,也可以无限上拉出下一条回答。这样的操作方式用户体验更好,后续消费也更多。最近几年的时间,微信小程序已经从一颗小小的萌芽成长为参天大树,形成了较大规模的生态,小程序也拥有了一个很大的流量入口。 2.demo体验 那如何才能在小程序中实现类原生APP效果的下一条无限刷体验? 这篇文章详细记录了下一条无限刷效果的实现原理,以及细节和体验优化,并将相关代码抽象成一个微信小程序代码片段,有需要的同学可查看demo源码。 线上效果请用微信扫码体验: [图片] 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a 3.实现原理 出于性能和兼容性考虑,我们尽量采用小程序官方提供的原生组件来实现下一条无限刷效果。我们发现,可以将无限上拉下一篇的文章看作一个竖向滚动的轮播图,又由于每一篇文章的内容长度高于一屏幕高度,所以需要实现文章内部可滚动,以及文章之间可以上拉和下拉切换的功能。 在多次尝试后,我们最终采用了在[代码]<swiper>[代码]组件内部嵌套一个[代码]<scroll-view>[代码]组件的方式实现,利用[代码]<swiper>[代码]组件来实现文章之间上拉和下拉切换的功能,利用[代码]<scroll-view>[代码]来实现一篇文章内部可上下滚动的功能。 所以页面的dom结构如下所示: [代码]<swiper class='scroll-swiper' circular="{{false}}" vertical="{{true}}" bindchange="bindChange" skip-hidden-item-layout="{{true}}" duration="{{500}}" easing-function="easeInCubic" > <block wx:for="{{articleData}}"> <swiper-item> <scroll-view scroll-top="0" scroll-with-animation="{{false}}" scroll-y > content </scroll-view> </swiper-item> </block> </swiper> [代码] 4.性能优化 我们知道view部分是运行在webview上的,所以前端领域的大多数优化方式都有用。例如减少代码包体积,使用分包,渲染性能优化等。下面主要讲一下渲染性能优化。 4.1 dom优化 由于页面需要无限上拉刷新,所以要在[代码]<swiper>[代码]组件中不断的增加[代码]<swiper-item>[代码],这样必然会导致页面的dom节点成倍数的增加,最后非常卡顿。 为了优化页面的dom节点,我们利用[代码]<swiper>[代码]的[代码]current[代码]和[代码]<swiper-item>[代码]的[代码]index[代码]来做优化,控制是否渲染dom节点。首先,仅当[代码]index <= current + 1[代码]时渲染[代码]<swiper-item>[代码],也就是页面中最多预先加载出下一条,而不是将接口返回的所有后续数据都渲染出来;其次,对于用户已经消费过的之前的[代码]<swiper-item>[代码],不能直接销毁dom节点,否则会导致[代码]<swiper>[代码]的[代码]current[代码]值出现错乱,但是我们可以控制是否渲染[代码]<swiper-item>[代码]内部的子节点,我们设置了仅当[代码]current <= index + 1 && index -1 <= current[代码]时才会渲染[代码]<swiper-item>[代码]中的内容,也就是仅渲染当先文章,及上一篇和下一篇的文章内容,其他文章的dom节点都被销毁了。 这样,无论用户上拉刷新了多少次,页面中最多只会渲染3篇文章的内容,避免了因为上拉次数太多导致的页面卡顿。 4.2 分页时setData的优化 setData工作原理 [图片] 小程序的视图层目前使用[代码]WebView[代码]作为渲染载体,而逻辑层是由独立的 [代码]JavascriptCore[代码] 作为运行环境。在架构上,[代码]WebView[代码] 和 [代码]JavascriptCore[代码] 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 [代码]evaluateJavascript[代码] 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 [代码]JS[代码] 脚本,再通过执行 [代码]JS[代码] 脚本的形式传递到两边独立环境。 而 [代码]evaluateJavascript[代码] 的执行会受很多方面的影响,数据到达视图层并不是实时的。 每次 [代码]setData[代码] 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关。 [代码]setData[代码] 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互。 [代码]setData[代码] 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。 避免不当使用setData [代码]data[代码] 应仅包括与页面渲染相关的数据,其他数据可绑定在this上。使用 [代码]data[代码] 在方法间共享数据,会增加 setData 传输的数据量,。 使用 [代码]setData[代码] 传输大量数据,通讯耗时与数据正相关,页面更新延迟可能造成页面更新开销增加。仅传输页面中发生变化的数据,使用 [代码]setData[代码] 的特殊 [代码]key[代码] 实现局部更新。 避免不必要的 [代码]setData[代码],避免短时间内频繁调用 [代码]setData[代码],对连续的setData调用进行合并。不然会导致操作卡顿,交互延迟,阻塞通信,页面渲染延迟。 避免在后台页面进行 [代码]setData[代码],这样会抢占前台页面的渲染资源。可将页面切入后台后的[代码]setData[代码]调用延迟到页面重新展示时执行。 优化示例 无限上拉刷新的数据会采用分页接口的形式,分多次请求回来。在使用分页接口拉取到下一刷的数据后,我们需要调用[代码]setData[代码]将数据写进[代码]data[代码]的[代码]articleData[代码]中,这个[代码]articleData[代码]是一个数组,里面存放着所有的文章数据,数据量十分庞大,如果直接[代码]setData[代码]会增加通讯耗时和页面更新开销,导致操作卡顿,交互延迟。 为了避免这个问题,我们将[代码]articleData[代码]改进为一个二维数组,每一次[代码]setData[代码]通过分页的 [代码]cachedCount[代码]标识来实现局部更新,具体代码如下: [代码]this.setData({ [`articleData[${cachedCount}]`]: [...data], cachedCount: cachedCount + 1, }) [代码] [代码]articleData[代码]的结构如下: [图片] 4.3 体验优化 解决了操作卡顿,交互延迟等问题,我们还需要对动画和交互的体验进行优化,以达到类原生APP效果的体验。 在文章间上拉切换时,我们使用了[代码]<swiper>[代码]组件自带的动画效果,并通过设置[代码]duration[代码]和[代码]easing-function[代码]来优化滚动细节和动画。 当用户阅读文章到底部时,会提示下一篇文章的标题等信息,而在页面上拉时,由于下一篇文章的内容已经加载出来了,这样在滑动过程中会出现两个重复的标题。为了避免这种情况出现,我们通过一个占满屏幕宽高的空白[代码]<view>[代码]来将下一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]hidden="{{index !== current && index !== current + 1}}"[代码]来隐藏这个空白[代码]<view>[代码],并对这个空白[代码]<view>[代码]的高度变化增加动画,来实现下一篇文章从屏幕底部滚动到屏幕顶部的效果: [代码].fake-scroll { height: 100%; width: 100%; transition: height 0.3s cubic-bezier(0.167,0.167,0.4,1); } [代码] [图片] 而当用户想要上拉查看之前阅读过的文章时,我们需要给用户一个“下滑查看上一条”提示,所以也可以采用同上的方式,通过一个占满屏幕宽高的提示语[代码]<view>[代码]来将上一篇文章的内容撑出屏幕,并在滚动结束时,通过[代码]wx:if="{{index + 1 === current}}"[代码]来隐藏这个提示语[代码]<view>[代码],并对这个提示语[代码]<view>[代码]的透明度变化增加动画,来实现下拉时提示“下滑查看上一条”的效果: [代码].fake-previous { height: 100%; width: 100%; opacity: 0; transition: opacity 1s ease-in; } .fake-previous.show-fake-previous { opacity: 1; } [代码] 至此,这个类原生APP效果的下一条无限刷体验的需求的所有要点和细节都已实现。 记录在此,欢迎交流和讨论。 小程序demo体验请点击:https://developers.weixin.qq.com/s/vIfPUomP7f9a
2019-06-25