- 【问题排查】小程序闪退
在使用小程序的时候,偶然会发生闪退。这里来讲一下闪退的问题该如何排查。 版本排查 发生闪退的时候,首先,要确认下 版本 是不是最新的。如果不是,建议更新版本再重试。旧版本的问题会在新版本进行修复哦。 微信版本: 微信官网 基础库版本:基础库更新日志小程序自查 确认版本都是最新情况下,还是有闪退的问题的话,建议先进行小程序自查~ 一般情况下,闪退是因为内存使用过多导致的,小程序侧可以通过基础库提供 wx.onMemoryWarning 接口来监听内存不足的告警,当收到告警时,通过回收一些不必要资源避免进一步加剧内存紧张。 反馈官方 如果问题还是会出现的话建议反馈给官方处理,需要附带上以下信息点协助排查(划重点:完整的提供信息才可以加速问题处理进度哦!!!) 示例: 系统及微信版本号:安卓7.0.17、IOS 7.0.17(出现问题的时候,建议两端都测试,给出有问题的case)必现 or 偶现:必现可复现场景:代码片段 或者 线上小程序复现步骤:进入首页,点击添加按钮等等,推荐录制复现的 视频(重点)进行上传。上传日志:提供微信号,复现时间点(操作步骤:手机微信那里上传下日志: 我 -> 设置 -> 帮助与反馈:右上角扳手 -> 上报日志,选择出现问题的日期,上传日志)
2020-11-03 - scroll-view的bindscroll获取到的scrollTop不为0
scroll-view的bindscroll监听滑动事件,经常会有这样的问题:scrollview惯性滑动到顶部后,最后的事件中的scrollTop不为0!!!!toupper事件也没有收到!!!
2018-10-08 - 网络调优新方法,请求速度一级fast
原文来自「微信开发者」公众号,小程序技术研发工程师 binnie 原创。 本文主要介绍了小程序网络调优方法的网络分析、能力支持、网络状态提醒。 [图片] 伴随着线上化趋势,小程序逐渐成为日常生活的数字连接器,为大家的数字化生活提供便利。在小程序的使用过程中,网络环境对于用户体验有着非常重要的影响。好的网络环境给予流畅的用户体验,赋能小程序业务。 针对网络环境的应用开发,微信团队提出 网络调优方法,提升网络请求速度,优化用户体验。 [视频] 查一查:网络分析 想要解决网络问题?第一步离不开查明网络问题原因。微信团队提供 Profile 信息、Errno 错误码 2 种网络分析方法,助力开发者快速定位网络问题,解决网络请求难题。 Profile 信息了解耗时信息 基于网络性能分析的需求,微信团队提供 Profile 信息,便于开发者了解网络连接关键时间点的耗时信息。通过以下 2 种方式即可获取 Profile 信息: [图片] Errno 错误码快速定位网络问题 自 iOS 8.0.20 (基础库 2.24.0)、安卓 8.0.20 (基础库 2.23.0) 开始,request 接口支持 Errno 错误码,帮助开发者快速定位网络问题,更好地进行调试。 对比其他错误分析方法,Errno 错误码具有以下优势: 更统一:拥有统一的设计规范,同时不同 API 错误码颗粒度统一,帮助开发者快速辨别错误类型更便捷:不同 API 出现相同错误,对应错误码一致,便于开发者便捷整理错误类型更全面:包含 API 类别信息,帮助开发者快速定位问题Errno 错误码统一简洁的格式助力开发者快速定位网络问题,如何识别错误码?以 (0)600002 错误码为例: (0)6 表示 API 接口的一级类目为【网络】00 表示 API 接口的二级类目为【通用网络错误】002 表示具体错误类型为【URL域名不在安全域名列表中】 用一用:能力支持 了解网络错误原因,开发者着手解决网络问题。微信团队提供 request 新参数、perMessageDeflate 压缩扩展 2 种方法,助力网络请求速度提升。 应用新参数,场景化加速网络水平 针对网络请求相关的新协议,微信团队现已提供 5 类新参数,加速网络水平: enableHttp2:如果后台支持,尝试使用 Http2 协议,适用于高并发、高带宽场景enableQuic:如果后台支持,尝试使用 Quic 协议,适用于低延迟、高吞吐场景enableCache:缓存内容,相同请求优先读取本地内容enableHttpDNS:开启 HttpDNS 服务,降低用户连接失败率enableChunked:开启 transfer-encoding chunked 服务,支持分块传输 启动压缩扩展,提升信息收发速度 当 WebSocket 启动 perMessageDeflate 压缩扩展后,收发信息过程自动压缩数据包,提升消息收发速度。 wx.connectSocket({ url: 'wss://example.qq.com',// 开发者服务器 wss 接口地址 header:{ // HTTP Header,Header 中不能设置 Referer 'content-type': 'application/json' }, protocols: ['protocol1'], // 子协议数组 perMessageDeflate:"true" // 是否开启压缩扩展,默认是 false }) 拍一拍:网络状态提醒 除了通过分析和解决网络难题,小程序开发者可以通过提醒的方式告知用户当前的网络状态,及时切换网络,提升小程序的使用体验。 前后台切换 在小程序切换后台 5 秒后,自动中断网络请求并且收到 interrupted 回调。建议开发者提醒用户重试,避免造成不良体验。 网络状态变化 小程序网络问题常常出现在断网情况,因此开发者可以通过事件 wx.onNetworkStatusChange 通知网络状态变化,引导用户进行网络切换。 wx.onNetworkStatusChange(function (res) { // 当前是否有网络链接,返回的是布尔值 console.log(res.isConnected) // 返回的是网络类型 // 包括 wifi、2g、3g、4g、5g、unknown(Android 下不常见网络类型)、none(无网络) console.log(res.networkType) }) 弱网状态变化 大部分的网络超时问题主要由弱网引起。弱网主要是基于以下现象判定: 8 次网络请求中,出现 3 次以上连接超时8 次网络请求中,出现 3 次 rtt 超过 4008 次网络请求中,出现 3 次以上丢包一方面开发者可以通过 缓存管理器 优化弱网时小程序的使用体验,另一方面开发者可以通过事件 wx.onNetworkWeakChange 监听弱网变化,给予用户更好的提示。 wx.onNetworkWeakChange(function (res) { console.log(res.weakNet) // 当前是否处于弱网状态 console.log(res.networkType) // 当前网络类型 }) // 取消监听 wx.offNetworkWeakChange() ------------------------------------- 网络环境对于小程序的体验有着非常重要的影响。通过原因分析了解网络问题的根因,通过使用协议扩展能力提高网络水平,通过网络状态提醒提升用户体验,最终提升请求速度,满足用户需求。关于网络调优的更多内容,请点击 官方文档 了解更多。 如有其他小程序应用相关的问题,可在 微信开放社区小程序交流专区 发帖互动,技术专员将为大家解答及进行深度交流。
2022-05-31 - 同一用户获取到的unionId不相同
这个问题可以按以下方式自查,举个例子: 1.小程序APPID:wx**********6c86a7 ——账号A 公众号APPID:wx**********8d6f47 ——账号B; 2.需要拿到相同的unionID,核实账号A和账号B 绑定同一个微信开放平台账号是:dl******fpt@sina.com ,所以同一个用户的unionid相同; 3.一般出现unionid不同的原因是:该openid不属于账号A,也不属于账号B,而是属于账号C或账号D,而账号C或账号D并没有绑定在同一个微信开放平台账号下,所以unionid不同。
2019-08-28 - 网页端管理系统在小程序上的实现
接到一个需求,要把一套现有的网页端管理系统完整的复制到小程序上。开始我是拒绝的,想想网页端那些表格要在手机、特别是小程序上复现就头疼。最后甲方给了我一个可以接受的理由:有了小程序就不用做app了啊。 虽然有难度,但是工作还是要做的,有问题就一点一点来解决。先说一个优势,这个网页端是我最近刚重构过的,改成了前后端分离,接口上用了jwt做登录校验(关于jwt的介绍可以移步:【接口相关】聊一聊数据接口的登录态校验以及JWT),可以直接拿到小程序里来用。接下来说一下实际遇到的问题和我的解决方案。 菜单 这个简单,把网页端左侧菜单栏里筛选出常用的放到小程序的tabBar里就可以了,直接用原生的tabBar,没什么花头。 测试的时候发现了一个bug,已经提交给了官方:XS Max真机调试、预览,原生tabBar上的线不显示? 数据表格 网页端数据列表基本都是使用表格来展现,到了小程序端就不适合再用表格了。一方面是小程序没有原生的表格组件,另一方面是手机屏幕不适合展示很宽的表格(横屏什么的从来不在我的考虑范围内)。 我最终采用的解决方案是用卡片列表的形式来展现数据列表。下图是网页端表格和小程序里对应的卡片列表。 [图片] [图片] 模态框+表单 网页端列表中用到的各种表单基本是在弹出模态框里使用表单,这个在小程序上我改成了放到从底部弹出的半屏弹窗。具体效果对比直接看图,小程序里为了防止弹窗内容太多超出屏幕限制,弹窗加了最大高度限制。 [图片] [图片] 日历 由于需求需要,这个项目有一个通过日历展示一个月的日程安排,先看网页端的效果,这里用的是antd的Calendar日历组件。 [图片] 由于手机屏幕的限制,就算能在小程序页面上展现日历,也没办法合适的展现需要的内容。最终决定小程序端只展示某一日的内容,通过从底部弹出的半屏弹窗里显示的日历来切换日期。 [图片] 还是要有取舍 虽然经过各种修改后,绝大多数功能都改成了适合小程序端展现的方式,但是还是有一些功能实在是不适合放到小程序端,或者从功能上来说没必要放到小程序端,这些就只能放弃了。 [图片]
2020-04-10 - 【好文】小程序动态换肤解决方案 - 本地篇
小程序动态换肤方案 – 本地篇 需求说明 在开发小程序的时候,尤其是开发第三方小程序,我们作为开发者,只需要开发一套模板,客户的小程序对我们进行授权管理,我们需要将这套模板应用到对方的小程序上,然后进行发版审核即可; 但是个别客户的小程序需要做 [代码]定制化配色方案[代码],也就是说,不同的小程序个体需要对页面的元素(比如:按钮,字体等)进行不同的配色设置,接下来我们来讨论一下怎么实现它。 方案和问题 一般来说,有两种解决方案可以解决小程序动态换肤的需求: 小程序内置几种主题样式,通过更换类名来实现动态改变小程序页面的元素色值; 后端接口返回色值字段,前端通过 [代码]内联[代码] 方式对页面元素进行色值设置。 当然了,每种方案都有一些问题,问题如下: 方案1较为死板,每次更改主题样式都需要发版小程序,如果主题样式变动不大,可以考虑这种; 方案2对于前端的改动很大,[代码]内联[代码] 也就是通过 [代码]style[代码] 的方式内嵌到[代码]wxml[代码] 代码中,代码的阅读性会变差,但是可以解决主题样式变动不用发版小程序的问题。 ps:我一直在尝试如何在小程序里面,通过js动态修改stylus的变量问题,这样就可以解决上面说的问题了,后期如果实现了,一定周知各位 本文先重点描述第一种方案的实现,文章末尾会贴上我的 [代码]github项目[代码] 地址,方便大家尝试。 前期准备 本文采用的是 [代码]gulp[代码] + [代码]stylus[代码] 引入预编译语言来处理样式文件,大家需要全局安装一下 [代码]gulp[代码],然后安装两个 [代码]gulp[代码] 的插件 [代码]gulp-stylus[代码](stylus文件转化为css文件) [代码]gulp-rename[代码](css文件重命名为wxss文件)。 gulp 这里简单贴一下gulpfile文件的配置,比较简单,其实就是借助 [代码]gulp-stylus[代码] 插件将 [代码].styl[代码] 结尾的文件转化为 [代码].css[代码] 文件,然后引入 [代码]gulp-rename[代码] 插件对文件重命名为 [代码].wxss[代码] 文件; 再创建一个任务对 [代码].styl[代码] 监听修改,配置文件如下所示: [代码]var gulp = require('gulp'); var stylus = require('gulp-stylus'); var rename = require('gulp-rename'); function stylusTask() { return gulp.src('./styl/*.styl') .pipe(stylus()) .pipe(rename(function(path) { path.extname = '.wxss' })) .pipe(gulp.dest('./wxss')) } function autosTask() { gulp.watch('./styl/*.styl', stylusTask) } exports.default = gulp.series(gulp.parallel(stylusTask, autosTask)) [代码] stylus 这里会分为两个文件,一个是主题样式变量定义文件,一个是页面皮肤样式文件,依次如下所示: 主题样式变量设置 [代码]// theme1 theme1-main = rgb(254, 71, 60) theme1-sub = rgb(255, 184, 0) // theme2 theme2-main = rgb(255, 158, 0) theme2-sub = rgb(69, 69, 69) // theme3 theme3-main = rgb(215, 183, 130) theme3-sub = rgb(207, 197, 174) [代码] 页面皮肤样式 [代码]@import './define.styl' // 拼接主色值 joinMainName(num) theme + num + -main // 拼接辅色值 joinSubName(num) theme + num + -sub // 遍历输出改变色值的元素类名 for num in (1..3) .theme{num} .font-vi color joinMainName(num) .main-btn background joinMainName(num) .sub-btn background joinSubName(num) [代码] 输出: [代码].theme1 .font-vi { color: #fe473c; } .theme1 .main-btn { background: #fe473c; } .theme1 .sub-btn { background: #ffb800; } .theme2 .font-vi { color: #ff9e00; } .theme2 .main-btn { background: #ff9e00; } .theme2 .sub-btn { background: #454545; } .theme3 .font-vi { color: #d7b782; } .theme3 .main-btn { background: #d7b782; } .theme3 .sub-btn { background: #cfc5ae; } [代码] 代码我写上了注释,我还是简单说明一下上面的代码:我首先定义一个主题文件 [代码]define.styl[代码] 用来存储色值变量,然后会再定义一个皮肤文件 [代码]vi.styl[代码] ,这里其实就是不同 [代码]主题类名[代码] 下需要改变色值的元素的属性定义,元素的色值需要用到 [代码]define.styl[代码] 预先定义好的变量,是不是很简单,哈哈哈。 具体使用 但是在具体页面中需要怎么使用呢,接下来我们来讲解一下 页面的 [代码]wxss[代码] 文件导入编译后的 [代码]vi.wxss[代码]文件 [代码]@import '/wxss/vi.wxss'; [代码] 页面的 [代码]wxml[代码] 文件需要编写需要改变色值的元素,并且引入变量 [代码]theme[代码] [代码]<view class="intro {{ theme }}"> <view class="font mb10">正常字体</view> <view class="font font-vi mb10">vi色字体</view> <view class="btn main-btn mb10">主色按钮</view> <view class="btn sub-btn">辅色按钮</view> </view> [代码] 页面 [代码]js[代码] 文件动态改变 [代码]theme[代码]变量值 [代码] data: { theme: '' }, handleChange(e) { const { theme } = e.target.dataset this.setData({ theme }) } [代码] 效果预览 [图片] 项目地址 项目地址:https://github.com/csonchen/wxSkin 这是本文案例的项目地址,为了方便大家浏览项目,我把编译后的wxss文件也一并上传了,大家打开就能预览,如果觉得好,希望大家都去点下star哈,谢谢大家。。。
2020-04-23 - 小程序直播组件更新至 1.1.8 版本
小程序直播组件已更新至 1.1.8 版本,请及时更新组件版本,确保能使用新功能。 1,新增商品讲解功能。【功能详细介绍,第 28 页】 直播中,主播能录制商品讲解视频。 观众可在直播中、结束页和回放中观看讲解视频,不再错过任何精彩的时刻! 2,新增商品点赞功能 货架上,观众能给喜欢的商品点赞。 商家可以通过商品点赞量,来判断商品受欢迎程度,为后续打造爆品、预测销量等提供依据。
2020-08-25 - 微信小程序 unionid 登录解决方案
第三方登录模块使开发者能快捷灵活的拥有自己的用户系统,是 LeanCloud 最受欢迎的功能之一。随着第三方平台的演化,特别是微信小程序的流行,LeanCloud 第三方登录模块也一直在改进: v2.0*:增加微信小程序一键登录功能。支持开发者不写任何后端代码实现微信小程序用户系统与 LeanCloud 用户系统的关联。 v3.6:增加 unionid 登录接口。支持开发者使用 unionid 关联一个微信开发者帐号下的多个应用从而共享一套 LeanCloud 用户系统。 这两个功能各自都非常简单可靠,但是其中重叠的部分需求却是一个难题:「如何在小程序中支持 unionid 登录,既能得到 unionid 登录机制的灵活性,又保留一键登录功能的便利性」。 在最近发布的 JavaScript SDK v3.13 中包含了微信小程序 unionid 登录支持。我们根据不同的需求设计了不同的解决方案。 * 这里的版本指开始支持该功能的 JavaScript SDK 版本。 一键登录 LeanCloud 的用户系统支持一键使用微信用户身份登录。要使用一键登录功能,需要先设置小程序的 AppID 与 AppSecret: 1.登录 微信公众平台,在 设置 > 开发设置 中获得 AppID 与 AppSecret。 前往 LeanCloud 控制台 > 组件 > 社交,保存「微信小程序」的 AppID 与 AppSecret。 这样你就可以在应用中使用[代码]AV.User.loginWithWeapp()[代码]方法来使用当前用户身份登录了。 [代码]AV.User.loginWithWeapp().then(user => { this.globalData.user = user; }).catch(console.error); [代码] 使用一键登录方式登录时,LeanCloud 会将该用户的小程序 [代码]openid[代码] 与 [代码]session_key[代码] 等信息保存在对应的 [代码]user.authData.lc_weapp[代码] 属性中,你可以在控制台的 [代码]_User[代码] 表中看到: [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA" } } } [代码] 如果用户是第一次使用此应用,调用登录 API 会创建一个新的用户,你可以在 控制台 > 存储 中的 [代码]_User[代码] 表中看到该用户的信息,如果用户曾经使用该方式登录过此应用(存在对应 openid 的用户),再次调用登录 API 会返回同一个用户。 用户的登录状态会保存在客户端中,可以使用 [代码]AV.User.current()[代码] 方法来获取当前登录的用户,下面的例子展示了如何为登录用户保存额外的信息: [代码]// 假设已经通过 AV.User.loginWithWeapp() 登录 // 获得当前登录用户 const user = AV.User.current(); // 调用小程序 API,得到用户信息 wx.getUserInfo({ success: ({userInfo}) => { // 更新当前用户的信息 user.set(userInfo).save().then(user => { // 成功,此时可在控制台中看到更新后的用户信息 this.globalData.user = user; }).catch(console.error); } }); [代码] [代码]authData[代码] 默认只有对应用户可见,开发者可以使用 masterKey 在云引擎中获取该用户的 [代码]openid[代码] 与 [代码]session_key[代码] 进行支付、推送等操作。详情的示例请参考 支付。 小程序的登录态([代码]session_key[代码])存在有效期,可以通过 wx.checkSession() 方法检测当前用户登录态是否有效,失效后可以通过调用 [代码]AV.User.loginWithWeapp()[代码] 重新登录。 使用 unionid 微信开放平台使用 unionid 来区分用户的唯一性,也就是说同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 unionid 都是同一个,而 openid 会是多个。如果你想要实现多个小程序之间,或者小程序与使用微信开放平台登录的应用之间共享用户系统的话,则需要使用 unionid 登录。 要在小程序中使用 unionid 登录,请先确认已经在 微信开放平台 绑定了该小程序 在小程序中有很多途径可以 获取到 unionid。不同的 unionid 获取方式,接入 LeanCloud 用户系统的方式也有所不同。 一键登录时静默获取 unionid 当满足以下条件时,一键登录 API [代码]AV.User.loginWithWeapp()[代码] 能静默地获取到用户的 unionid 并用 unionid + openid 进行匹配登录。 微信开放平台帐号下存在同主体的公众号,并且该用户已经关注了该公众号。 微信开放平台帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。 要启用这种方式,需要在一键登录时指定参数 [代码]preferUnionId[代码] 为 true: [代码]AV.User.loginWithWeapp({ preferUnionId: true, }); [代码] 使用 unionid 登录后,用户的 authData 中会增加 _[代码]weixin_unionid[代码] 一项(与 [代码]lc_weapp[代码] 平级): [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA", "unionid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" }, "_weixin_unionid": { "uid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" } } } [代码] 用 unionid + openid 登录时,会按照下面的步骤进行用户匹配: 如果已经存在对应 [代码]unionid(authData._weixin_unionid.uid[代码])的用户,则会直接作为这个用户登录,并将所有信息([代码]openid[代码]、[代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中。 如果不存在匹配 unionid 的用户,但存在匹配 openid([代码]authData.lc_weapp.openid[代码])的用户,则会直接作为这个用户登录,并将所有信息([代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中,同时将 [代码]unionid[代码] 保存到 [代码]authData._weixin_unionid.uid[代码] 中。 如果不存在匹配 unionid 的用户,也不存在匹配 openid 的用户,则创建一个新用户,将所有信息([代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中,同时将 [代码]unionid[代码] 保存到 [代码]authData._weixin_unionid.uid[代码] 中。 不管匹配的过程是如何的,最终登录用户的 [代码]authData[代码] 都会是上面这种结构。 LeanTodo Demo 便是使用这种方式登录的,如果你已经关注了其关联的公众号(搜索 AVOSCloud,或通过小程序关于页面的相关公众号链接访问),那么你在登录后会在 LeanTodo Demo 的 设置 - 用户 页面看到当前用户的 [代码]authData[代码] 中已经绑定了 unionid。 [图片] 微信扫描二维码进入 Demo 需要注意的是: 如果用户不符合上述静默获取 unionid 的条件,那么就算指定了 [代码]preferUnionId[代码] 也不会使用 unionid 登录。 如果用户符合上述静默获取 unionid 的条件,但没有指定 [代码]preferUnionId[代码],那么该次登录不会使用 unionid 登录,但仍然会将获取到的 unionid 作为一般字段写入该用户的 [代码]authData.lc_weapp[代码] 中。此时用户的 [代码]authData[代码] 会是这样的: [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA", "unionid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" } } } [代码] 通过其他方式获取 unionid 后登录 如果开发者自行获得了用户的 unionid(例如通过解密 wx.getUserInfo 获取到的用户信息),可以在小程序中调用 [代码]AV.User.loginWithWeappWithUnionId()[代码] 投入 unionid 完成登录授权: [代码]AV.User.loginWithWeappWithUnionId(unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 通过其他方式获取 unionid 与 openid 后登录 如果开发者希望更灵活的控制小程序的登录流程,也可以自行在服务端实现 unionid 与 openid 的获取,然后调用通用的第三方 unionid 登录接口指定平台为 [代码]lc_weapp[代码] 来登录: [代码]const unionid = ''; const authData = { openid: '', session_key: '' }; const platform = 'lc_weapp'; AV.User.loginWithAuthDataAndUnionId(authData, platform, unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 相对上面提到的一些 Weapp 相关的登录 API,loginWithAuthDataAndUnionId 是更加底层的第三方登录接口,不依赖小程序运行环境,因此这种方式也提供了更高的灵活度: 可以在服务端获取到 unionid 与 openid 等信息后返回给小程序客户端,在客户端调用 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 来登录。 也可以在服务端获取到 unionid 与 openid 等信息后直接调用 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 登录,成功后得到登录用户的 [代码]sessionToken[代码] 后返回给客户端,客户端再使用该 [代码]sessionToken[代码] 直接登录。 关联第二个小程序 这种用法的另一种常见场景是关联同一个开发者帐号下的第二个小程序。 因为一个 LeanCloud 应用默认关联一个微信小程序(对应的平台名称是 [代码]lc_weapp[代码]),使用小程序系列 API 的时候也都是默认关联到 [代码]authData.lc_weapp[代码] 字段上。如果想要接入第二个小程序,则需要自行获取到 unionid 与 openid,然后将其作为一个新的第三方平台登录。这里同样需要用到 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 方法,但与关联内置的小程序平台([代码]lc_weapp[代码])有一些不同: 需要指定一个新的 [代码]platform[代码] 需要将 [代码]openid[代码] 保存为 [代码]uid[代码](内置的微信平台做了特殊处理可以直接用 [代码]openid[代码] 而这里是作为通用第三方 OAuth 平台保存因此需要使用标准的 [代码]uid[代码] 字段)。 这里我们以新的平台 [代码]weapp2[代码] 为例: [代码]const unionid = ''; const openid = ''; const authData = { uid: openid, session_key: '' }; const platform = 'weapp2'; AV.User.loginWithAuthDataAndUnionId(authData, platform, unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 获取 unionid 后与现有用户关联 如果一个用户已经登录,现在通过某种方式获取到了其 unionid(一个常见的使用场景是用户完成了支付操作后在服务端通过 getPaidUnionId 得到了 unionid)希望与之关联,可以在小程序中使用 [代码]AV.User#associateWithWeappWithUnionId()[代码]: [代码]const user = AV.User.current(); // 获取当前登录用户 user.associateWithWeappWithUnionId(unionid, { asMainAccount: true }).then(console.log, console.error); [代码] 启用其他登录方式 上述的登录 API 对接的是小程序的用户系统,所以使用这些 API 创建的用户无法直接在小程序之外的平台上登录。如果需要使用 LeanCloud 用户系统提供的其他登录方式,如用手机号验证码登录、邮箱密码登录等,在小程序登录后设置对应的用户属性即可: [代码]// 小程序登录 AV.User.loginWithWeapp().then(user => { // 设置并保存手机号 user.setMobilePhoneNumber('13000000000'); return user.save(); }).then(user => { // 发送验证短信 return AV.User.requestMobilePhoneVerify(user.getMobilePhoneNumber()); }).then({ // 用户填写收到短信验证码后再调用 AV.User.verifyMobilePhone(code) 完成手机号的绑定 // 成功后用户的 mobilePhoneVerified 字段会被置为 true // 此后用户便可以使用手机号加动态验证码登录了 }).catch(console.error); [代码] 验证手机号码功能要求在 控制台 > 存储 > 设置 > 用户账号 启用「用户注册时,向注册手机号码发送验证短信」。 绑定现有用户 如果你的应用已经在使用 LeanCloud 的用户系统,或者用户已经通过其他方式注册了你的应用(比如在 Web 端通过用户名密码注册),可以通过在小程序中调用 [代码]AV.User#associateWithWeapp()[代码] 来关联已有的账户: [代码]// 首先,使用用户名与密码登录一个已经存在的用户 AV.User.logIn('username', 'password').then(user => { // 将当前的微信用户与当前登录用户关联 return user.associateWithWeapp(); }).catch(console.error); [代码] 更多内容欢迎查看《在微信小程序与小游戏中使用 LeanCloud》。
2019-04-28 - 揭秘!我们研究了20000+条小程序违规记录,这些雷区要避开!
作者:Eva 在今年的微信之夜上,张小龙提到一个小程序背后的故事: “我还记得很清楚,我在微信公开课里面说我们要推出小程序这样一个服务的时候,当天晚上,我跟我们的团队就坐在一起讨论,讨论一个主题,我们小程序会有哪几种死法会挂掉?我记得特别清楚,因为当天晚上我们不是讨论小程序有多么美好的未来,而是说它有多难。” 除了开发者不再维护、用户的新鲜劲过了不再使用等原因,被微信封禁也是很多小程序的死法。 自小程序上线后,某个或某类小程序被封的消息屡见不鲜。我们用新榜小程序产品量子查查查询了从2018年6月-12月的违规小程序被封情况,发现违规被封小程序占总样本量的2.19%。也就是说,100个小程序里有2.19个小程序被封或者曾经被封。 其中,占比最高的是涉嫌违反相关法律法规和政策,占23.97%。其次是涉嫌过度营销和所选类目于小程序运营内容不符合,分别占19.21%和13.21%。而涉嫌诱导分享的只有480个小程序,占2.33%。 [图片] [图片] 为什么是这些小程序被封? 光看违规原因可能并不清楚具体所指,我们综合微信运营规范文档、媒体公开报道案例和量子查查数据试图解读一二。 “涉嫌违反相关法律法规和政策”这一原因比较笼统。从被封禁的小程序名单来看,都是一些包括博彩、赌博、暗网交易、试管婴儿、办假证、假货高仿等小程序。本就是在“刀尖上”赚钱,小程序被平台封禁也再正常不过。比如,微信曾在2018年8月发布公告,表示已经下架了300多个违规引流赌博小程序,并在审核和线上运营环节进行巡查和拦截。 [图片] 根据微信运营规范提供的截图显示,过度营销包括类似淘宝客为其他平台引流等操作,一经发现将根据违规程度对该小程序账号进行封禁“操作剪切板”功能或封号下架处理。 [图片] “所选类目与小程序运营内容不符合”也是小程序开发者容易碰到的问题,包括以修改或隐藏小程序的内容等方式避开、妨碍腾讯的发布审核。其中,不同类目的处罚规则也不一样: 金融类类目不符的,一经发现则永久下架处理。 高危类类目不符的(医疗,社交,公益,政务民生,洗浴保健,时政信息,文娱-视频),给予3天整改警告,到期未整改则永久下架处理。 其他类目不符的,给予3天整改警告,到期未整改则永久封禁“通过二维码打开小程序、被搜索、小程序内容分享”功能。 “内容属于平台未开放的服务范围”比较好理解,虽然微信小程序目前开放了320多个类目,难免还有一些服务范围没有触达。 以「小协议」为例,这是一款区块链小程序。据官方介绍,用户可以只用它3分钟就能创建一份不可篡改的协议,保证内容真实可信和双方身份可靠。据区块链业内人士推测,腾讯未将区块链列入小程序服务范围内的谨慎态度,或与区块链行业相对薄弱的政策法制环境有关。 [图片] 低俗、性暗示或色情信息也是微信严打的小程序内容。去年1月,新京报发表快评《“共享床位,异性拼房”,想啥呢》,认为主打“床位共享”的小程序「同住酒店拼房」的“异性拼房”模式和宣传语“和TA一起睡,重返20岁”,有涉黄之嫌。 同月,「同住酒店拼房」因涉及低俗、性暗示或色情信息被封。此外,附近的小程序也成了小程序涉黄的“重灾区”。去年2月,潇湘晨报发文,称长沙多个地段有人利用附近的小程序招嫖。这些小程序以“桑拿洗浴”、“顶级按摩”等名义示人,并展示一些暧昧的图片。记者经过实地暗访,发现这些小程序里的“休闲会所”隐藏在多家酒店内,提供色情服务。 涉嫌诱导分享是很多开发者相对熟悉的封禁原因。微信官方曾对诱导分享做过界定,如不得要求(包括明示或暗示)用户分享才可操作;不得通过利益诱惑诱导用户分享、传播;不得用夸张言语来胁迫、引诱用户分享。 具体有以下几种类型: 强制诱导分享后才能继续下一步操作。包括但不限于分享后方可解锁功能或能力,分享后方可查阅、下载图片或视频等。 诱导用户分享至群或其好友,完成分享至群或好友操作即可获得利益。包括但不限于:分享后即可获得礼品(包括但不限于现金、红包、实物奖品虚拟奖品等) 3.诱导分享至朋友圈内可获得包括但不限于:现金奖励、实物奖品、虚拟奖品(红包、优惠券、代金券、积分、话费、流量、信息等) 4.以与客服咨询无关的文案、图片(包括但不限于红包、抽奖、虚拟代金券等形式)诱导用户点击进入客服会话。 比如,接力“保护中华田园犬”的小程序「宠物帮领养中心」一夜之间席卷朋友圈和微信群,吸引超300万人次参与,但因涉嫌诱导分享被短暂封禁。小程序开发者或许可以参考小程序「拼多多」引导分享的套路。毕竟没被封、尚且存在的玩法应该就是微信审核能接受的程度,对很多小程序来说都有借鉴意义。 [图片] 最后,还有一条封禁原因值得关注“经属地互联网信息内容主管部门二次审核不通过”。如果是具有新闻媒体属性和社会动员功能的媒体类或含有UGC内容的社交小程序,腾讯可能会统一提交到中央网信办。中央网信办将根据小程序的属地,分配到属地网信办。属地网信办会根据开发者提供的资料,观察小程序是否有违规现象,确定小程序是需要下架还是继续正常运行。 [图片] 如何避免一夜归零? 对于开发者来说,辛辛苦苦开发的小程序因触及红线,走向死亡可以说是得不偿失。怎么避免这种情况的出现? 我们总结了一些经验: 仔细阅读《微信小程序平台运营规范》,仔细阅读《微信小程序平台运营规范》,仔细阅读《微信小程序平台运营规范》。 重要的事情说三遍。 如果有不清楚的地方,也可以通过微信官方的小程序「微信法务」查询条款和规范。或者多浏览微信官方的开发者社区,了解更多案例,针对性地解决问题。 根据小程序的实际情况选择相应类目。如果服务类目有调整,一定要及时更新。要知道,微信团队会根据对个别小程序做回顾,一旦发现,会有更重的处理。 微信团队公布信息显示,2018年有超过4000起小程序侵权投诉。开发者如果收到了侵权通知,一定要认真阅读。如果侵权行为确实存在,最好先行处理,避免遭遇下架。 [图片] 封禁小程序对小程序开发者来说可能是灭顶之灾,但对于微信生态的整体来说,未尝不是一件好事。其实,这也是微信生态自我净化的必经阶段,逐步规范化也是为了平台整体的积极向上。
2019-02-21 - 微信小程序银行卡号的识别
整体流程图如下: [图片] 1、小程序调起摄像头 <camera device-position=“back” flash=“off” binderror=“error” mode=“normal” style=“width: 100%; height: 300px;”></camera> <button type=“primary” bindtap=“takePhoto”>拍照</button> 2、拍摄照片并把照片转化为base64编码 拍摄的照片路径为临时路径,所以需要转化为base64编码 [代码]takePhoto() { const me = this; const ctx = wx.createCameraContext() ctx.takePhoto({ quality: 'normal', success: (res) => { const fs = wx.getFileSystemManager(); fs.readFile({ filePath: res.tempImagePath, encoding: 'base64', success(res) { } }) } }) }, [代码] 3、图片传送给第三方云平台ocr 接口(这里是以百度云举例) 1、获取第三方云平台的token [代码] const token = await identificationCard.getToken({ query: { grant_type: 'client_credentials', client_id: '自己申请的账号', client_secret: '自己申请的密钥' } }) [代码] 2、第三方云平台解析拍摄的银行卡 [代码]identificationCard.getCardDetail返回的card就是相应图片中银行卡信息 const card = await identificationCard.getCardDetail(token.data.access_token, { query: { image: base64 }, method: "POST" }) [代码] 4、完整代码 wxml <camera device-position=“back” flash=“off” binderror=“error” mode=“normal” style=“width: 100%; height: 300px;”></camera> <button type=“primary” bindtap=“takePhoto”>拍照</button> [代码]api const identificationCard = { getToken: (params) => wxRequest(params, 'https://aip.baidubce.com/oauth/2.0/token'), getCardDetail: (query, params)=> wxRequest(params, 'https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard?access_token='+query), } js async getCardDetail(base64){ const token = await identificationCard.getToken({ query: { grant_type: 'client_credentials', client_id: '自己申请的账号', client_secret: '自己申请的密钥' } }) const card = await identificationCard.getCardDetail(token.data.access_token, { query: { image: base64 }, method: "POST" }) } takePhoto() { const me = this; const ctx = wx.createCameraContext() ctx.takePhoto({ quality: 'normal', success: (res) => { const fs = wx.getFileSystemManager(); fs.readFile({ filePath: res.tempImagePath, encoding: 'base64', success(res) { me.getCardDetail(res.data) } }) } }) } ```[代码]
2019-02-21 - 如何写出一手好的小程序之多端架构篇
本文大致需要 14m+ 的阅读时间。 简述小程序的通信体系 为了大家能更好的开发出一些高质量、高性能的小程序,这里带大家理解一下小程序在不同端上架构体系的区分,更好的让大家理解小程序一些特有的代码写作方式。 整个小程序开发生态主要可以分为两部分: 桌面 nwjs 的微信开发者工具(PC 端) 移动 APP 的正式运行环境 一开始的考虑是使用双线程模型来解决安全和可控性问题。不过,随着开发的复杂度提升,原有的双线程通信耗时对于一些高性能的小程序来说,变得有些不可接受。也就是每次更新 UI 都是通过 webview 来手动调用 API 实现更新。原始的基础架构,可以参考官方图: [图片] 不过上面那张图其实有点误导行为,因为,webview 渲染执行在手机端上其实是内核来操作的,webview 只是内核暴露的一下 DOM/BOM 接口而已。所以,这里就有一个性能突破点就是,JSCore 能否通过 Native 层直接拿到内核的相关接口?答案是可以的,所以上面那种图其实可以简单的再进行一下相关划分,新的如图所示: [图片] 简单来说就是,内核改改,然后将规范的 webview 接口,选择性的抽一份给 JsCore 调用。但是,有个限制是 Android 端比较自由,通过 V8 提供 plugin 机制可以这么做,而 IOS 上,苹果爸爸是不允许的,除非你用的是 IOS 原生组件,这样的话就会扯到同层渲染这个逻辑。其实他们的底层内容都是一致的。 后面为了大家能更好理解在小程序具体开发过程中,手机端调试和在开发者工具调试的大致区分,下面我们来分析一下两者各自的执行逻辑。 tl;dr 开发者工具 通信体系 (只能采用双向通信) 即,所有指令都是通过 appservice <=> nwjs 中间层 <=> webview Native 端运行的通信体系: 小程序基础通信:双向通信-- ( core <=> webview <=> intermedia <=> appservice ) 高阶组件通信:单向通信体系 ( appservice <= android/Swift => core) JSCore 具体执行 appservice 的逻辑内容 开发者工具的通信模式 一开始考虑到安全可控的原因使用的是双线程模型,简单来说你的所有 JS 执行都是在 JSCore 中完成的,无论是绑定的事件、属性、DOM操作等,都是。 开发者工具,主要是运行在 PC 端,它内部是使用 nwjs 来做,不过为了更好的理解,这里,直接按照 nwjs 的大致技术来讲。开发者工具使用的架构是 基于 nwjs 来管理一个 webviewPool,通过 webviewPool 中,实现 appservice_webview 和 content_webview。 所以在小程序上的一些性能难点,开发者工具上并不会构成很大的问题。比如说,不会有 canvas 元素上不能放置 div,video 元素不能设置自定义控件等。整个架构如图: [图片] 当你打开开发者工具时,你第一眼看见的其实是 appservice_webview 中的 [代码]Console[代码] 内容。 [图片] content_webview 对外其实没必要暴露出来,因为里面执行的小程序底层的基础库和 开发者实际写的代码关系不大。大家理解的话,可以就把显示的 WXML 假想为 content_webview。 [图片] 当你在实际预览页面执行逻辑时,都是通过 content_webview 把对应触发的信令事件传递给 service_webview。因为是双线程通信,这里只要涉及到 DOM 事件处理或者其他数据通信的都是异步的,这点在写代码的时候,其实非常重要。 如果在开发时,需要什么困难,欢迎联系:开发者专区 | 微信开放社区 IOS/Android 协议分析 前面简单了解了开发者工具上,小程序模拟的架构。而实际运行到手机上,里面的架构设计可能又会有所不同。主要的原因有: IOS 和 Android 对于 webview 的渲染逻辑不同 手机上性能瓶颈,JS 原始不适合高性能计算 video 等特殊元素上不能被其他 div 覆盖 … 一开始做小程序的双线程架构和开发者工具比较类似,content_webview 控制页面渲染,appservice 在手机上使用 JSCore 来进行执行。它的默认架构图其实就是这个: [图片] 但是,随着用户量的满满增多,对小程序的期望也就越高: 小程序的性能是被狗吃了么? 小程序打开速度能快一点么? 小程序的包大小为什么这么小? … 这些,我们都知道,所以都在慢慢一点一点的优化。考虑到原生 webview 的渲染性能很差,组内大神 rex 提出了使用同层渲染来解决性能问题。这个办法,不仅搞定了 video 上不能覆盖其他元素,也提高了一下组件渲染的性能。 开发者在手机上具体开发时,对于某些 高阶组件,像 video、canvas 之类的,需要注意它们的通信架构和上面的双线程通信来说,有了一些本质上的区别。为了性能,这里底层使用的是原生组件来进行渲染。这里的通信成本其实就回归到 native 和 appservice 的通信。 为了大家更好的理解 appservice 和 native 的关系,这里顺便简单介绍一下 JSCore 的相关执行方法。 JSCore 深入浅出 在 IOS 和 Android 上,都提供了 JSCore 这项工程技术,目的是为了独立运行 JS 代码,而且还提供了 JSCore 和 Native 通信的接口。这就意味着,通过 Native 调起一个 JSCore,可以很好的实现 Native 逻辑代码的日常变更,而不需要过分的依靠发版本来解决对应的问题,其实如果不是特别严谨,也可以直接说是一种 "热更新" 机制。 在 Android 和 IOS 平台都提供了各自运行的 JSCore,在国内大环境下运行的工程库为: Anroid: 国内平台较为分裂,不过由于其使用的都是 Google 的 Android 平台,所以,大部分都是基于 chromium 内核基础上,加上中间层来实现的。在腾讯内部通常使用的是 V8 JSCore。 IOS: 在 IOS 平台上,由于是一整个生态闭源,在使用时,只能是基于系统内嵌的 webkit 引擎来执行,提供 webkit-JavaScriptCore 来完成。 这里我们主要以具有官方文档的 webkit-JavaScriptCore 来进行讲解。 JSCore 核心基础 普遍意义上的 JSCore 执行架构可以分为三部分 JSVirtualMachine、JSContext、JSValue。由这三者构成了 JSCore 的执行内容。具体解释参考如下: JSVirtualMachine: 它通过实例化一个 VM 环境来执行 js 代码,如果你有多个 js 需要执行,就需要实例化多个 VM。并且需要注意这几个 VM 之间是不能相互交互的,因为容易出现 GC 问题。 JSContext: jsContext 是 js代码执行的上下文对象,相当于一个 webview 中的 window 对象。在同一个 VM 中,你可以传递不同的 Context。 JSValue: 和 WASM 类似,JsValue 主要就是为了解决 JS 数据类型和 swift 数据类型之间的相互映射。也就是说任何挂载在 jsContext 的内容都是 JSValue 类型,swift 在内部自动实现了和 JS 之间的类型转换。 大体内容可以参考这张架构图: [图片] 当然,除了正常的执行逻辑的上述是三个架构体外,还有提供接口协议的类架构。 JSExport: 它 是 JSCore 里面,用来暴露 native 接口的一个 protocol。简单来说,它会直接将 native 的相关属性和方法,直接转换成 prototype object 上的方法和属性。 简单执行 JS 脚本 使用 JSCore 可以在一个上下文环境中执行 JS 代码。首先你需要导入 JSCore: [代码]import JavaScriptCore //记得导入JavaScriptCore [代码] 然后利用 Context 挂载的 evaluateScript 方法,像 new Function(xxx) 一样传递字符串进行执行。 [代码]let contet:JSContext = JSContext() // 实例化 JSContext context.evaluateScript("function combine(firstName, lastName) { return firstName + lastName; }") let name = context.evaluateScript("combine('villain', 'hr')") print(name) //villainhr // 在 swift 中获取 JS 中定义的方法 let combine = context.objectForKeyedSubscript("combine") // 传入参数调用: // 因为 function 传入参数实际上就是一个 arguemnts[fake Array],在 swift 中就需要写成 Array 的形式 let name2 = combine.callWithArguments(["jimmy","tian"]).toString() print(name2) // jimmytian [代码] 如果你想执行一个本地打进去 JS 文件的话,则需要在 swift 里面解析出 JS 文件的路径,并转换为 String 对象。这里可以直接使用 swift 提供的系统接口,Bundle 和 String 对象来对文件进行转换。 [代码]lazy var context: JSContext? = { let context = JSContext() // 1 guard let commonJSPath = Bundle.main.path(forResource: "common", ofType: "js") else { // 利用 Bundle 加载本地 js 文件内容 print("Unable to read resource files.") return nil } // 2 do { let common = try String(contentsOfFile: commonJSPath, encoding: String.Encoding.utf8) // 读取文件 _ = context?.evaluateScript(common) // 使用 evaluate 直接执行 JS 文件 } catch (let error) { print("Error while processing script file: \(error)") } return context }() [代码] JSExport 接口的暴露 JSExport 是 JSCore 里面,用来暴露 native 接口的一个 protocol,能够使 JS 代码直接调用 native 的接口。简单来说,它会直接将 native 的相关属性和方法,直接转换成 prototype object 上的方法和属性。 那在 JS 代码中,如何执行 Swift 的代码呢?最简单的方式是直接使用 JSExport 的方式来实现 class 的传递。通过 JSExport 生成的 class,实际上就是在 JSContext 里面传递一个全局变量(变量名和 swift 定义的一致)。这个全局变量其实就是一个原型 prototype。而 swift 其实就是通过 context?.setObject(xxx) API ,来给 JSContext 导入一个全局的 Object 接口对象。 那应该如何使用该 JSExport 协议呢? 首先定义需要 export 的 protocol,比如,这里我们直接定义一个分享协议接口: [代码]@objc protocol WXShareProtocol: JSExport { // js调用App的微信分享功能 演示字典参数的使用 func wxShare(callback:(share)->Void) // setShareInfo func wxSetShareMsg(dict: [String: AnyObject]) // 调用系统的 alert 内容 func showAlert(title: String,msg:String) } [代码] 在 protocol 中定义的都是 public 方法,需要暴露给 JS 代码直接使用的,没有在 protocol 里面声明的都算是 私有 属性。接着我们定义一下具体 WXShareInface 的实现: [代码]@objc class WXShareInterface: NSObject, WXShareProtocol { weak var controller: UIViewController? weak var jsContext: JSContext? var shareObj:[String:AnyObject] func wxShare(_ succ:()->{}) { // 调起微信分享逻辑 //... // 成功分享回调 succ() } func setShareMsg(dict:[String:AnyObject]){ self.shareObj = ["name":dict.name,"msg":dict.msg] // ... } func showAlert(title: String, message: String) { let alert = AlertController(title: title, message: message, preferredStyle: .Alert) // 设置 alert 类型 alert.addAction(AlertAction(title: "确定", style: .Default, handler: nil)) // 弹出消息 self.controller?.presentViewController(alert, animated: true, completion: nil) } // 当用户内容改变时,触发 JS 中的 userInfoChange 方法。 // 该方法是,swift 中私有的,不会保留给 JSExport func userChange(userInfo:[String:AnyObject]) { let jsHandlerFunc = self.jsContext?.objectForKeyedSubscript("\(userInfoChange)") let dict = ["name": userInfo.name, "age": userInfo.age] jsHandlerFunc?.callWithArguments([dict]) } } [代码] 类是已经定义好了,但是我们需要将当前的类和 JSContext 进行绑定。具体步骤是将当前的 Class 转换为 Object 类型注入到 JSContext 中。 [代码]lazy var context: JSContext? = { let context = JSContext() let shareModel = WXShareInterface() do { // 注入 WXShare Class 对象,之后在 JSContext 就可以直接通过 window.WXShare 调用 swift 里面的对象 context?.setObject(shareModel, forKeyedSubscript: "WXShare" as (NSCopying & NSObjectProtocol)!) } catch (let error) { print("Error while processing script file: \(error)") } return context }() [代码] 这样就完成了将 swift 类注入到 JSContext 的步骤,余下的只是调用问题。这里主要考虑到你 JS 执行的位置。比如,你可以直接通过 JSCore 执行 JS,或者直接将 JSContext 和 webview 的 Context 绑定在一起。 直接本地执行 JS 的话,我们需要先加载本地的 js 文件,然后执行。现在本地有一个 share.js 文件: [代码]// share.js 文件 WXShare.setShareMsg({ name:"villainhr", msg:"Learn how to interact with JS in swift" }); WXShare.wxShare(()=>{ console.log("the sharing action has done"); }) [代码] 然后,我们需要像之前一样加载它并执行: [代码]// swift native 代码 // swift 代码 func init(){ guard let shareJSPath = Bundle.main.path(forResource:"common",ofType:"js") else{ return } do{ // 加载当前 shareJS 并使用 JSCore 解析执行 let shareJS = try String(contentsOfFile: shareJSPath, encoding: String.Encoding.utf8) self.context?.evaluateScript(shareJS) } catch(let error){ print(error) } } [代码] 如果你想直接将当前的 WXShareInterface 绑定到 Webview Context 中的话,前面实例的 Context 就需要直接修改为 webview 的 Context。对于 UIWebview 可以直接获得当前 webview 的Context,但是 WKWebview 已经没有了直接获取 context 的接口,wkwebview 更推崇使用前文的 scriptMessageHandler 来做 jsbridge。当然,获取 wkwebview 中的 context 也不是没有办法,可以通过 KVO 的 trick 方式来拿到。 [代码]// 在 webview 加载完成时,注入相关的接口 func webViewDidFinishLoad(webView: UIWebView) { // 加载当前 View 中的 JSContext self.jsContext = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext let model = WXShareInterface() model.controller = self model.jsContext = self.jsContext // 将 webview 的 jsContext 和 Interface 绑定 self.jsContext.setObject(model, forKeyedSubscript: "WXShare") // 打开远程 URL 网页 // guard let url = URL(string: "https://www.villainhr.com") else { // return //} // 如果没有加载远程 URL,可以直接加载 // let request = URLRequest(url: url) // webView.load(request) // 在 jsContext 中直接以 html 的形式解析 js 代码 // let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html") // self.jsContext.evaluateScript(try? String(contentsOfURL: url!, encoding: NSUTF8StringEncoding)) // 监听当前 jsContext 的异常 self.jsContext.exceptionHandler = { (context, exception) in print("exception:", exception) } } [代码] 然后,我们可以直接通过上面的 share.js 调用 native 的接口。 原生组件的通信 JSCore 实际上就是在 native 的一个线程中执行,它里面没有 DOM、BOM 等接口,它的执行和 nodeJS 的环境比较类似。简单来说,它就是 ECMAJavaScript 的解析器,不涉及任何环境。 在 JSCore 中,和原生组件的通信其实也就是 native 中两个线程之间的通信。对于一些高性能组件来说,这个通信时延已经减少很多了。 那两个之间通信,是传递什么呢? 就是 事件,DOM 操作等。在同层渲染中,这些信息其实都是内核在管理。所以,这里的通信架构其实就变为: [图片] Native Layer 在 Native 中,可以通过一些手段能够在内核中设置 proxy,能很好的捕获用户在 UI 界面上触发的事件,这里由于涉及太深的原生知识,我就不过多介绍了。简单来说就是,用户的一些 touch 事件,可以直接通过 内核暴露的接口,在 Native Layer 中触发对应的事件。这里,我们可以大致理解内核和 Native Layer 之间的关系,但是实际渲染的 webview 和内核有是什么关系呢? 在实际渲染的 webview 中,里面的内容其实是小程序的基础库 JS 和 HTML/CSS 文件。内核通过执行这些文件,会在内部自己维护一个渲染树,这个渲染树,其实和 webview 中 HTML 内容一一对应。上面也说过,Native Layer 也可以和内核进行交互,但这里就会存在一个 线程不安全的现象,有两个线程同时操作一个内核,很可能会造成泄露。所以,这里 Native Layer 也有一些限制,即,它不能直接操作页面的渲染树,只能在已有的渲染树上去做节点类型的替换。 最后总结 这篇文章的主要目的,是让大家更加了解一下小程序架构模式在开发者工具和手机端上的不同,更好的开发出一些高性能、优质的小程序应用。这也是小程序中心一直在做的事情。最后,总结一下前面将的几个重要的点: 开发者工具只有双线程架构,通过 appservice_webview 和 content_webview 的通信,实现小程序手机端的模拟。 手机端上,会根据组件性能要求的不能对应优化使用不同的通信架构。 正常 div 渲染,使用 JSCore 和 webview 的双线程通信 video/map/canvas 等高阶组件,通常是利用内核的接口,实现同层渲染。通信模式就直接简化为 内核 <=> Native <=> appservice。(速度贼快) 参考: 教程 | 《小程序开发指南》
2019-02-19 - 【微信小程序】性能优化
为什么要做性能优化? 一切性能优化都是为了体验优化 1. 使用小程序时,是否会经常遇到如下问题? 打开是一直白屏 打开是loading态,转好几圈 我的页面点了怎么跳转这么慢? 我的列表怎么越滑越卡? 2. 我们优化的方向有哪些? 启动加载性能 渲染性能 3. 启动加载性能 1. 首次加载 你是否见过小程序首次加载时是这样的图? [图片] 这张图中的三种状态对应的都是什么呢? 小程序启动时,微信会为小程序展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作:[代码]下载小程序代码包[代码]、[代码]加载小程序代码包[代码]、[代码]初始化小程序首页[代码]。下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包。 2. 加载顺序 小程序加载的顺序是如何? 微信会在小程序启动前为小程序准备好通用的运行环境。这个运行环境包括几个供小程序使用的线程,并在其中完成小程序基础库的初始化,预先执行通用逻辑,尽可能做好小程序的启动准备。这样可以显著减少小程序的启动时间。 [图片] 通过2,我们知道了,问题1中第一张图是[代码]资源准备[代码](代码包下载);第二张图是[代码]业务代码的注入以及落地页首次渲染[代码];第三张图是[代码]落地页数据请求时的loading态[代码](部分小程序存在) 3. 控制包大小 提升体验最直接的方法是控制小程序包的大小,这是最显而易见的 勾选开发者工具中“上传代码时,压缩代码”选项; 及时清理无用的代码和资源文件(包括无用的日志代码) 减少资源包中的图片等资源的数量和大小(理论上除了小icon,其他图片资源从网络下载),图片资源压缩率有限 从开发者的角度看,控制代码包大小有助于减少小程序的启动时间。对低于1MB的代码包,其下载时间可以控制在929ms(iOS)、1500ms(Android)内。 4. 采用分包加载机制 根据业务场景,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载; [图片] 使用分包时需要注意代码和资源文件目录的划分。启动时需要访问的页面及其依赖的资源文件应放在主包中。 5 采用分包预加载技术 在4的基础上,当用户点击到子包的目录时,还是有一个代码包下载的过程,这会感觉到明显的卡顿,所以子包也不建议拆的太大,当然我们可以采用子包预加载技术,并不需要等到用户点击到子包页面后在下载子包,而是可以根据后期数据,做子包预加载,将用户在当先页可能点击的子包页面先加载,当用户点击后直接跳转; [图片] 这种基于配置的子包预加载技术,是可以根据用户网络类型来判断的,当用户处于网络条件好时才预加载;是灵活可控的 6. 采用独立分包技术 目前很多小程序[代码]主包+子包[代码](2M+6M)的方式,但是在做很多运营活动时,我们会发现活动(红包)是在子包里,但是运营、产品投放的落地页链接是子包链接,这是的用户在直达落地时,必须先下载主包内容(一般比较大),在下载子包内容(相对主包,较小),这使得在用户停留时间比较短的小程序场景中,用户体验不是很好,而且浪费了很大部分流量; [图片] 可以采用独立分包技术,区别于子包,和主包之间是无关的,在功能比较独立的子包里,使用户只需下载分包资源; 7. 首屏加载的优化建议 7.1 提前请求 异步请求可以在页面onLoad就加载,不需要等页面ready后在异步请求数据;当然,如果能在前置页面点击跳转时预请求当前页的核心异步请求,效果会更好; 7.2 利用缓存 利用storage API, 对变动频率比较低的异步数据进行缓存,二次启动时,先利用缓存数据进行初始化渲染,然后后台进行异步数据的更新,这不仅优化了性能,在无网环境下,用户也能很顺畅的使用到关键服务; 7.3 避免白屏 可以在前置页面将一些有用的字段带到当前页,进行首次渲染(列表页的某些数据–> 详情页),没有数据的模块可以进行骨架屏的占位,使用户不会等待的很焦虑,甚至走了; 7.4 及时反馈 及时的对需要用户等待的交互操作进行反馈,避免用户以为小程序卡了,无响应 渲染性能优化 1. 小程序渲染原理 双线程下的界面渲染,小程序的逻辑层和渲染层是分开的两个线程。在渲染层,宿主环境会把WXML转化成对应的JS对象,在逻辑层发生数据变更的时候,我们需要通过宿主环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。 [图片] 分析这个流程不难得知:页面初始化的时间大致由页面初始数据通信时间和初始渲染时间两部分构成。其中,数据通信的时间指数据从逻辑层开始组织数据到视图层完全接收完毕的时间,数据量小于64KB时总时长可以控制在30ms内。传输时间与数据量大体上呈现正相关关系,传输过大的数据将使这一时间显著增加。因而减少传输数据量是降低数据传输时间的有效方式。 [图片] 2. 避免使用不当setData 在数据传输时,逻辑层会执行一次[代码]JSON.stringify[代码]来去除掉[代码]setData[代码]数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将[代码]setData[代码]所设置的数据字段与[代码]data[代码]合并,使开发者可以用[代码]this.data[代码]读取到变更后的数据。因此,为了提升数据更新的性能,开发者在执行[代码]setData[代码]调用时,最好遵循以下原则: 2.1 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用; [图片] 2.2 数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用[代码]setData[代码]来设置这些数据; [图片] 2.3 与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下 [图片] 提升数据更新性能方式的代码示例 [代码]Page({ onShow: function() { // 不要频繁调用setData this.setData({ a: 1 }) this.setData({ b: 2 }) // 绝大多数时候可优化为 this.setData({ a: 1, b: 2 }) // 不要设置不在界面渲染时使用的数据,并将界面无关的数据放在data外 this.setData({ myData: { a: '这个字符串在WXML中用到了', b: '这个字符串未在WXML中用到,而且它很长…………………………' } }) // 可以优化为 this.setData({ 'myData.a': '这个字符串在WXML中用到了' }) this._myData = { b: '这个字符串未在WXML中用到,而且它很长…………………………' } } }) [代码] 利用setData进行列表局部刷新 在一个列表中,有[代码]n[代码]条数据,采用上拉加载更多的方式,假如这个时候想对其中某一个数据进行点赞操作,还能及时看到点赞的效果 解决方法 1、可以采用setData全局刷新,点赞完成之后,重新获取数据,再次进行全局重新渲染,这样做的优点是:方便,快捷!缺点是:用户体验极其不好,当用户刷量100多条数据后,重新渲染量大会出现空白期(没有渲染过来) 2、说到重点了,就是利用[代码]setData[代码]局部刷新 [代码]> a.将点赞的`id`传过去,知道点的是那一条数据, 将点赞的`id`传过去,知道点的是那一条数据 [代码] [代码]<view wx:if="{{!item.status}}" class="btn" data-id="{{index}}" bindtap="couponTap">立即领取</view> [代码] [代码]> b.重新获取数据,查找相对应id的那条数据的下标(`index`是不会改变的) > c.用setData进行局部刷新 [代码] [代码]this.setData({ list[index] = newList[index] }) [代码] 其实这个小操作对刚刚接触到微信小程序的人来说应该是不容易发现的,不理解setData还有这样的写法。 2.4 切勿在后台页面进行setData 在一些页面会进行一些操作,而到页面跳转后,代码逻辑还在执行,此时多个[代码]webview[代码]是共享一个js进程;后台的[代码]setData[代码]操作会抢占前台页面的渲染资源; [图片] [图片] 3. 用户事件使用不当 视图层将事件反馈给逻辑层时,同样需要一个通信过程,通信的方向是从视图层到逻辑层。因为这个通信过程是异步的,会产生一定的延迟,延迟时间同样与传输的数据量正相关,数据量小于64KB时在30ms内。降低延迟时间的方法主要有两个。 1.去掉不必要的事件绑定(WXML中的[代码]bind[代码]和[代码]catch[代码]),从而减少通信的数据量和次数; 2.事件绑定时需要传输[代码]target[代码]和[代码]currentTarget[代码]的[代码]dataset[代码],因而不要在节点的[代码]data[代码]前缀属性中放置过大的数据。 [图片] 4. 视图层渲染原理 4.1首次渲染 初始渲染发生在页面刚刚创建时。初始渲染时,将初始数据套用在对应的WXML片段上生成节点树。节点树也就是在开发者工具WXML面板中看到的页面树结构,它包含页面内所有组件节点的名称、属性值和事件回调函数等信息。最后根据节点树包含的各个节点,在界面上依次创建出各个组件。 [图片] 在这整个流程中,时间开销大体上与节点树中节点的总量成正比例关系。因而减少WXML中节点的数量可以有效降低初始渲染和重渲染的时间开销,提升渲染性能。 简化WXML代码的例子 [代码]<view data-my-data="{{myData}}"> <!-- 这个 view 和下一行的 view 可以合并 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> <text> <!-- 这个 text 通常是没必要的 --> {{myText}} </text> </view> </view> <!-- 可以简化为 --> <view class="my-class" data-my-data="{{myData}}" bindtap="onTap"> {{myText}} </view> [代码] 4.2 重渲染 初始渲染完毕后,视图层可以多次应用[代码]setData[代码]的数据。每次应用[代码]setData[代码]数据时,都会执行重渲染来更新界面。初始渲染中得到的data和当前节点树会保留下来用于重渲染。每次重渲染时,将[代码]data[代码]和[代码]setData[代码]数据套用在WXML片段上,得到一个新节点树。然后将新节点树与当前节点树进行比较,这样可以得到哪些节点的哪些属性需要更新、哪些节点需要添加或移除。最后,将[代码]setData[代码]数据合并到[代码]data[代码]中,并用新节点树替换旧节点树,用于下一次重渲染。 [图片] 在进行当前节点树与新节点树的比较时,会着重比较[代码]setData[代码]数据影响到的节点属性。因而,去掉不必要设置的数据、减少[代码]setData[代码]的数据量也有助于提升这一个步骤的性能。 5. 使用自定义组件 自定义组件的更新只在组件内部进行,不受页面其他不能分内容的影响;比如一些运营活动的定时模块可以单独抽出来,做成一个定时组件,定时组件的更新并不会影响页面上其他元素的更新;各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、setData调用。 [图片] 6. 避免不当的使用onPageScroll 每一次事件监听都是一次视图到逻辑的通信过程,所以只在必要的时候监听pageSrcoll [图片] 总结 小程序启动加载性能 控制代码包的大小 分包加载 首屏体验(预请求,利用缓存,避免白屏,及时反馈 小程序渲染性能 避免不当的使用setData 合理利用事件通信 避免不当的使用onPageScroll 优化视图节点 使用自定义组件
2019-03-07 - 小程序性能和体验优化方法
[图片] 小程序应避免出现任何 JavaScript 异常 出现 JavaScript 异常可能导致小程序的交互无法进行下去,我们应当追求零异常,保证小程序的高鲁棒性和高可用性 小程序所有请求应响应正常 请求失败可能导致小程序的交互无法进行下去,应当保证所有请求都能成功 所有请求的耗时不应太久 请求的耗时太长会让用户一直等待甚至离开,应当优化好服务器处理时间、减小回包大小,让请求快速响应 避免短时间内发起太多的图片请求 短时间内发起太多图片请求会触发浏览器并行加载的限制,可能导致图片加载慢,用户一直处理等待。应该合理控制数量,可考虑使用雪碧图技术或在屏幕外的图片使用懒加载 避免短时间内发起太多的请求 短时间内发起太多请求会触发小程序并行请求数量的限制,同时太多请求也可能导致加载慢等问题,应合理控制请求数量,甚至做请求的合并等 避免 setData 的数据过大 setData工作原理 小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。 而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。 由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间 常见的 setData 操作错误 频繁的去 setData Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层 染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时 每次 setData 都传递大量新数据 由setData的底层实现可知,数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程 后台态页面进行 setData 当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行 避免 setData 的调用过于频繁 setData接口的调用涉及逻辑层与渲染层间的线程通过,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用 避免将未绑定在 WXML 的变量传入 setData setData操作会引起框架处理一些渲染界面相关的工作,一个未绑定的变量意味着与界面渲染无关,传入setData会造成不必要的性能消耗 合理设置可点击元素的响应区域大小 我们应该合理地设置好可点击元素的响应区域大小,如果过小会导致用户很难点中,体验很差 避免渲染界面的耗时过长 渲染界面的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要校验下是否同时渲染的区域太大 避免执行脚本的耗时过长 执行脚本的耗时过长会让用户觉得卡顿,体验较差,出现这一情况时,需要确认并优化脚本的逻辑 对网络请求做必要的缓存以避免多余的请求 发起网络请求总会让用户等待,可能造成不好的体验,应尽量避免多余的请求,比如对同样的请求进行缓存 wxss 覆盖率较高,较少或没有引入未被使用的样式 按需引入 wxss 资源,如果小程序中存在大量未使用的样式,会增加小程序包体积大小,从而在一定程度上影响加载速度 文字颜色与背景色搭配较好,适宜的颜色对比度更方便用户阅读 文字颜色与背景色需要搭配得当,适宜的颜色对比度可以让用户更好地阅读,提升小程序的用户体验 所有资源请求都建议使用 HTTPS 使用 HTTPS,可以让你的小程序更加安全,而 HTTP 是明文传输的,存在可能被篡改内容的风险 不使用废弃接口 使用即将废弃或已废弃接口,可能导致小程序运行不正常。一般而言,接口不会立即去掉,但保险起见,建议不要使用,避免后续小程序突然运行异常 避免过大的 WXML 节点数目 建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长 避免将不可能被访问到的页面打包在小程序包里 小程序的包大小会影响加载时间,应该尽量控制包体积大小,避免将不会被使用的文件打包进去 及时回收定时器 定时器是全局的,并不是跟页面绑定的,当页面因后退被销毁时,定时器应注意手动回收 避免使用 css ‘:active’ 伪类来实现点击态 使用 css ‘:active’ 伪类来实现点击态,很容易触发,并且滚动或滑动时点击态不会消失,体验较差 建议使用小程序内置组件的 ‘hover-*’ 属性来实现 滚动区域可开启惯性滚动以增强体验 惯性滚动会使滚动比较顺畅,在安卓下默认有惯性滚动,而在 iOS 下需要额外设置 [代码]-webkit-overflow-scrolling: touch[代码] 的样式
2019-03-15 - 微信小程序之登录态的探索
原文来自 https://segmentfault.com/a/1190000017042906 上一篇:开发微信小程序必须要知道的事 https://segmentfault.com/a/1190000017028505 登录,几乎什么项目都会用到,其重要性不言而喻,而小程序的登录却一直是为人头疼的一件事,这里我分享下我们在小程序登录上的探索通常的登录都是通过一个表单,这很正常,但如果在小程序里你也这么做那就有点不可思议了,微信的一键登录对用户体验有多好你难道不知道?不用是不是脑子有坑?最主要——你要利用微信的生态必须需要用微信的登录,以获取相关信息来和微信交互,OK,我们进入正题 用户在小程序、小游戏中需要点击组件后,才可以触发登录授权弹窗、授权自己的昵称头像等数据友情提示一下:[代码]wx.login[代码]并不需要点击组件,需要的是[代码]wx.getUserInfo[代码],但通常我们都会用到[代码]UnionID、encryptedData、iv等信息[代码]完成完整的登录流程,本文主要聚焦的也是这种场景所以之前直接通过调用API的方式就行不通了,那么问题来了——这个点击按钮要放到哪里? 放到首页,一进小程序就必须先登录。这样显然很粗暴,而且问题并没有解决,反而会把用户直接拒之门外,毕竟你不是用小程序做后台系统,什么场景都需要授权,先授权也是必须的 在需要授权的时候跳到登陆页面。这样就解决了上面遇到的不需要授权的时候也被强制授权,可是这样好吗? 体验上不好,操作被打断,尤其整个页面都不需要授权只有在一个地方需要授权的,例如:你正在读一篇文章,读罢深有感触,想评论一番,洋洋洒洒几十字写完正准备点击呢,他妈的跳转了!跳转了! 又一个漏斗,增加用户流失率。还TM要登录!很多用户心里一定这么想 那就直接放在需要登录的页面上(这不是漏斗吗?很多读者一定这么想。但想想看上面那个场景,点评论时只是需要点击下弹出的登录按钮,而且还假模假样的以微信的口吻提醒你需要登录,那你会不会登录?最起码你很愿意登录,而且来的很突然,我控几不住自己的手就点了!点了!) 可是这种方式有一个问题 怎么在需要的页面都能弹出登录按钮应该很多人都能想到:抽离出组件,那怎么保证在需要的页面都有这个组件呢?错杀一千也不能放过一个!把登录组件集成到共用的父组件,然后在每个页面都使用。我也建议这么做,因为这个共用的父组件其实又很多用处,例如iPhoneX适配等 等等,什么都准备好了,什么时候需要登录呢?XX,这个肯定是你自己控制的啦。嗯~好吧,我们来理一理 在哪里校验是否需要鉴权请求接口的时候,嗯~这是大家的共识 BOSS来了怎么鉴权 官方的这张图已经做了很详尽的说明,这里不做赘述 但是看到[代码]session_key[代码]了吗?看到官方同时说的了吗 所以问题又来了 怎么保证session_key的有效性诚如上图 要保证调用接口时后端[代码]session_key[代码]不失效,只能在每次调用前先使用[代码]wx.checkSession[代码]检查是否有效 实践中也发现[代码]wx.checkSeesion[代码]非常耗时,大约200ms,所以也不能每次接口调用前都使用[代码]wx.checkSession[代码]检查是否有效 同时要注意⚠️前端不能随便重新执行[代码]wx.login[代码],因为可能导致正在进行的其它后端任务session_key失效 天啦噜,怎么办?! 通过实践和偶然的发现——官方的示例代码 得知:在使用小程序期间session_key是不会失效的,so,你想到了什么? 在每个请求前去校验有效性 将校验有效性的结果存储起来 通过async/await和刚才存储起来的结果来保证不过多调用wx.checkSession 先问个问题:你准备用什么方式来存储校验的结果? 。。。 让思考先飞一会 。。。。。。 。。。。。。。。。 。。。。。。。。。。。。 storage吗?当然可以,不过不够完美,为什么?因为storage是永久的存储,而session_key的有效期却只是在使用小程序期间,所以你需要在小程序结束后手动重置该状态以重新校验其有效性,那是不是在app的onUnload里重置呢?不是!开发过小程序的应该都知道,那就是结束使用小程序的方式太多,不能保证每种方式都会触发onUnload,例如用户直接销毁了微信进程😳(其实你也可以在app的onShow里搞)那用什么呢?直接用内存啊,借助内存的自动管理来智能管理,所以最终代码应该是这样的 [代码]// doRequest.jslet wxSessionValid = null // 微信session_key的有效性// 带鉴权的请求封装async function doRequestWithCheckAuth() { ... if (typeof wxSessionValid !== 'boolean') { wxSessionValid = await checkWxSession() // 检查微信session是否有效 } if (!wxSessionValid) { await reLogin() // 重新登录 } wxSessionValid = true // 重新登陆后session_key一定有效 ... }[代码]这样是不是看起来比较完美了?嗯~ 不知道有没有同学着急问业务侧的session(自定义的登录态)怎么没讲?嗯,那现在讲吧 怎么校验完整的认证体系其实很简单,都不想把它作为一部分来讲,但既然讲了就必然有我想强调的 校验微信端的session_key略有麻烦,但不应该把它抛给服务端 服务端不能直接校验session_key的有效性而是通过调用接口发现错误了才知道失效了,这是被动的 服务端需要同时维护两个session 而放在前端我们只需要校验两个session的有效性即可,任何一个失效就重新登录,这是积极主动有效的操作,应该被提倡 贯通OK,基本上梳理的差不多了,就差弹登录按钮了,这个简单,调用刚才封装的组件的方法就行了嘛,bingo,可是,点完允许后呢?怎么继续用户的操作呢?怎么能让用户的体验不被打断呢?先回放下刚才reLogin的代码 [代码]async function reLogin() { // 确保有用户信息 await new Promise(resolve => { // ⚠️注意开头有await!!! wx.getSetting({ success: (res) => { // 如果用户没有授权或者没有必要的用户信息 if (!res.authSetting['scope.userInfo'] || !_.isRealTrue(wx.getStorageSync('userInfoRes').userInfo)) { navToLogin(resolve) // 去提示用户点击登录按钮,⚠️注意:并把当前的resolve带过去 } else { resolve() // 静默登录 } } }) }) return new Promise((resolve) => { wx.login({ success: res => { login(res.code).then((jwt) => { resolve(jwt) // resolve jwt }) // 通过code进行登录 }, fail(err) { wx.showToast({ title: err.errMsg, icon: 'none', duration: 2000 }) } }) }) }function navToLogin(resolve) { /* eslint-disable no-undef */ const pages = getCurrentPages() const page = pages[pages.length - 1] // 当前page page.openLoginModal(resolve) // 打开登录按钮弹框,并把当前的resolve带过去}[代码]上面的代码注释里有两个⚠️注意看到没?是的,通过回调的方式😂当用户同意授权了就继续余下的逻辑,如果被拒绝了,则安利他,再拒绝就终止操作,下次需要授权也会继续弹出授权 有不明白欢迎评论留言指出,我再做说明修改 完整源码以后会放出的,通过wepy搭建的一个框架 [代码]下一篇会讲api的封装[代码]
2018-11-16 - 蓝牙回调 监听不到消息 可以尝试改成es6 表达式的方式
[图片] [图片] 我在国庆之前用的上面那种方式是可以获取到设备消息 的 国庆后突然发现不能了 加上一直有一些其他问题 没能定位到问题所在 后面尝试改成es6 表达式的方式 发现可以获取到对应的设备数据了 可能官方忘记更新API示例了 希望官方尽快更新吧 wx.onBLECharacteristicValueChange((characteristic) => { console.log("onBLECharacteristicValueChange") })
2018-11-24 - web-view postMassage h5往小程序传参
[图片] 官方提供了三种 但是小程序后退 和分享 显然不适合 大多数 在网上找了好多方法 发现没啥用 都是套话 所以我就尝试了 组件销毁 一开始不明白 啥意思 习惯性用wx.miniProgram.navigateTo 跳转页面 但是组件并没有销毁。 于是 用了 wx.miniProgram.redirectTo 去跳转页面 发现 触发了 [图片] 感觉挺简单的 为啥网上没人推荐这种方法呢,可能有问题 但是目前是我找到一个办法 ,有问题希望大家多多提出来
2018-10-31 - 在小程序使用jsEncrypt.js
问题描述由于jsEncrypt.js代码里面含有window、document、navigator对象,这些对象可以在pc端的浏览器使用,但是小程序没有这些对象,所以直接在小程序引入jsEncrypt.js会直接报错,下面主要介绍如何在jsEncrypt.js里面对这些对象进行兼容。 jsEncrypt.js介绍功能一种RSA加密的解决方案。 这种加密模式被称为"非对称加密算法"。 (1)乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的。 (2)甲方获取乙方的公钥,然后用它对信息加密。 (3)乙方得到加密后的信息,用私钥解密。 如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。 使用场景加密验证码。 [代码]// 实例化JSEncrypt对象。并设置公钥[代码] [代码]var cryptFirst = new JSEncrypt();[代码] [代码]cryptFirst.setPublicKey('-----BEGIN PUBLIC KEY----- MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALQCZoxawKSTyMTLEU5QlYNVIOBMafGq vVGp6bFv2gQ8Px5ZJVzIG1TjIUQo1IpGQBgC+JSJFaGLsv196dlcloUCAwEAAQ== -----END PUBLIC KEY-----');[代码][代码]// 实例化JSEncrypt对象。并设置私钥[代码] [代码]var cryptSec = new JSEncrypt();[代码] [代码]cryptSec.setPrivateKey('-----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtAJmjFrApJPIxMsR TlCVg1Ug4Exp8aq9UanpsW/aBDw/HlklXMgbVOMhRCjUikZAGAL4lIkVoYuy/X3p 2VyWhQIDAQABAkBwdoN9MwHikNZBZSGFzUDsEZZ9rCAQtXycdcykZ95NAA5Mlb0j 77FLUhON8XEa+YVORwYn2GoP+ZZxGib7OtYBAiEA4nJRIzFn2Yr6y36r1rI6GP28 lxEDe5z/wytqm+XFswUCIQDLgJis+LVfzLM0MLVzDyuEy2GoLY46+WnaW8J2PSet gQIhANykSLzC2g6VJbN8VJFYjdVl/wkvMbaTjn4r4q/OnejFAiBPatgqaMUfpdsp uviU9o6dPGHYKC8hhMRymuzBCAy8AQIhAJlTJgdsJD7cjDCvFOv5v2Xz1JQQp03I vGjIsmEbdbEp -----END PRIVATE KEY-----'); var text = 'secret';[代码][代码]// 使用公钥加密数据[代码] [代码]var enc = cryptFirst.encrypt(text);[代码] [代码]// 现在使用私钥解密数据[代码] [代码]var dec = cryptSec.decrypt(enc);[代码] [代码]console.log(dec === text); // true[代码]开始兼容处理源代码在线预览:源代码地址 兼容window.crypto源代码第2754行 [代码]if (window.crypto && window.crypto.getRandomValues) { // 生成长度为256,元素值为0的数组 var z = new Uint32Array(256); // 生成长度为256,元素随机值的数组 window.crypto.getRandomValues(z); }[代码]兼容代码 [代码]var getRandomValues = function (array) { [代码] [代码] for (var i = 0, l = array.length; i < l; i++) {[代码] [代码] array[i] = Math.floor(Math.random() * 256); } return array;[代码][代码]}[代码] [代码]var z = new Uint32Array(256);[代码] [代码]getRandomValues(z);[代码]兼容window.removeEventListener、window.detachEvent源代码 [代码]if (window.removeEventListener) { [代码] [代码] window.removeEventListener("mousemove", onMouseMoveListener_1, false);[代码] [代码]} else if ([代码]window.detachEvent) { window.detachEvent("onmousemove", onMouseMoveListener_1); [代码]}[代码]兼容处理: 直接删掉,监听的事件不会影响到加密和解密 兼容navigator.appName、navigator.userAgent源代码 [代码]if (j_lm && (navigator.appName == "Microsoft Internet Explorer")) { BigInteger.prototype.am = am2; dbits = 30; } else if (j_lm && (navigator.appName != "Netscape")) { BigInteger.prototype.am = am1; dbits = 26; } else { // Mozilla/Netscape seems to prefer am3 BigInteger.prototype.am = am3; dbits = 28; }[代码]兼容处理: 直接删掉。navigator主要是对浏览器的判断 最后去一个在线压缩js的网站压缩一下代码,然后在小程序里引入就搞定啦。 总结兼容处理其实比较简单粗暴,主要学到了一些第三方库在小程序的兼容处理其实很简单,不用太过畏惧修改源码。
2018-11-21 - 环形进度条完整Demo
之前做项目需要做一个弧形进度,做了个小Demo,图一到图二的效果。(加上动图了),仅供参考! 代码片段:https://developers.weixin.qq.com/s/0rR3A6mQ7i4y [图片]
2018-12-27 - 关于小程序获取手机号解密失败的踩坑历程
小程序中获取微信手机号的前提条件这里贴一个链接,各位同学自己看 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html 主要说说为什么会解密失败,主要有以下两点原因 微信服务器报错 你解密的姿势错了 对于第一点,比较少见,如果出现了,嗯 ,坐着等死就行了 解密失败一般是由于你的 sessionKey 失效导致的,而sessionKey 由 wx.login 这个api先获取code, 再由我们的后台拿这个code向微信服务器请求获得。code有效期五分钟。 上面的链接中有这样一句话 注意 在回调中调用 [代码]wx.login[代码] 登录,可能会刷新登录态。此时服务器使用 code 换取的 sessionKey 不是加密时使用的 sessionKey,导致解密失败。建议开发者提前进行 [代码]login[代码];或者在回调中先使用 [代码]checkSession[代码] 进行登录态检查,避免 [代码]login[代码] 刷新登录态。 这句话这么理解呢,打个比方,假设你第一次调用wx.login获取的为 sessionKey_1,在它的有效期内,你再去获取一个加密数据 encryptedData_1,这时候你用sessionKey_1去解密encryptedData_1就可以正常解密。 但是在获取encryptedData_1的时候会有个回调函数,如果你在这个回调函数里又调了一次wx.login,获取了 sessionKey_2,这时候登录态可能会被刷新,加密encryptedData_1时使用的sessionKey_1就会失效,再去解密encryptedData_1就会有很高的几率解密失败。至于为什么不会百分百刷新登录态,鬼知道这逻辑是怎么写的。 贴上我的部分代码(只贴获取手机号的部分,其他逻辑请根据自己产品业务酌情参考) [代码][代码] const app = getApp(); [代码][代码] [代码]Page({[代码] [代码] [代码][代码]data: {[代码][代码] [代码][代码]code: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]},[代码] [代码] [代码][代码]// 获取手机号[代码][代码] [代码][代码]getPhoneAndLogin: [代码][代码]function[代码][代码](e){[代码][代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]// console.log(e);[代码] [代码] [代码][代码]if[代码] [代码](e.detail.errMsg !== [代码][代码]"getPhoneNumber:ok"[代码][代码]){[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]wx.showLoading({[代码][代码] [代码][代码]mask: [代码][代码]true[代码][代码],[代码][代码] [代码][代码]})[代码] [代码] [代码][代码]// 检查登录态是否过期[代码][代码] [代码][代码]wx.checkSession({[代码][代码] [代码][代码]success(res) {[代码][代码] [代码][代码]// session_key 未过期,并且在本生命周期一直有效[代码][代码] [代码][代码]// console.log(res);[代码] [代码] [代码][代码]wx.request({[代码][代码] [代码][代码]url: app.globalData.apiUrl + [代码][代码]'**********'[代码][代码],[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]code: that.data.code,[代码][代码] [代码][代码]encryptedData: e.detail.encryptedData,[代码][代码] [代码][代码]iv: e.detail.iv,[代码][代码] [代码][代码]},[代码][代码] [代码][代码]method: [代码][代码]"POST"[代码][代码],[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]// console.log(res);[代码] [代码] [代码][代码]},[代码][代码] [代码][代码]fail: res => {[代码][代码] [代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]icon: [代码][代码]"none"[代码][代码],[代码][代码] [代码][代码]title: [代码][代码]'服务器繁忙'[代码][代码],[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]complete: res => {[代码][代码] [代码][代码]wx.hideLoading();[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail(err) {[代码][代码] [代码][代码]// session_key 已经失效,需要重新执行登录流程[代码] [代码] [代码][代码]wx.login({[代码][代码] [代码][代码]success: res => {[代码][代码] [代码][代码]that.data.code = res.code[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码] [代码][代码]/**[代码][代码] [代码][代码]* 生命周期函数--监听页面加载[代码][代码] [代码][代码]*/[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码] [代码](options) {[代码][代码] [代码][代码]let that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]// console.log(options);[代码] [代码] [代码][代码]wx.login({[代码][代码] [代码][代码]success: res =>{[代码][代码] [代码][代码]that.data.code = res.code[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码] [代码]})[代码] 以上,如果有问题欢迎各位大佬指正
2018-11-28 - canvas绘制圆角头像
使用canvas的arc方法 circleImg(ctx, img, x, y, r) { ctx.save(); var d =2 * r; var cx = x + r; var cy = y + r; ctx.arc(cx, cy, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(img, x, y, d, d); ctx.restore(); }
2017-12-27