- wx.downloadFile 重复下载同一张图片会报错 download fail
不支持这样的调用,因为临时存储路径是根据url计算的,每个下载又都是相互隔离的,这里连续下载两个相同url会导致两个问题 1.两个线程同时操作这个临时存储路径,内容很可能是错乱的 2.文件移除后不存在,返回ERR_OP_FAIL
2020-04-13 - wx.downloadFile 重复下载同一张图片会报错 download fail?
版本:微信安卓7.03,机型:魅族16thpus ios7.02测试正常 代码片段: https://developers.weixin.qq.com/s/0M0wwumF7rgu 备注:不同的图片路径循环下载没有问题,一旦有相同的就会报错。 开发工具下载没问题,安卓真机调试回报download fail
2020-04-09 - input聚焦弹出键盘时, 防止自定义导航上移的一种解决方法
前提 在开发小程序的过程中,会经常使用到[代码]input[代码]组件,其聚焦的时候,会在必要时自动上移整个页面以便弹起的键盘不回遮住[代码]input[代码]组件 但是当页面应用自定义导航时,聚焦时会把自定义导航也上移,而这就不符合我们的预期,我们希望的是导航栏始终固定在顶部,下面的内容上移即可!参见该帖子 为了解决这个问题,我们只好自己来处理页面上移的事情了! 准备工作 首先我们需要先关闭掉输入框自动上移页面的功能 [代码]<input type="text" adjust-position="{{false}}" /> [代码] 接下来我们就要自己处理页面上移的工作了 为了让除了导航栏的区域整体上移, 我们需要先可以定义一个值(top)来表示上移距离 [代码]// index.js Page({ data: { top: 0 } }) [代码] 其次 需要固定好页面的结构, 如下: 我们要做的便是只上移[代码].cointainer[代码]这个组件, 故使用[代码]top: -{{top}}px[代码]来表示内容上移 [代码]// index.wxml <mp-navigation-bar background="#d8d8d8" title="导航栏标题"></mp-navigation-bar> <view class="container" style="position: relative; top: -{{top}}px"> 内容区域 </view> [代码] 计算上移距离 我们先思考什么情况下, 页面需要上移? 显然是[代码]input[代码]距离底部的距离小于弹出的键盘高度的时候需要上移, 而上移的距离便是距离底部的距离与键盘高度的差 那么只需要知道这两个值便可实现该功能了. 键盘高度 翻阅input的文档, 我们发现监听[代码]bindfocus[代码]或者[代码]bindkeyboardheightchange[代码]事件都可以获取到键盘的高度 [代码]// index.wxml # index.wxml <input id="input1" type="text" adjust-position="{{false}}" bindfocus="onHandleFocus" bindblur="onBlur" /> // index.js onHandleFocus (e) { console.log('focus', e) const keyboradHeight = e.detail.height } [代码] 距离底部的距离 首先我们需要货值输入框的位置, 这里可以使用[代码]SelectorQuery[代码]来获取输入框的位置 [代码] # index.js onHandleFocus (e) { console.log('focus', e) const keyboradHeight = e.detail.height const id = e.currentTarget.id this.createSelectorQuery() .select(`#${id}`) .boundingClientRect(rect => { console.log('==> rect', rect) }).exec() } [代码] 获取到的rect对象同DOM的[代码]getBoundingClientRect[代码], 然后我们再观察这张图片: [图片] 显然要计算输入框距离底部的距离只需用显示高度减去bottom即可 页面的显示高度可以用如下方法获得 [代码]const { windowHeight } = wx.getSystemInfoSync() [代码] 最后我们上诉思路整理成对应代码 [代码] # index.js onHandleFocus (e) { console.log('focus', e) const keyboradHeight = e.detail.height const id = e.currentTarget.id this.createSelectorQuery() .select(`#${id}`) .boundingClientRect(rect => { console.log('==> rect', rect) const { windowHeight } = wx.getSystemInfoSync() const bottom = windowHeight - rect.bottom if (bottom > keyboradHeight) { // 距离足够, 不需要上移 return } this.setData({ top: keyboradHeight - bottom }) }).exec() } [代码] 完整代码见该代码片段 尾声 该方法虽然能够解决标题所述的问题,当也存有其他问题: 页面结构必须固定结构(分为标题-内容两大块) 所有的[代码]input[代码]都必须加上[代码]id[代码],否则[代码]createSelectorQuery[代码]选择器没法找到对应的[代码]input[代码],且必须监听[代码]focus[代码]和[代码]blur[代码]方法。 最外层的内容[代码]view[代码]的[代码]top[代码]样式被占用 PS: 计算上移距离的这部分代码可以写成一个[代码]behavior[代码]方便其他页面直接引入
2021-04-22 - input聚焦弹起键盘, 导致自定义导航栏上移?
使用自定义导航, 聚焦input后键盘会把导航推上去, 如何处理让导航栏不动(同默认的导航栏一样)? 片段:https://developers.weixin.qq.com/s/CcpkRBmV7jpL 除了监听键盘高度, 自己去调整布局 没有其他方法了吗? 看到这个几年前的提问(https://developers.weixin.qq.com/community/develop/doc/000e4afcc080c0aadfa7a7e0251400?highLine=input%2520%25E4%25B8%258A%25E7%25A7%25BB) 里面也没有看到解决方法 --- 导航栏设置了fixed 照样被顶上去: https://developers.weixin.qq.com/s/kRzVVBmK7Bpz --- 整理了自己的解决方法 https://developers.weixin.qq.com/community/develop/article/doc/00002864144918ff850c9b81a51813
2021-04-23 - 小程序分享到群里或者个人如果禁止长按转发
- 当前 Bug 的表现(可附上截图) - 预期表现 - 复现路径 - 提供一个最简复现 Demo 表现为小程序分享的时候带withShareTicket=true 的时候,分享到群里长按卡片没有转发,分享给个人长按卡片可以转发。由于分享给个人长按可以转发,这个人长按转发到群里,这时候群里的人都可以长按转发。如何屏蔽掉所有聊天内长按转发的功能
2019-05-15 - 只有三行代码的神奇云函数的功能之五:获取群id
这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦) 云函数:login index.js: const cloud = require('wx-server-sdk') cloud.init() exports.main = async (event) => { return { ...event, ...cloud.getWXContext() } } 神奇功能之五:获取群id: 将小程序分享到某群里,可获得该群的群id, page.js: onShareAppMessage: function () { wx.showShareMenu({ withShareTicket: true }) let path = '/pages/xxx/xxx' let title = 'title' let imageUrl = `http://xxx.com/100.jpg` return {title,imageUrl,path} }, app.js: onLaunch: function (options) { options.shareTicket && this.getOpenGId(options.shareTicket) }, getOpenGId: function (shareTicket) { wx.getShareInfo({ shareTicket, success: function (res) { wx.cloud.callFunction({ name: 'login', data: { weRunData: wx.cloud.CloudID(res.cloudID) }, success: res=> { console.log(res.result.weRunData.data.openGId); } }) } }) }, 需要说明一下的是:从群里点击分享卡片进入小程序,必须是重启的小程序,不能是已经打开的小程序,否则得不到shareTicket。 其他功能: 神奇功能之一:获取openid: https://developers.weixin.qq.com/community/develop/article/doc/00080c6e3746d8a940f9b43e55fc13 神奇功能之二:不用授权获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/000a0c6b580338e947f9db0c65b813 神奇功能之三:100%成功获取unionid: https://developers.weixin.qq.com/community/develop/article/doc/00066a967c4e384949f93fe1151413 神奇功能之四:获取电话号码: https://developers.weixin.qq.com/community/develop/article/doc/0006a8ec7ac860c94bf90a34f5d813 [图片]
2020-10-20 - Cannot read property 'model' of undefined怎么回事?
在小程序后台管理错误日志中看到报错信息,微信客户端版本是3.0.0,报Cannot read property 'model' of undefined这个错误,查看全局只有getSystemInfo使用到了model属性请问怎么排查是不是这个api导致的?怎么模拟微信客户端3.0.0复现这个问题呢
2020-10-16 - 低版本可以用 __wxWebviewI 兼容 getPageId 吗?
[图片] 目前观察到 getPageId 取到的是 __wxWebviewId__,组件和组件所在页面的 __wxWebviewId__ 是一样的 所以: __wxWebviewId__ 一定会有吗?组件和组件所在页面的 __wxWebviewId__ 一定是一样的吗?低版本可以用 __wxWebviewI 兼容 getPageId 吗?
2020-03-31 - Cannot read property '__wxWebviewId__
HONOR KNT-AL10 Android 6.0 目前不知如何复现,小程序正常使用情况下,有机率发生如下错误: [代码]appServiceSDKScriptError TypeError: Cannot read property '__wxWebviewId__' of undefined; at wx.createSelectorQuery [代码][代码] [代码][代码]X5JsCore:48:14266 Function.i[代码][代码] [代码][代码]X5JsCore:43:7757 Object.createSelectorQuery[代码][代码] [代码][代码]X5JsCore:3031:842 Function.e[代码][代码] [代码][代码]X5JsCore:42:13773 [代码][代码] [代码][代码]X5JsCore:47:7186[代码]疑似发生在内核 [代码]function[代码] [代码](e, t, n) {[代码][代码] [代码][代码]Object.defineProperty(t, [代码][代码]"__esModule"[代码][代码], {[代码][代码] [代码][代码]value: !0[代码][代码] [代码][代码]}), t.createSelectorQuery = void 0;[代码][代码] [代码][代码]var[代码] [代码]r = n(163),[代码][代码] [代码][代码]o = [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]return[代码] [代码]e && e.__esModule ? e : {[代码][代码] [代码][代码]default[代码][代码]: e[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}(r),[代码][代码] [代码][代码]i = [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]var[代码] [代码]t = [代码][代码]null[代码][代码];[代码][代码] [代码][代码]if[代码] [代码](e && e.page) t = e.page.__wxWebviewId__;[代码][代码] [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]var[代码] [代码]n = getCurrentPages();[代码][代码] [代码][代码]t = n[n.length - 1].__wxWebviewId__[代码][代码] [代码][代码]}[代码][代码] [代码][代码]return[代码] [代码]new[代码] [代码]o.[代码][代码]default[代码][代码](t)[代码][代码] [代码][代码]};[代码][代码] [代码][代码]t.createSelectorQuery = i[代码][代码] [代码][代码]}[代码]请问什麽情况下会发生,要如何避免这个错误,谢谢
2018-01-12 - 小程序input设置maxlength,输入时的bug
小程序input设置了maxlength,在输入汉字时,拼音被算在了input的数里,无法正常输入内容。
2017-09-13 - 直播页面 getCurrentPages 取到的是 null ?
[图片] 这个 null 就是这样设计的吗? 这样导致好多 currentPage.xxx 这种写法,都需要先判断一下 currentPage 是否存在
2020-05-27 - Fundebug微信小程序BUG监控服务支持Source Map
[图片] Source Map功能 微信小程序的Source Map功能目前只在 iOS 6.7.2 及以上版本支持。 微信小程序在打包时,会将所有 js 代码打包成一个文件,从而减少体积,加快访问速度。 然而,压缩代码的错误是很难Debug的,因为错误位置是这样的: 文件:app-service.js 行号:13782 列号:7974 这时,错误的位置信息(文件,行号和列号)失去了价值,因为开发者很难知道它所对应的源代码位置。 Fundebug的微信小程序BUG监控支持通过Source Map还原出错位置: 文件:utils/util.js 行号:573 列号:8 这样的话,开发者能够迅速定位出错的源代码。 在Fundebug控制台,只需要点击Source Map按钮,就可以切换压缩前后的堆栈: [图片] 如果希望使用Source Map功能,用户则需要: 从微信小程序管理后台下载Source Map文件 在Fundebug项目管理后台上传Source Map文件 下载Source Map文件 登陆微信公众平台 切换到左侧"开发"页面 点击链接"下载线上版本Source Map文件" 上传Source Map文件 将下载的Source Map文件解压缩,仅需上传解压缩的文件中的**__APP__/app-service.map.map**文件。 上传步骤 进入Fundebug『控制台』 选择『项目设置』 点击『Source Map』 选中需要上传的Source Map文件(支持上传多个Source Map文件) 点击『上传』 上传Source Map时可以配置应用版本: [图片] 下图为已经上传的不同版本的Source Map文件: [图片] 若希望区分不同版本微信小程序的Source Map文件,则需要在接入Fundebug插件时,配置对应的appversion属性,与上传Source Map时设置的版本保持一致: [代码]fundebug.init({ appVersion: "3.2.5" }); [代码] Fundebug微信小游戏BUG监控服务的Source Map功能也将尽快推出,敬请期待。 最后,感谢青团社的小伙伴的协助~ 参考 Fundebug文档: 微信小程序Source Map Source Map入门教程 关于Fundebug Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了20亿+错误事件,付费客户有阳光保险、核桃编程、荔枝FM、掌门1对1、微脉、青团社等众多品牌企业。欢迎大家免费试用! [图片] 版权声明 转载时请注明作者 Fundebug以及本文地址: https://blog.fundebug.com/2019/08/26/fundebug-wechat-miniprogram-support-sourcemap/
2019-08-26 - 微信开发者工具下载的 sourcemaps 怎么用。
什么是 Sourcemaps uglifyjs、bable 等工具会对 源代码 进行编译处理生成编译后的代码(下称目标代码),而 sourcemaps 就是保留了目标代码在源代码中的 位置信息 --------- 大神分割线 --------- 如何解读 Sourcemaps Sourcemaps 是一个 json [代码]{ "version": 3, "sources": ["a.js", "b.js"], // 源文件列表,这个表示是由 a.js 和 b.js 合并生成 "names": ["myFn", "test"], // 如果开启了变量名混淆,这里会保留变量名在源文件中名字信息 "sourcesContent: [], // 可选项,保存源码信息,顺序与 sources 字段对应,chrome 的 sources 面板中源码使用了这个字段的内容进行展示 "sourceRoot": "", // 源文件所在的目录信息 "file": "dist.js", // 可选,编译后的文件名 "mappings": "" // 这个是重点,是目标代码和源文件的位置的映射关系 } [代码] mappings 目标文件"行"的信息 mappings 是使用 ; 分隔的,每个部分对应目标代码的行 如: “;AAAA;AAAA,BBBB;;” 本例子目标文件有 4 行 第 0 行和第 3 行没有源文件对应信息,所以这两行是编译过程中加入的代码 目标文件的"列"信息 如: “AAAA,CAEA,CAEA;” ‘,’ 表示行内的位置信息分隔符 本例表示目标文件的这一行有三个有效的位置信息。 位置信息的第一位表示目标文件的列的 偏移 信息 本例中,表示列的信息是 ‘A’、‘C’、‘C’,对应的数字为 0、+1、+1,(vlq 编码,在线编解码工具) 注意,这个是偏移信息; 列数从 0 开始,依次累加偏移值可以算出当前的位置信息对应的真正的列 所以本例中表示的是目标文件的第 n 行中的第 0 列,第 1 列,第 2 列(没错是第 2 列) 源文件的信息 如:‘AAAA;ACAA;ADAA;’ 位置信息的第二位表示源文件的信息,本例子中是 ‘A’、‘C’、‘D’,对应数字是 0、+1、-1 如果 sourcemaps 中的 sources 字段只有一个文件的话,那么位置信息中第二位一直是 A(不需要偏移) 假设 sourcemaps 中 sources: [‘a.js’, ‘b.js’] 本例的意思是 AAAA: 目标文件第 0 行第 0 列 对应 第 0 个文件 a.js ACAA; 目标文件第 1 行第 0 列 对应 第 1 个文件 b.js ADAA; 目标文件第 2 行第 0 列 对了 第 0 个文件 a.js (偏移是 -1 又回到了 a.js) 源文件的行信息 位置信息的第三位表示源文件中的行的信息, 理解了位置偏移的概念,我们很容易理解 如:‘AACA,CACA;AACA;‘ 那么 AACA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 1 行 CACA: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 1+1 行 AACA:目标文件的第 1 行第 0 列 对应 第 0 个文件的第 1 行 (注意:’;’ 后的行列偏移信息归 0) 源文件中的列信息 位置信息的第四位表示源文件中的列的信息 如:'AAAA,CAAC;' 那么 AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列 CAAC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列 位置信息的第五位 第五位表示变量的偏移,对应 sourcemaps 中的 names 字段,表示目标文件中的变量名对应域源文件中的变量 如:’AAAA,CAACC;AAAAD;' sourcemaps 中 names 字段是 [‘a’, ‘b’] 那么 AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,没有变量的信息 CAACC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列,有变量信息,变量在源文件中是 ‘b’ (0+1=1) AAAAD: 目标文件的第 1 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,有变量信息,变量在源文件中是 ‘a’ (1-1=0) --------- 大神分割线 --------- 怎么使用 Sourcemaps Q: 线上小程序报错,我怎么通过 sourcemaps 还原到源代码中? A: 如报错 appservice.js 1:15000, 表示目标文件第一行 第 15000 列位置报错。根据上文介绍的,通过 mappings 字段算。 Q: 不会。 A: 如果你会写代码的话,参考下边 [代码]import fs = require('fs') import {SourceMapConsumer} from 'source-map' async function originalPositionFor(line, column) { const sourceMapFilePath = '如果你不真的替换的成 sourcemaps 在硬盘中的位置,那你还是放弃自己写代码吧。 ' const sourceMapConsumer = await new SourceMapConsumer(JSON.parse(fs.readFileSync(sourceMapFilePath, 'utf8'))) return sourceMapConsumer.originalPositionFor({ line, column, }) } originalPositionFor(出错的行,出错的列) [代码] Q: 不会写代码 A: 下载最新版的开发者工具,菜单-设置-拓展设置-调试器插件 [图片] [图片] Q: 为啥都是 null? A: 每个小程序版本都应该对应一个sourcemap文件。 运营中心那里下载的 sourcemap 是对应线上最新的小程序版本。但运营中心的报错集合了多个小程序版本。拿旧小程序版本的报错信息,和最新版本的 sourcemap,是匹配不出的。开发者工具和ci 上传的时候,会提示下载对应版本的 sourcemap 信息,可以自助保存。 [图片] Q: 怎么确定有没有版本对应上 A: 下载的 sourcemap 中有个 wx 字段,标明了该 sourcemap 文件对应小程序版本号。 [图片] [图片] 前提 1.确保发生错误的小程序版本和下载回来的 sourcemap 版本是一致的。 a. 下载 sourceMap 文件,可在 mp 后台或开发者工具上传成功弹窗下载 2.确保 map 文件和发生错误的 js 文件是对应的。sourcemap 的目录和文件说明 a. APP 是主包,FULL 是整包(仅在不支持分包的低版本微信中使用),其他目录是分包 b. 每个分包下都有对应的 app-service.js.map 文件。 c. 如果是使用了按需注入特性(app.json中配置了lazyCodeLoading),那么每个分包下还会有 appservice.app.js.map(对应分包下非页面的js),和所有页面的 xxx.js.map 以上事情都确保正确之后,还是出现行列号匹配不出来的情况。那就需要进一步排查。 线上运行的小程序 sourcemap 文件是怎么生成的? 处理流程:源码 [ a.js a.js.map b.js b.js.map ] -> 开发者工具(JS转 ES5,压缩)-> 微信后台(合并 js 文件)[ appservice.app.js appservice.app.js.map]。 注意:如果源码在交给工具之前是经过了 webpack 等打包工具的处理,那源码这里需要有 map 文件。否则不需要存在 map 文件。 可以看出,map 文件经过三个步骤的处理,每个步骤都有可能导致出错,因此开发者需要先排查,是否是前两个步骤出错导致的 map 文件失效的。 如何排查前两个步骤产生的 map 文件是否有问题。 1.排查 a.js.map 文件是否有问题。 a. 可以在 a.js 的代码中写一下 throw new Error(‘test sourcemap’)。 b. 使用了 webpack 的情况下,要构建为生产环境的版本。 c. 在开发者工具模拟器中运行对应的页面,看看控制台中的报错,错误行列号是否能正常映射到源文件。 2.排查 开发者工具(JS转 ES5,压缩)步骤是否有问题。 在排查完第一步的基础上,点击预览,用微信上扫码预览,并打开调试 vConsole 功能,检查 vConsole 中是否有报错信息,检查报错信息中的行列号是否能正常映射到源文件。 如何排查 微信后台(合并 js 文件)是否有问题。 a. 一定要先排查完前两个步骤再来排查这一步,一般情况下,这一步是不会出错的。 b. 如果有问题,也只会导致 map 文件中的行号信息出现偏移。比如 Error 信息中显示报错地址是 100: 200,行号是 100。那么你可能直接用 100: 200 在 map 文件中搜索不出信息,但是如果 用 150: 200 就可以搜索出来,说明行号偏移了 50。那其他报错也可以偏移 50 后再进行搜索就找到结果。 c. 怎么排查偏移了多少?可以结合 error.message 的内容,初步判断大概错误的内容是什么。把对应的 map 文件放到这个网站上 source-map-visualization 进行搜索,找出哪些相同列号的地方。再结合 error.message 的内容进行判断。 d. 如果排查到是这一步导致的问题,请在社区上联系我们,我们会在后续版本进行修复。 依旧排查不出原因? 先整理一下按照上述步骤排查的结论,再在社区上联系我们协助
2023-02-10 - source map 文件怎么使用
[代码]{[代码][代码] [代码][代码]"version"[代码][代码]: 3,[代码][代码] [代码][代码]"file"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"sourceroot"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"sources"[代码][代码]: [[代码][代码] [代码][代码]"pages/aboutBlibee/index.js"[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"names"[代码][代码]: [[代码][代码] [代码][代码]"_interopRequireDefault"[代码][代码],[代码][代码] [代码][代码]"e"[代码][代码],[代码][代码] [代码][代码]"__esModule"[代码][代码],[代码][代码] [代码][代码]"default"[代码][代码],[代码][代码] [代码][代码]"_we2"[代码][代码],[代码][代码] [代码][代码]"require"[代码][代码],[代码][代码] [代码][代码]"_config2"[代码][代码],[代码][代码] [代码][代码]"Page"[代码][代码],[代码][代码] [代码][代码]"data"[代码][代码],[代码][代码] [代码][代码]"versionArr"[代码][代码],[代码][代码] [代码][代码]"getVersion"[代码][代码],[代码][代码] [代码][代码]"push"[代码][代码],[代码][代码] [代码][代码]"title"[代码][代码],[代码][代码] [代码][代码]"ctx"[代码][代码],[代码][代码] [代码][代码]"wx_app_ver"[代码][代码],[代码][代码] [代码][代码]"t"[代码][代码],[代码][代码] [代码][代码]"wx"[代码][代码],[代码][代码] [代码][代码]"getSystemInfoSync"[代码][代码],[代码][代码] [代码][代码]"SDKVersion"[代码][代码],[代码][代码] [代码][代码]"this"[代码][代码],[代码][代码] [代码][代码]"setData"[代码][代码],[代码][代码] [代码][代码]"goToAgreementBlibee"[代码][代码],[代码][代码] [代码][代码]"navigateTo"[代码][代码],[代码][代码] [代码][代码]"url"[代码][代码],[代码][代码] [代码][代码]"onLoad"[代码][代码] [代码][代码]],[代码][代码] [代码][代码]"sourcesContent"[代码][代码]: [[代码][代码] [代码][代码]// 删除的代码[代码][代码] [代码][代码]][代码][代码]}[代码] 下载的sourcemap,格式化后的代码,不知道如何定位
2019-05-20 - Android 7.0 微信 6.5.4 Object.assign() 不存在
Android 7.0 微信 6.5.4 (来自 Google Play) 部分 Android 机器正常, iOS 10 正常.
2017-03-13 - 官方的es6转es5 在安卓上面报错
安卓上面缺少 Object.assign 方法..
2016-11-29 - 业务域名设置--校验文件检查失败自查指引
目前不少开发者在设置业务域名时,发现检查校验文件失败,可先按照如下步骤进行自查: 如果想保存的业务为https://test.com/,下载下来的校验文件为AbC.txt,则需要确保https://test.com/AbC.txt能够访问。 校验文件内容错误。校验文件内容一般是非HTML数据,如果下载下来的校验文件内容为HTML数据,一般为登录态过期。请重新登录小程序下载校验文件。 使用4G网络尝试访问链接,确认自身服务器没有拦截请求(常见于设置了白名单或者防火墙的服务器,需开发者自行确认下) https证书过期。请确保https证书处于有效期内。 使用curl 测试链接,确保curl能够正常访问链接,且curl出来的内容为校验文件内容。 使用time curl https://test.com/abc.txt查看链接时间,建议耗时在1s之内。 请确保url中的文件名与下载下来的文件名大小写一致。如下载的文件是AbC.txt,确保url是https://test.com/AbC.txt,不能是https://test.com/abc.txt 部分用户的服务器配置较陈旧,安全性差(如配置 768位 的 DH),为了保证通信安全,微信后台不支持,请更新服务器配置。 (1)通过https://cloud.tencent.com/product/tools#userDefined12,检测网址是否支持TLS1.2。 (2)可通过工具 https://www.ssllabs.com/ssltest/analyze.html 检查自己的服务器,对该工具标红的各项漏洞逐项修补,建议更新配置直到该工具打分为 C及以上 。 9. 如上述检查都没有问题,请重新下载校验文件重试,确保上传到服务器的文件内容与新下载的文件内容一致。
2018-06-21 - 小程序cover-view不支持字体吗?
[图片]开发工具的模拟器是支持字体的,但是手机扫码预览额度时候字体没有变化, 发现我这两个按钮是用cover-view写的,是不是cover-view不支持外部字体呢? (其他页面没用cover-view,用的普通view标签,是支持该字体的)
2019-10-30 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 论如何进行小程序自定义组件的单元测试
前言 自从小程序自定义组件和 npm 功能面世之后,组件化和开源思想逐步开始萌芽了。我们可以将一些通用的部件,如自定义导航栏之类的封装到一个自定义组件中,然后借由 npm 平台开源出去给其他开发者使用,这样可以省去很多劳动。相信各位开发老爷们应该或多或少都有过使用开源包的经历,但是在使用前,这个开源包得能赢取我们的信任,一个很重要的指标就是单元测试通过率和覆盖率。 但是因为小程序独特的运行环境和不完全开源的基础款,使得对小程序自定义组件的单元测试稍微有点困难。目前市面上无论是 vue 还是 react,这些组件化框架都有一套完善的单元测试解决方案,但是对于小程序自定义组件来说却寥寥无几,因此这个工具集—— miniprogram-simulate 便应运而生了。 痛点 闲话不多说,我们先看下小程序的运行机制: [图片] 可以看出,小程序自定义组件是渲染与逻辑脱离,想在逻辑层拿到渲染的结果进而进行对比测试是很难办到的。而且目前小程序的环境并不开放,想要完整构造模拟出小程序的运行环境也不太科学。另外我们这边只是需要对小程序的自定义组件做单元测试,对于小程序中很多非自定义组件相关的功能可以不考虑,而且在性能上也不那么苛求,所以一个思路是调整底层运行机制,将双线程合并为一个线程。 实现 只是有思路还不够,在实现过程中还是有一些坎的。比如要如何比较好的模拟出小程序自定义组件的各种特性和功能呢?自己实现也不是不行,问题在于维护的成本,如果小程序自定义组件实现了一个功能,测试工具还得更新一下。另外如果在实现上略有差池的话,可能小程序端的一个小调整对于测试工具都可能是伤筋动骨式的改造。所以这里直接将小程序自定义组件的最核心模块—— exparser 从基础库中抽离出来。 exparser 是自定义组件系统的内核,是一个完整独立的模块,不依赖于基础库中其他模块。它完全脱离于小程序的 api 和运行机制体系,所以无论是单线程还是双线程机制都可以使用。exparser 提供的是自定义组件系统最底层的接口,测试工具将其进行二次封装成自定义组件测试环境。如果基础库有关于自定义组件的更新,如果是底层改造,则直接更新 exparser 模块即可;如果只是外层改造,那基本上是暴露接口层面的调整,也不必作太多大范围的调整。 PS:目前虽然 exparser 已经发布到 npm,但是仍然只是混淆压缩后到代码,属于半开源状态,不建议开发者直接使用。 使用 miniprogram-simulate 本是自定义组件脚手架 miniprogram-custom-component 中的一部分,现单独抽离出来,方便开发者们作更多的使用选择(脚手架中默认使用 jest 来搭配使用,直接使用此工具集则可以搭配其他想要使用的测试框架,比如 mocha 等)。 下述只简单介绍下用法,首先安装此工具集: [代码]npm install --save-dev miniprogram-simulate [代码] 然后此工具集必须搭配其他测试框架和 jsdom 来使用,比如 jest。因为 jest 内置有 jsdom,所以也就不需要额外安装 jsdom 了,以下面一个自定义组件作为例子: [代码]<!-- 自定义组件:comp.wxml --> <view class="index">{{prop}}</view> [代码] [代码]/* 自定义组件:comp.wxss */ .index { color: green; } [代码] [代码]// 自定义组件 comp.js Component({ properties: { prop: { type: String, value: 'index.properties' }, }, }) [代码] 这是一个极其简单的自定义组件,之后我们便可开始在 comp.test.js 里编写测试用例。 起步 加载和渲染自定义组是最基础的功能: [代码]// 自定义组件 comp 的测试用例:comp.test.js const path = require('path') const simulate = require('miniprogram-simulate') test('comp', () => { const id = simulate.load(path.join(__dirname, './comp')) // 此处必须传入绝对路径 const comp = simulate.render(id) // 渲染成自定义组件树实例 const parent = document.createElement('parent-wrapper') // 创建父亲节点 comp.attach(parent) // attach 到父亲节点上,此时会触发自定义组件的 attached 钩子 expect(comp.querySelector('.index').dom.innerHTML).toBe('index.properties') // 测试渲染结果 // 执行其他的一些测试逻辑 comp.detach() // 将组件从父亲节点中移除,此时会触发自定义组件的 detached 生命周期 }) [代码] 获取数据 可以获取自定义组件的数据: [代码]test('comp', () => { // 前略 // 判断组件数据 expect(comp.data).toEqual({ a: 111, }) }) [代码] 更新数据 可以更新自定义组件的数据: [代码]test('comp', () => { // 前略 // 更新组件数据 comp.setData({ a: 123, }) }) [代码] 获取子组件 可以获取自定义组件的子组件: [代码]test('comp', () => { // 前略 const childComp = comp.querySelector('#child-id') expect(childComp.dom.innerHTML).toBe('<div>child</div>') }) [代码] 触发事件 可以模拟触发自定义组件的事件: [代码]test('comp', () => { // 前略 comp.dispatchEvent('touchstart') // 触发组件的 touchstart 事件 childComp.dispatchEvent('tap') // 触发子组件的 tap 事件 }) [代码] 至此,应该能大概了解到这个工具集的用途。这些只是简单的使用介绍,本文只是个引子,更多详细的用法请移步到 github 仓库上查阅。 尾声 要想判断一个自定义组件的质量如何,其中最简单的方法就是看单元测试的表现,想要别人使用你的自定义组件,质量把关很重要,目前 miniprogram-simulate 已经实现了最基本的功能,其他功能也在尽力施工中,有什么好的建议或者在使用上遇到什么问题也可以提 issue。
2019-02-25 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - 内部团队单元测试探索、落地及后续思考
说明 本文记录了内部团队为小程序探索、搭建基于jest框架单元测试的整个过程,关于单元测试的编写风格借鉴了很多优秀的文章中的观点【见附录】,测试框架的建立也从兄弟团队借鉴了很多的经验,所以也特别感谢兄弟团队的同学们提供的大量帮助。 导读 本文共计5000字,预计通篇阅读时长 10 min。 “探索“、和”落地”两个部分,主要讲述了我们探索小程序单元测试的思考过程。包含了我们是如何定义”什么是一个好的单元测试“,以及针对小程序的每个组成部分,哪些需要单元测试,哪些不需要,我们最后决定采用的测试策略是什么。 “实践”和“原理”两个部分,主要围绕“如何编写一个符合质量要求的单元测试”,以及“如何在微信小程序中引入基于jest框架的单元测试”这两个基础问题展开。 希望通过下文的阅读可以给你带来帮助。 探索 探索之初,只有一个大概的认知就是:我们需要一个比UI自动化测试更加贴近底层逻辑的测试工具,它是什么,我们怎么找到它,找到后要怎么落地,还是不太明确。 起初学习了小程序官方提供的单元测试库:miniprogram-simulate。尝试实践了一下,感觉整个库提供的功能更偏向组件的UI渲染测试,测试的结果是以渲染DOM为基础,测试DOM内容是否正确,DOM绑定的事件点击后一些改变是否正常,等等。总之这种测试感觉很无力,如果需要这么测,现在的UI自动化足够胜任。而且这个库作为官方推荐库,本身的API不是十分丰富,使用起来较为复杂,社区活跃度也不是十分的高,如果直接引入到生产环境,还是有很大风险的,所以并没有考虑使用这个。 不过,在这个尝试的过程中我们逐渐明确了,我们需要的是对于底层逻辑的单元测试,也就是说对于单个函数逻辑功能的测试,UI渲染方面的测试需求不是很大。特别是在小程序单元测试这方面的文档是空白的,所以我们决定将问题更加抽象一些,探索的方向拓宽为前端的单元测试该如何做,这样就可以参考各种框架中优秀单元测试的实践指南了。这期间看到了很多关于前端单元测试落地的文章,关于实践的思考成果也大多从这个阶段产出的。 什么是好的单元测试?“什么才是好的单元测试”,以及“如何写出这样的单元测试?”这些问题是我们要解决的核心问题。通过查阅“React单元测试”的相关文章,我们找到了一些优秀的、要求明确的案例。下面,我们来看一个最简单的JavaScript单元测试长什么样 [图片] 基本规则 通过观察显然可得,这个单元测试是一个三段式的,而且逻辑也比较严谨,描述清晰,可读性强。所以编写测试的思想一定是遵循这个given-when-then结构的,可以让你写出比较清晰的测试结构,既易于阅读,也易于维护。 例如: 首先准备一些测试用例数据【give】, 同时引入你想要测试的函数,并将你准备好的参数传入【when】 最后对函数执行的结果进行断言【then】 为了明确,你甚至可以先将结果写出来, const expectResult = 1000 expect(result).toEqual(expectResult); 这种单元测试结构是基础,几乎所有函数的测试都可以通过这样一种范式来表达。有了这种范式,我们就可以制定一些标准,然后通过对细节做出描述,这样结合起来就得出了“什么才是好的单元测试”这个问题的答案。那么,除了这个基本的“骨架”,每一句语言的描述又有什么要求呢? 约束原则 如果说三段式的结构是一个人的骨架,那么这些约束原则,则是一个人的血肉,这二者结合,才是一副有血有肉的躯体。这些原则对于任何语言、任何层级的测试都适用。总结于此,既可以事前参考,提升自己的编写能力;又可以此为镜,时时检验你的单元测试套件是否高效: 只关注输入输出,不关注内部实现【重要】 只要测试输入没有变,输出就不应该变。这个特性,是测试支撑重构的基础 如果你开始关注被测函数的内部逻辑了,那么显然就这个测试是不可靠的,不能作为重构支持的,因为这种情况下你的单元测试好像已经变成了业务函数的一部分,不再能提供可靠的外部支持。 特别有一点需要注意:单元测试关注于内部函数的执行顺序也是一种关注内部实现的表现。 只测一条分支【重要】 通常来说,一条分支就是一个业务场景,是你做任务分解过程的一个细粒度的task。为什么测试只测一条分支呢?很显然,如此你才能给它一个好的描述,这个测试才能保护这个特定的业务场景,挂了的时候能给你细致到输入输出级别的业务反馈。 在我们的实际操作中,一条业务分支通常是由一个describe来划分的,在第一个参数的位置给出对于被测任务的描述。然后每一个测试点只测一个函数,通过尽可能丰富的参数来增强测试的健壮性。 常见的反模式是,单元测试本身就做了太多的事情,不符合SRP原则。单一职责原则(Single Responsibility Principle))。 表达力极强【次重要】 表达力强的测试,能在失败的时候给你非常迅速的反馈。它讲的是两方面: ① 看到测试时,你就知道它测的业务点是啥 ② 测试挂掉时,能清楚地知道失败的业务场景、期望数据与实际输出的差异 总结起来,这些表达力主要体现在以下的方面: 测试描述。遵循上一条原则(一个单元测试只测一个分支)的情况下,描述通常能写出一个相当详细的业务场景。这为测试的读者提供了极佳的业务上下文 测试数据准备。无关的测试数据(比如对象中的很多无关字段)不应该写出来,应只准备能体现测试业务的最小数据 输出报告。选用断言工具时,应注意除了要提供测试结果,还要能准确提供“期望值”与“实际值”的差异 不包含逻辑【次重要】 跟写声明式的代码一样的道理,测试需要都是简单的声明:准备数据、调用函数、断言,让人一眼就明白这个测试在测什么。如果含有逻辑,你读的时候就要多花时间理解;一旦测试挂掉,你咋知道是实现挂了还是测试本身就挂了呢? 运行速度快【探索阶段,暂不考虑】 单元测试只有在毫秒级别内完成,开发者才会愿意频繁地运行它,将其作为快速反馈的手段也才能成立。那么为了使单元测试更快,我们需要: 尽可能地避免依赖。除了恰当设计好对象,关于避免依赖我已知有两种不同的看法: 使用mock适当隔离掉三方的依赖(如数据库、网络、文件等) 避免mock,换用更快速的数据库、启动轻量级服务器、重点测试文件内容等来迂回 将依赖、集成等耗时、依赖三方返回的地方放到更高层级的测试中,有策略性地去做。 由于没有特别关心这个方面,所以我没有了解这个部分,以后有机会会继续学习。 落地明确了什么是好的单元测试,编写的过程应该遵循哪些规范,但是具体到小程序,我们该如何落地呢?针对不同的层次,我们又有哪些策略呢? 策略架构中的不同元素有不同的特点,因此即便是单元测试,我们也有针对性的测试策略: 注:此处的“覆盖”问题指的是被测对象层面,即:每个函数都有对应的测试函数相佐,而非单个测试函数内部用例的覆盖情况。 [图片] 说明:此处的页面类型划分并不是建议单元测试的组织方式,只是为了方便表述。从TDD的角度出发,测试一定是会持续维护的,所以单元测试 的组织方式,我感觉应该是需要和项目结构差不多的。 actions 测试 这一层获益于架构的简单性,小程序的action并不是成熟的React-redux,所以action的产生都是手动写出来的,没有工厂函数生成的,所以可以不用测试。 reducer 测试 reducer 大概有两种: 一种比较简单,仅一一保存对应的数据切片; 一种复杂一些,里面具有一些计算逻辑。 不管我们要写的是哪一种,结合我们的落地策略,我们关心的只有被测函数执行前的一个state,以及执行后的状态。所以give - when - then 在这个测试中的对应关系就很明确了。 describe('reducer相关功能', () => { test('成功发布作品', () => { // give 给出了对于结果的断言,以及想要执行的action。 const resultState = { title: "测试作品标题", ... videoBgmList: [{}, {}], }; const action = { type: 'PUBLISH_WORKS_SUCCESS' }; const pervState = {} // when 调用该reducer,对传入的prevState进行更改,根据测试需求的不同可以对prevState进行定制化处理 const newState = videoTool(pervState, action); // then 对于执行的结果进行断言,断言的方式是多种多样的,此处给出的是按需比较,你也可以用整个对象的值进行比较 expect(newState.title).toEqual(resultState.title); .... expect(newState.videoBgmList.length).toBeGreaterThan(0); }); }) component 测试 component测试和page测试在测试方面的感觉是没有什么区别的,都是经过预先设置好的mock文件,mock好一些底层函数,然后针对性的对一些实现功能的核心函数进行调用测试。这些函数可能改变了页面data,也可能调用了其他的函数,这些我们都可行进行断言。所以整体来说,难度并不是很大。 下面的实践中讲解的用例就是对一个发布component的核心函数"发布"的测试。此处就不再赘述用例了。 实践 经过上面文章的洗礼,感觉自己信心满满,似乎已经习得了单元测试的精髓,那我们撸起袖子,亲自上手试试看吧。 目标函数 handlePostBtnTap() { this.handlePrintStatistic('entrance'); const { sendDesc } = this.properties; const buttonList = [ { name: '发视频', src: `./post_video.png`, }, { name: '发图片', src: `./post_photos.png`, }, ]; this.setData({ actionSheet: { hidden: false, onCancel: 'handleHideAS', buttons: buttonList, }, }); }, 仿照规则,编写测试首先,依照目标,我想要测试推荐页的发布函数的代码(代码如上)。 give:引入发视频函数。when:调用发视频函数then:该函数被调用后,当前页的data将会发生改变,某些关联函数将会被调用。 能走完这三个过程,一个发视频函数的单元测试就编写完了。看似简单,但是实际操作的时候,第一个坎儿就出现了 give怎么引入我想要调用的函数到测试文件呢? 导入待测页面到当前的测试文件,通过global对象,取到当前component(或者app、page都可以),此时 global.wxComponentInstance 对象所代表的就是待测页面的页面对象。这样,我们就可以引入待测函数了。 为了方便,我们将 global.wxComponentInstance 赋值给components 引入待测函数以后,我们就该走下一步了,对函数进行测试: when如何进行函数的调用呢? 这个同样也是有办法的,例如,我们要测试的函数是 handlePostBtnTap ,那么我们就可以在当前页的 components 对象下找到这个函数,并将当前对象作为this传入,例如: components.methods.handlePostBtnTap.call(components) 这样,我们就完成了目标函数的调用(当然你也可以选择直接调用,只是调用后页内各个函数的层级结构会有所不同,读者可以自行尝试,此处不进一步展开)。 then至此,一个单元测试的三步,我们已经完成了两个步骤:give和when,then? 根据 handlePostBtnTap 逻辑,我们可以很明白的整理出: this.handlePrintStatistic 将会被调用。当前页面的data中的actionSheet的值将会被改变。所以,我们根据上面的整理结果,对函数运行后的结果进行断言: expect(components.handlePrintStatistic).toBeCalled(); expect(components.data.actionSheet.buttons).toHaveLength(2); 两个日志函数将会被调用,当前页的data的actionSheet.buttons的长度将会变成个。 整理一下,我们完整的写一下当前的这个测试函数: test('发布按钮点击', () => { components.methods.handlePostBtnTap.call(components); expect(components.handlePrintStatistic).toBeCalled(); expect(components.data.actionSheet.buttons).toHaveLength(2); }); 完善好的,该做的工作我们做完了,应该准备运行了。 npm run test 回车!开始了!运行起来了!运行到了目标函数了!欸 ?报错了。。。 没错呀,写的没问题呀,为啥就报错呢? 我们先分析一下错误: TypeError: this.handlePrintStatistic is not a function 显然,这个函数是被调用的其中一个日志函数。当我们调用被测试函数的时候,被测函数就会调用这些相关的函数,但是我们只引入了被测函数一个函数,所以这些相关函数当然要报错了。根据测试原则 只关注输入输出,不关注内部实现 我们将这些函数Mock掉,我们不关心他们具体做了什么,我们只关心他们是否被调用了(被调用也是被测函数的一种输出)。 所以,我们把这些无关的函数都Mock掉,然后,我们的测试函数就变成了这样: test('发布按钮点击', () => { components.handlePrintStatistic = jest.fn(); components.methods.handlePostBtnTap.call(components); expect(components.handlePrintStatistic).toBeCalled(); expect(components.data.actionSheet.buttons).toHaveLength(2); }); 然后我们再运行一下我们的测试函数, npm run test 回车!开始了!运行起来了!运行到了目标函数了!怎么又报错了。。。 老办法,继续分析错误: TypeError: Cannot destructure property `sendDesc` of 'undefined' or 'null'. const { sendDesc } = this.properties; | ^ 56 | const buttonList = [ 57 | { 58 | name: '发视频', 有了上次的经验,我们已经知道了,调用这个函数的时候,没有this.properties,所以报错了,同样,这种我们并不关心的内部逻辑,直接Mock就可以了。 继续完善我们的单元测试代码如下: test('发布按钮点击', () => { components.methods.properties = { sendDesc: '' }; components.handlePrintStatistic = jest.fn(); components.methods.handlePostBtnTap.call(components); expect(components.handlePrintStatistic).toBeCalled(); expect(components.data.actionSheet.buttons).toHaveLength(2); }); 然后我们再运行一下我们的测试函数, npm run test 回车!开始了!运行起来了!运行到了目标函数了!成功了! [图片] 不出意外的话,你会看到如图所示的通过提示,至此,一个符合我们当前要求的单元测试就诞生啦!!! 当然,这只是最简单的函数的单元测试,但是如果想要体现我们的思想和基本要求,这就足够了。 以后可能会有更复杂的单元测试需要编写,但是它的基本原则是不变的,围绕基本原则编写,就可以写出优秀的单元测试。 原理环境搭建如果是一个全新的小程序,如何搭建一个单测框架? 当前项目采用的是手动配置的方式,在项目的package.json中进行必要的配置,只需三步,即可搭建好运行环境。 1、在工程的根目录下创建一个babel.config.js文件用于配置与你当前Node版本兼容的Babel: module.exports = { presets: [ [ '@babel/preset-env', { targets: { node: 'current', }, }, ], ], plugins: ['transform-es2015-modules-commonjs'], }; 2、需要在测试环境引入一些依赖包(只在测试环境下进行单元测试), "devDependencies": { "@babel/core": "^7.5.5", "@babel/preset-env": "^7.5.5", "babel-jest": "^24.9.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", "jest": "^24.9.0", "miniprogram-simulate": "^1.0.8" } 3、添加scripts命令和jest基础配置 // scripts 中加入test命令,方便运行 "scripts": { "test": "jest", }, // 增加jest configuring, // 具体可见:https://jestjs.io/docs/zh-Hans/configuration "transform": { "^.+\\.js$": "babel-jest" }, "setupFiles": [ "./test/wx.js" // 此处的文件路径是每个测试用例运行前都需要先执行的mock函数的存放文件 ] } 此处关于setUpFiles配置项再说一下: 运行一些代码来配置或设置测试环境的模块的路径列表。每个setupFile将针对每个测试文件运行一次。由于每个测试都在其自己的环境中运行,所以这些脚本将在测试环境中执行,然后立即执行测试代码本身。 在实际使用中,我们大部分项目测试前都需要Mock掉一些wx提供的函数,并且给getApp()等全局变量中赋初值,这些操作都可以在setUpFiles里面的文件中去做,下面我给出一些示例: // 当前文件的路径是 /test/wx.js // 每个测试文件都需要Mock的wx提供的内置函数,没有用到的可以删除,没有出现的可以补充 global.wx = { chooseVideo: jest.fn(), chooseImage: jest.fn(), showLoading: jest.fn(), hideLoading: jest.fn(), request: jest.fn(), getStorageSync: jest.fn(), showShareMenu: jest.fn(), getSystemInfo: jest.fn(), setStorageSync: jest.fn(), uploadFile: jest.fn(), createSelectorQuery: jest.fn(), }; // 然后是对页面中setData函数的Mock export const setData = jest.fn(function fn(newData, cb) { this.data = { ...this.data, ...newData, }; if (cb) cb(); }); // 注册组件的Component的Mock,同理可以创建App和Page的。 global.Component = ({ data, properties, ...rest }) => { const component = { properties, data, setData, created: noop, attached: noop, ready: noop, moved: noop, detached: noop, error: noop, methods: {}, ...rest, }; global.wxComponentInstance = component; return component; } 运行技巧 如果你使用的是vscode,那么在【资源管理器】这个tab下,可以看到NPM脚本的选项,点击我们配置好的test,通过GUI的方式运行脚本。 当然你也可以通过命令行运行来运行测试脚本: npm run test // 运行所有的测试用例 npm run test targetTastFile //运行单个目标测试文件 写在最后关于单测能力 这次落地经历,让我们更加深刻的认识到了单元测试的必要性。但是在完成单测落地以后,我从头到尾梳理了一遍以后,还是觉得单测目前在我们项目中的地位很鸡肋,感觉没有非常大的收益。直到同事和我重新说了一下她的想法,我才猛然发现,后补的单测确实很鸡肋,感觉是在徒增开发工作量。但是从TDD(UTDD)视角出发,一切都会变的那么自然而且有力,优先编写单元测试,修改代码也先从单元测试的更改开始,感觉上是一种思维的转变。有兴趣可以看【附录3】的文章(深度解读 - TDD(测试驱动开发)),我觉得是非常好的解答。 就目前了解到的知识来看,我自己还是觉得UTTD可能才会是提升我们编写单元测试意愿的驱动因子。如果总是先写业务再补测试,这种鸡肋的感觉可能会挥之不去。但是鉴于TDD目前还是我们知识的盲区,所以接下来的探索仍然是任重而道远。 文章正确性 第一次整理这种偏记录性质文章,所以文章内容可能存在不得当、甚至是错误的地方,欢迎批评斧正。如果有任何发现或者想法,欢迎随时与我们交流探讨。 内容方面,文章只是基础入门的文章,所以没有详细的对于单元测试划分,及单元测试的进阶能力等方面进行描述及展望,这些仍然需要我们以后一起探索,一起进步。 附录1.React单元测试策略及落地 作者:ThoughtWorks 链接:https://www.jianshu.com/p/97d8e5e33431 2.jest 文档 作者: FaceBook 链接:https://jestjs.io/docs/zh-Hans/getting-started 3.深度解读 - TDD(测试驱动开发) 作者:SeabornLee 链接:https://www.jianshu.com/p/62f16cd4fef3
2020-03-20 - 【优化】利用函数防抖和函数节流提高小程序性能
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/article/doc/000040a5dc4518005d2842fdf51c13 [代码]今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 [代码] 首先先来理解一下两者的概念和区别: [代码] 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ [代码] 适用场景 函数防抖 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 函数节流 滚动加载,加载更多或滚到底部监听 搜索联想功能 实现原理 [代码] 函数防抖 [代码] [代码]const _.debounce = (func, wait) => { let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); }; }; [代码] [代码] 函数节流 [代码] [代码]const throttle = (func, wait) => { let last = 0; return () => { const current_time = +new Date(); if (current_time - last > wait) { func.apply(this, arguments); last = +new Date(); } }; }; [代码] [代码] 上面两个方法都是比较常见的,算是简化版的函数 [代码] lodash中的 Debounce 、Throttle [代码] lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51[代码]
2019-02-22 - 小程序单元测试
小程序单元测试小程序的测试和web应用测试区别不大,可以利用jest进行测试,但是由于jest只提供了nodejs和浏览器执行环境,因此小程序的api我们需要mock,下面讲解小程序测试的一些mock技巧。 mock小程序API我们测试小程序时,经常会调用微信api,例如wx.showLoading方法,但是因为我们的执行环境未定义该方法,会出现调用错误。 我们可以通过jest提供的global设置全局变量,可以在测试文件中单独编写,或者在package.json的jest块设置setupFiles属性,让jest自动加载。 [代码] "jest": { "setupFiles": ["./__tests__/wx.js"] },复制代码[代码]./tests/wx.js文件内容如下,表示将小程序的api方法定义为mock方法。 [代码]global.wx = { showLoading: jest.fn(), hideLoading: jest.fn(), showModal: jest.fn(), request: jest.fn(), getStorageSync: jest.fn(), showShareMenu: jest.fn(), };复制代码[代码]测试小程序页面[代码]// 空白的小程序页面代码 Page({ onLoad () { // your code } })复制代码[代码]一个空白的小程序页面,代码会被Page方法包裹,同时Page初始化后,会执行onLoad、onReady等生命周期方法,而且当前对象还能调用setData方法对页面data数据进行修改。 我们需要mock Page方法的实现,代码如下。 [代码]export const noop = () => {};export const isFn = fn => typeof fn === 'function';let wId = 0; global.Page = ({ data, ...rest }) => { const page = { data, setData: jest.fn(function (newData, cb) { this.data = { ...this.data, ...newData, }; cb && cb(); }), onLoad: noop, onReady: noop, onUnLoad: noop, __wxWebviewId__: wId++, ...rest, }; global.wxPageInstance = page; return page; };复制代码[代码]举个例子假设我们的小程序页面是一个电影列表展示,业务代码如下。 [代码]const filmServer = require('../../server/film.js'); Page({ data: { comingFilms: [], }, onLoad() { this.getComingFilm(); }, // 获取即将上映电影列表 getComingFilm() { return filmServer.getComingSoon(1, 5).then((data) => { data.films.forEach((film) => { const displayDate = `${new Date(film.premiereAt).getMonth() + 1}月${new Date(film.premiereAt).getDate()}日`; film.displayDate = displayDate; }); this.setData({ comingFilms: data.films }); }); }, });复制代码[代码]我们的编写两个测试用例保证代码的正确运行。1、保证onLoad时执行getComingFilm方法。2、保证getComingFilm后日期数据进行格式化。[代码]import '../../pages/film'; // 加载需要测试的页面 // 获取当前初始化的page对象,后续可用来调用setData等方法,类似小程序页面里的this。 const page = global.wxPageInstance; // mock网络请求 jest.mock('../../server/film.js'); describe('电影首页', () => { describe('onLoad', () => { beforeAll(() => { // spyOn后可使方法具有mock属性,同时不影响方法调用。 jest.spyOn(page, 'getComingFilm'); // 执行页面onLoad生命周期。 page.onLoad(); }); it('should getComingFilm', () => { // 断言onLoad后,是否执行了getComingFilm方法。因为我们前面已经将getComingFilm进行spyOn了,所以可以执行toBeCalled判断,否则会出错。 expect(page.getComingFilm).toBeCalled(); }); }); describe('getComingFilm', () => { it('should format premiereAt as MM月DD日 ', () => page.getComingFilm().then(() => { // 断言获取数据后,原始数据增加displayDate属性,格式化为MM月DD日 expect(page.data.comingFilms[0].displayDate).toEqual('9月12日'); })); }); });复制代码[代码]🌟🌟由于测试代码比较长,上面只截取了部分,完整代码可以访问github获取
2018-10-08 - 小程序自动化测试工具miniprogram-automator如何断言wx.showToast组件?
miniprogram-automator小程序自动化测试工具,如何去断言wx.showToast组件的提示框?失败或者成功都有提示框,只是传入给showToast的title,image,duration不同。可以通过获取传入的参数去断言吗?
2020-04-21 - 小游戏开发安卓端获取画布的 imageData 无效
let ctx = canvas.getContext('2d'); ctx.drawImage(mainImg, 0, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); 使用canvas的getImageData的api,设备是华为的mate20和华为荣耀play分别是7.0.0和6.7.3版本返回的数据都是0的数组,都是安卓9.0版本,但是用苹果手机调试却一切正常。。。
2019-01-02 - wx.createSelectorQuery().select有时候返回为null,求解答
通过wx.createSelectorQuery选择的节点时动态的,所以放到了setData的回调里,那为什么有时候会报错呢,求解答 this.setData({ testList }, function () { wx.createSelectorQuery().select('.test-item').boundingClientRect(function (rec) { console.log(rec.width);// 此处偶尔会报错,rec为null }).exec(); });
2019-09-23 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - html-canvas 生成小程序分享图
简介 基于 HTML 和 CSS 实现 Canvas 绘图。 项目地址 代码片段:https://developers.weixin.qq.com/s/9zFHKdmh7De2 原理 构建虚拟DOM 树,依据 CSS 规范计算样式,使用 CSS 盒模型对 DOM 进行布局,计算出所有元素的位置。最后将 DOM 树通过 Canvas Api 进行绘制。 小程序开发工具内运行 demo [代码]git clone https://github.com/alexayan/html-canvas.git npm i npm run watch [代码] 已支持的 CSS 属性 margin,margin-left,margin-top,margin-right,margin-bottom,padding,padding-left,padding-top,padding-right,padding-bottom,width,height,border,border-left,border-top,border-right,border-bottom,border-width,border-style,border-color,border-left-style,border-left-color,border-left-width,border-top-style,border-top-color,border-top-width,border-right-style,border-right-color,border-right-width,border-bottom-style,border-bottom-color,border-bottom-width,color,display,background-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-left-radius,border-bottom-right-radius,box-sizing,font,font-style,font-variant,font-weight,font-stretch,font-size,line-height,font-family,text-align,position,overflow,overflow-x,overflow-y,top,left,right,bottom,z-index demo canvas-draw.html [图片]
2020-01-08 - 微信后台报错TypeError: Type error,这是为什么?
问好,进入微信小程序后台 -> 开发 -> 运维中心 -> 错误查询,发现有一条错报很多次,请问这是为什么?而且并没有触发微信的告警群发消息。 [图片]
2020-05-07 - APP-SERVICE-SDK:setStorageSync:fail write DB data?
突然iOS iPhonexs 13.3.1 微信版本7.0.12 报这个异常 开发者工具一切正常,体验版本和开发者版本都正常,线上版本推送1min就报错,请问这怎么处理?
2020-04-09 - setStorageSync:fail write DB data
只有我的手机有问题,突然发生这种现象。删除小程序,清缓存,重启微信,重启手机都试过了,就差重装微信没试过。 其他同事都是可以正常使用的。正式版,体验版,开发版现象一致。 功能是将接口带回的token存储,后续其他接口调用都会用到。 APPID:wxf9c391aa2a80c106 [图片]
2019-08-01 - 微信小程序开发经验+
😺总结了一些小程序开发经验,包括一些工具、组件、技巧等,具体如下图所示👇 [图片]
2019-05-21 - 小程序奇技淫巧之 -- 日志能力
日志与反馈 前端开发在进行某个问题定位的时候,日志是很重要的。因为机器兼容性问题、环境问题等,我们常常无法复现用户的一些bug。而微信官方也提供了较完整的日志能力,我们一起来看一下。 用户反馈 小程序官方提供了用户反馈携带日志的能力,大概流程是: 开发中日志打印,使用日志管理器实例 LogManager。 用户在使用过程中,可以在小程序的 profile 页面(【右上角胶囊】-【关于xxxx】),点击【投诉与反馈】-【功能异常】(旧版本还需要勾选上传日志),则可以上传日志。 在小程序管理后台,【管理】-【反馈管理】,就可以查看上传的日志(还包括了很详细的用户和机型版本等信息)。 这个入口可能对于用户来说过于深入(是的,官方也发现这个问题了,所以后面有了实时日志),我们小程序也可以通过[代码]button[代码]组件,设置[代码]openType[代码]为[代码]feedback[代码],然后用户点击按钮就可以直接拉起意见反馈页面了。利用这个能力,我们可以监听用户截屏的操作,然后弹出浮层引导用户主动进行反馈。 [代码]<view class="dialog" wx:if="{{isFeedbackShow}}"> <view>是否遇到问题?</view> <button open-type="feedback">点击反馈</button> </view> wx.onUserCaptureScreen(() => { // 设置弹窗出现 this.setData({isFeedbackShow: true}) }); [代码] LogManager 关于小程序的 LogManager,大概是非常实用又特别低调的一个能力了。它的使用方式其实和 console 很相似,提供了 log、info、debug、warn 等日志方式。 [代码]const logger = wx.getLogManager() logger.log({str: 'hello world'}, 'basic log', 100, [1, 2, 3]) logger.info({str: 'hello world'}, 'info log', 100, [1, 2, 3]) logger.debug({str: 'hello world'}, 'debug log', 100, [1, 2, 3]) logger.warn({str: 'hello world'}, 'warn log', 100, [1, 2, 3]) [代码] 打印的日志,从管理后台下载下来之后,也是很好懂: [代码]2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] [v1.1.0] request begin 2019-6-25 22:11:6 [log] wx.request api invoke with seq 0 2019-6-25 22:11:6 [log] wx.request success callback with msg request:ok with seq 0 2019-6-25 22:11:6 [log] [v1.1.0] request done 2019-6-25 22:11:7 [log] wx.navigateTo api invoke 2019-6-25 22:11:7 [log] page packquery/pages/index/index onHide have been invoked 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onLoad have been invoked 2019-6-25 22:11:7 [log] [v1.1.0] logs | onShow | | [] 2019-6-25 22:11:7 [log] wx.setStorageSync api invoke 2019-6-25 22:11:7 [log] wx.setStorageSync return 2019-6-25 22:11:7 [log] wx.reportMonitor api invoke 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onShow have been invoked 2019-6-25 22:11:7 [log] wx.navigateTo success callback with msg navigateTo:ok [代码] LogManager 最多保存 5M 的日志内容,超过5M后,旧的日志内容会被删除。基础库默认会把 App、Page 的生命周期函数和 wx 命名空间下的函数调用写入日志,基础库的日志帮助我们定位具体哪些地方出了问题。 实时日志 小程序的 LogManager 有一个很大的痛点,就是必须依赖用户上报,入口又是右上角胶囊-【关于xxxx】-【投诉与反馈】-【功能异常】这么长的路径,甚至用户的反馈过程也会经常丢失日志,导致无法查问题。 为帮助小程序开发者快捷地排查小程序漏洞、定位问题,微信推出了实时日志功能。从基础库 2.7.1 开始,开发者可通过提供的接口打印日志,日志汇聚并实时上报到小程序后台。 使用方式如下: 使用 wx.getRealtimeLogManager 在代码⾥⾯打⽇志。 可从小程序管理后台【开发】-【运维中心】-【实时日志】进入日志查询页面,查看开发者打印的日志信息。 开发者可通过设置时间、微信号/OpenID、页面链接、FilterMsg内容(基础库2.7.3及以上支持setFilterMsg)等筛选条件查询指定用户的日志信息: [图片] 由于后台资源限制,实时日志使用规则如下: 为了定位问题方便,日志是按页面划分的,某一个页面,在onShow到onHide(切换到其它页面、右上角圆点退到后台)之间打的日志,会聚合成一条日志上报,并且在小程序管理后台上可以根据页面路径搜索出该条日志。 每个小程序账号每天限制500万条日志,日志会保留7天,建议遇到问题及时定位。 一条日志的上限是5KB,最多包含200次打印日志函数调用(info、warn、error调用都算),所以要谨慎打日志,避免在循环里面调用打日志接口,避免直接重写console.log的方式打日志。 意见反馈里面的日志,可根据OpenID搜索日志。 setFilterMsg 可以设置过滤的 Msg。这个接口的目的是提供某个场景的过滤能力,例如[代码]setFilterMsg('scene1')[代码],则在 MP 上可输入 scene1 查询得到该条日志。比如上线过程中,某个监控有问题,可以根据 FilterMsg 过滤这个场景下的具体的用户日志。FilterMsg 仅支持大小写字母。如果需要添加多个关键字,建议使用 addFilterMsg 替代 setFilterMsg。 日志开发技巧 既然官方提供了 LogManager 和实时日志,我们当然是两个都要用啦。 log.js 我们将所有日志的能力都封装在一起,暴露一个通用的接口给调用方使用: [代码]// log.js const VERSION = "0.0.1"; // 业务代码版本号,用户灰度过程中观察问题 const canIUseLogManage = wx.canIUse("getLogManager"); const logger = canIUseLogManage ? wx.getLogManager({level: 0}) : null; var realtimeLogger = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null; /** * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function DEBUG(file, ...args) { console.debug(file, " | ", ...args); if (canIUseLogManage) { logger!.debug(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function RUN(file, ...args) { console.log(file, " | ", ...args); if (canIUseLogManage) { logger!.log(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function ERROR(file, ...args) { console.error(file, " | ", ...args); if (canIUseLogManage) { logger!.warn(`[${VERSION}]`, file, " | ", ...args); } if (realtimeLogger) { realtimeLogger.error(`[${VERSION}]`, file, " | ", ...args); // 判断是否支持设置模糊搜索 // 错误的信息可记录到 FilterMsg,方便搜索定位 if (realtimeLogger.addFilterMsg) { try { realtimeLogger.addFilterMsg( `[${VERSION}] ${file} ${JSON.stringify(args)}` ); } catch (e) { realtimeLogger.addFilterMsg(`[${VERSION}] ${file}`); } } } } // 方便将页面名字自动打印 export function getLogger(fileName: string) { return { DEBUG: function(...args) { DEBUG(fileName, ...args); }, RUN: function(...args) { RUN(fileName, ...args); }, ERROR: function(...args) { ERROR(fileName, ...args); } }; } [代码] 通过这样的方式,我们在一个页面中使用日志的时候,可以这么使用: [代码]import { getLogger } from "./log"; const PAGE_MANE = "page_name"; const logger = getLogger(PAGE_MANE); [代码] autolog-behavior 现在有了日志组件,我们需要在足够多的地方记录日志,才能在问题出现的时候及时进行定位。一般来说,我们需要在每个方法在被调用的时候都打印一个日志,所以这里封装了一个 autolog-behavior 的方式,每个页面(需要是 Component 方式)中只需要引入这个 behavior,就可以在每个方法调用的时候,打印日志: [代码]// autolog-behavior.js import * as Log from "../utils/log"; /** * 本 Behavior 会在小程序 methods 中每个方法调用前添加一个 Log 说明 * 需要在 Component 的 data 属性中添加 PAGE_NAME,用于描述当前页面 */ export default Behavior({ definitionFilter(defFields) { // 获取定义的方法 Object.keys(defFields.methods || {}).forEach(methodName => { const originMethod = defFields.methods![methodName]; // 遍历更新每个方法 defFields.methods![methodName] = function(ev, ...args) { if (ev && ev.target && ev.currentTarget && ev.currentTarget.dataset) { // 如果是事件类型,则只需要记录 dataset 数据 Log.RUN(defFields.data.PAGE_NAME, `${methodName} invoke, event dataset = `, ev.currentTarget.dataset, "params = ", ...args); } else { // 其他情况下,则都记录日志 Log.RUN( defFields.data.PAGE_NAME, `${methodName} invoke, params = `, ev, ...args); } // 触发原有的方法 originMethod.call(this, ev, ...args); }; }); } }); [代码] 我们能看到,日志打印依赖了页面中定义了一个[代码]PAGE_NAME[代码]的 data 数据,所以我们在使用的时候可以这么处理: [代码]import { getLogger } from "../../utils/log"; import autologBehavior from "../../behaviors/autolog-behavior"; const PAGE_NAME = "page_name"; const logger = getLogger(PAGE_NAME); Component({ behaviors: [autologBehavior], data: { PAGE_NAME, // 其他数据 }, methods: { // 定义的方法会在调用的时候自动打印日志 } }); [代码] 页面如何使用 Behavior 看看官方文档:事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应[代码]json[代码]文件中包含[代码]usingComponents[代码]定义段。 完整的项目可以参考wxapp-typescript-demo。 参考 LogManager 实时日志 Component构造器 behaviors 结束语 使用自定义组件的方式来写页面,有特别多好用的技巧,behavior 就是其中一个比较重要的能力,大家可以发挥自己的想象力来实现很多奇妙的功能。
2019-12-10 - 使用插件后, wx.request不允许重写
为了解决wx.request并发的限制, 通过以下代码重载了wx.request的实现 var wxRequest = wx.request; Object.defineProperty(wx, "request" , { writable: true }); wx.request = function(){ //重写wx.request逻辑 } 不使用插件是正常的, 但如果使用插件, 报以下的错误: sdk uncaught third Error Cannot redefine property: request TypeError: Cannot redefine property: request 请问, 这是使用插件后安全性限制导致的吗? 如果是的话, 有没有解决办法(原始需求是想重写wx下的某些方法)?
2018-06-11 - 目前小程序canvas中怎么使用外部字体啊?
小程序canvas中需要引入外部字体,画图,目前支持么? 有这个功能么?context.font = '40px Inconsolata'
2019-06-19 - Android 下JS部分 Date.toLocaleString() 无效
代码非常短,代码片段已经提供。 js [代码]const app = getApp()[代码][代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]datedisplay:[代码][代码]""[代码][代码],[代码][代码] [代码][代码]},[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]let d = [代码][代码]new[代码] [代码]Date()[代码][代码] [代码][代码]this[代码][代码].setData({date:d})[代码][代码] [代码][代码]this[代码][代码].setData({datedisplay:d.toLocaleString()})[代码][代码] [代码][代码]},[代码][代码]})[代码] wxml [代码]<[代码][代码]wxs[代码] [代码]module[代码][代码]=[代码][代码]"util"[代码][代码]>[代码][代码] [代码][代码]function toLocal(time) {[代码][代码] [代码][代码]d = getDate(time)[代码][代码] [代码][代码]if (d == "Invalid Date") return ""[代码][代码] [代码][代码]return d.toLocaleString()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]module.exports.toLocal = toLocal[代码][代码]</[代码][代码]wxs[代码][代码]>[代码] [代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"intro"[代码][代码]><[代码][代码]text[代码][代码]>[代码][代码]JS toLocaleString:[代码][代码]{{datedisplay}}[代码][代码]</[代码][代码]text[代码][代码]>[代码][代码]</[代码][代码]view[代码][代码]>[代码] [代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"intro"[代码][代码]>[代码][代码]<[代码][代码]text[代码][代码]>[代码][代码]WXS toLocaleString:[代码][代码]{{util.toLocal(date)}}[代码][代码]</[代码][代码]text[代码][代码]></[代码][代码]view[代码][代码]>[代码] 做的事情也非常简单,对比js部分的Date.toLocaleString() 和wxs部分Date.toLocaleString() 安卓下截图: [图片] iOS截图 [图片] BUG显而易见。很显然在安卓里,js框架内的Date对象toLocaleString()被简单地作为toString()处理了,其他toLocaleTimeString(), toLocaleDateString()都存在问题。这个问题非常久了,希望赶紧处理。这都是小程序框架基础函数级别的服务,有这种问题那么久没发现也是一个奇迹。
2018-05-31 - wx.onError、App.onError疑惑及如何捕获Promise异常?
1、官方文档上说 wx.onError 和 App.onError 的回调时机与参数一致(https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onError.html),是指两种方式收集到的异常信息完全一致吗?我们实践过程中发现,wx.onError 获取到的信息比 App.onError 要少。 在我们上一个版本的微信小程序中,我们是在 App.onError 中监控异常,并通过 ELK 收集、查询异常。收集到的部分异常信息截图如下: [图片] [图片] 在我们小程序的最新版本中,我们使用了针对小程序平台的 Sentry SDK(https://github.com/lizhiyao/sentry-miniapp,该 SDK 原理是使用 wx.onError、wx.onPageNotFound、wx.onMemoryWarning 监控异常信息)进行信息收集上报,基于公司私有化部署的 Sentry 服务接收、存储、展示异常信息。结果发现 Sentry 服务没有收到 wx.onError 上报的异常(1. 上线之前有做过测试,Sentry SDK 是可以正常上报代码执行异常的。 2. 可以收集到页面无法找到、内存警告异常,说明线上版本小程序中 Sentry SDK 已经成功初始化,可以进行信息上报): [图片] 但是官方的微信预警群是有推送异常信息的: [图片] [图片] [图片] 2. App.onError 收集到的信息和官方后台运维中心收集到的信息是一致的吗?我们发现 onError 捕获的信息,在小程序官方后台查不到。 比如: 通过 App.onError 在 8.12 收集到了这样一条异常信息: [图片] 在小程序官方后台是搜不到这个异常信息记录的: [图片] 3. 假设 wx.onError 和 App.onError 获取到的异常信息完全一致,且和官方后台收集记录的异常信息完全一致。如果小程序后台运维中心的预警推送频率设置为 1次/5min,那么 onError 获取到的信息和微信预警群推送的信息完全一致吗? 4. 关于 Promise 的异常,对于浏览器有 window.onunhandledrejection,对于 node 有 global.process.on('unhandledRejection', callback()),对于小程序平台,有什么推荐的方式可以获取到 Promise 的异常吗?官方后台运维中心有收集到小程序中 Promise 中的异常吗?目前实践来看,小程序的 App.onError、wx.onError 中是无法捕获 Promise 的异常的。示例代码可参考:https://github.com/lizhiyao/sentry-miniapp/blob/master/examples/weapp/app.js 。 5. 在小程序官方后台及官方预警群中,会发现偶尔会出现非线上版本的异常被收集和上报了。请问这种情况是正常的吗? 比如:截图中 小程序版本对应为 0 的就是我们未发布版本代码中出现的异常。判断的依据是 /pages/homepage-config/skilled-tag/index 是新版本新增的页面,异常上报时新版本并未发布。 [图片]
2019-08-15 - textarea 的bindinput 失效?
微信版本号:7.0.4 基础库:2.9.3 手机:ios 9.3.3 预期效果:bindinput 有效 实际:bindinput 失效
2019-11-11 - hooks 在微信小程序中的试验
PS:首先,这不是一个成熟的东西,只是一个实现极其简单的玩具而已。 前言 前段时间 react hooks 特性刷得沸沸扬扬的,看起来挺有意思的,估计不少其他框架也会逐步跟进,所以也来尝试一下能不能用在小程序上。 react hooks 允许你在函数式组件中使用 state,用一段官方的简单例子概括如下: [代码]import { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } [代码] 函数式组件本身非常简洁,不维护生命周期和状态,是一个可以让性能得以优化的使用方式。但是在之前这种方式只能用于纯展示组件或者高阶组件等,它很难实现一些交互行为。但是在 hooks 出现之后,你就可以为所欲为了。 这里有一份官方的文档,不明围观群众有兴趣的可以点进去了解一下:https://reactjs.org/docs/hooks-intro.html。 hooks 的使用目前有两个限制: 只能在函数式组件内或其他自定义 hooks 内使用,不允许在循环、条件或普通 js 函数中调用 hooks。 只能在顶层调用 hooks 。 这个限制和 hooks 的实现方式有关,下面小程序 hooks 也会有同样限制,原因应该也是类似的。为了能让开发者更好的使用 hooks,react 官方也提供了一套 eslint 插件来协助我们开发:https://reactjs.org/docs/hooks-rules.html#eslint-plugin。 下面就来介绍下在小程序中的尝试~ 函数式组件 小程序没有提供函数式组件,这倒是很好理解,小程序的架构是双线程运行模式,逻辑层执行 js 代码,视图层负责渲染。那么声明在逻辑层的自定义组件要渲染在视图层必须保证来两个线程都存在自定义组件实例并一一对应,这样的架构已经成熟,目前对函数式组件并没有强烈的需求。在基础库不大改的情况下,就算提供了函数式组件也只是提供了另一种新写法而已,本质上的实现没有区别也不能提升什么性能。 不过也不排除以后小程序会提供一种只负责渲染不维护生命周期不做任何逻辑的特殊组件来优化渲染性能,这种的话本质上就和函数式组件类似了,不过函数式组件较为极端的是在理论上是有办法做到无实例的,这个在小程序中怕是有点困难。 言归正传,小程序没有提供函数式组件,那么就强行封装出一个写法好了,假设我们有一个自定义组件,它的 js 和 wxml 内容分别是这样的: [代码]// component.js const {useState, useEffect, FunctionalComponent} = require('miniprogram-hooks') FunctionalComponent(function() { const [count, setCount] = useState(1) useEffect(() => { console.log('count update: ', count) }, [count]) const [title, setTitle] = useState('click') return { count, title, setCount, setTitle, } }) [代码] [代码]<!-- component.wxml --> <view>{{count}}</view> <button bindtap="setCount" data-arg="{{count + 1}}">{{title}}</button> <button bindtap="setTitle" data-arg="{{title + '(' + count + ')'}}">update btn text</button> [代码] 一个很奇葩的例子,但是能看明白就行。小程序里视图和逻辑分离,不像 react 可以将视图和逻辑写到一起,那么小程序里的函数式组件里想返回一串渲染逻辑就不太科学了,这里就改成返回要用于渲染的 state 和方法。 PS:wxml 里不支持 bindtap=“setCount(count + 1)” 这种写法,所以参数就走 dataset 的方式传入了。 FunctionComponent 函数其实就相当于封装了小程序原有的 Component 构造器,它的实现类似这样: [代码]function FunctionalComponent(func) { func = typeof func === 'function' ? func : function () {} // 定义自定义组件 return Component({ attached() { this._$state = {} this._$effect = {} this._$func = () => { currentCompInst = this // 记录当前的自定义组件实例 callIndex = 0 // 初始化调用序号 const newDef = func.call(null) || {} currentCompInst = null const {data, methods} = splitDef(newDef) // 拆分 state 和方法 // 设置 methods Object.keys(methods).forEach(key => { this[key] = methods[key] }) // 设置 data this.setData(data) } this._$func() }, detached() { this._$state = null this._$effect = null this._$func = null } }) } [代码] 实现很简单,就是在 attached 的时候跑一下传入的函数,拿到 state 和方法后设置到自定义组件实例上就行。其中 currentCompInst 和 callIndex 在 useState 和 useEffect 的实现上会用到,下面来介绍。 useState 和 useEffect 这里的一个难点是,useState 是没有指定变量名的。初次渲染还好,二次渲染的话要找回这个变量就要费一段代码了。 PS:后续的实现除了参考了 react 的 hooks 外,也参考了 vue-hooks 的尝试,有兴趣的同学也可以去观摩一下。 这里上面提到的 currentCompInst 和 callIndex,将上一次的变量存储在 currentCompInst 中,用 callIndex 记录调用 useState 和 useEffect 的顺序,这样就可以在二次渲染的时候通过顺序找回上一次使用的变量: [代码]function useState(initValue) { if (!currentCompInst) throw new Error('component instance not found!') const index = callIndex++ const compInst = currentCompInst if (compInst._$state[index] === undefined) compInst._$state[index] = initValue const updater = function (evt) { let value = evt // wxml 事件回调 if (typeof evt === 'object' && evt.target && evt.currentTarget) { const dataset = evt.currentTarget.dataset value = dataset && dataset.arg } // 存入缓存 compInst._$state[index] = value compInst._$func() } updater._isUpdater = true return [compInst._$state[index], updater] } [代码] useEffect 的实现逻辑也类似,这里就不再贴代码了。小程序本身没有提供 render 函数,调 FunctionalComponent 声明函数式组件传入的函数就作为 render 函数来用。每次调 setXXX 方法——也就是上面代码中返回的 updater 的时候,找到原本存储这个 state 的地方存储进去,然后再次执行 render 函数,进行组件的渲染。 到这里应该就明白了,对于 hooks 使用为什么会有一开始的那两条限制。如果在一些条件、循环等语句内使用 hooks,就无法确保 state 的顺序,再二次渲染时就不一定能找回对应的 state。 尾声 完整的代码在 https://github.com/wechat-miniprogram/miniprogram-hooks,不过这终究只是个试验性质的尝试,并不推荐拿来实战,写在这里是为与大家共享~
2019-02-27 - 在Page()之外定义的变量作用域?为啥页面关闭了,变量值还值在
问题是:为啥页面关闭了,再次打开,还是上次更新后的值?难道页面关闭非Page()里面设置的变量,不能自动销毁是吗? 比如一个页面test.js代码如下 [代码]var[代码] [代码]_M={name:[代码][代码]'初始值'[代码][代码]}[代码][代码]Page({[代码][代码] [代码][代码]data: {[代码] [代码] [代码][代码]},[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码] [代码](options) {[代码][代码] [代码][代码]console.log([代码][代码]'onLoad-更新前'[代码][代码],_M);//这里当前页面关闭后,再次进入页面后,怎么会是最后面设置的值,难道页关闭后,当前页的变量不会自动销毁?谁能解释下?[代码][代码] [代码][代码]_M.name=[代码][代码]'更新了'[代码][代码] [代码][代码]console.log([代码][代码]'onLoad-更新后'[代码][代码],_M)[代码][代码] [代码][代码]},[代码] [代码] [代码][代码]onUnload: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]console.log([代码][代码]'onUnload'[代码][代码],_M)[代码][代码] [代码][代码]}[代码][代码]})[代码][图片]
2019-01-15 - 部分iphone进入小程序白屏
- 当前 Bug 的表现(可附上截图) [图片] [图片] - 预期表现 - 复现路径 正常进入小程序首页 - 提供一个最简复现 Demo
2019-07-03 - 安卓fail ssl hand shake error 错误
- 当前 Bug 的表现(可附上截图) [图片] - 预期表现 IOS系统下正常,没有复现此问题。安卓机时不时的会复现,但不是每次,因为还是会有几率正常,不出现fail ssl hand shake error错误。而且如图,奇怪的是,截图中红色标记部分,最后一个是出现问题请求之后的,同样的https域名地址请求,前两个出现问题,最后一个反而不出问题,能正确读取到数据。照道理如果是证书问题,所有的请求都会出现啊。此页面同时还有wss请求,域名与https一样,没有出现,链接正常 [图片] [图片] [图片] [图片] - 复现路径 pages/home/index/index - 提供一个最简复现 Demo [图片] 此二维码扫描进去后,点击热销车型中的“AMG GT”车型进去后,点击预约试驾按钮
2018-10-19 - 线上错误,Failed get storage group..
[图片] webviewSDKScriptError Failed get storage group metadata; getGlobalStorage:fail:access denied Error: Failed get storage group metadata at Ke (:44:20578) at :44:23185 at Object.w (:41:2398) at Object.ret.invokeCallbackHandler (:21:28) at :1:67 小程序名称:【足球战术板】 开发框架:wepy 错误原因分析:我在onError()中设置了将错误信息发送到邮箱, 然后就看到这些错误,应该是获取storage数据出错,错误原因可能是这个小程序较为频繁的操作storage,但详细信息不清楚,错误堆栈也很模糊,请问这是程序问题还是小程序本身问题?
2018-09-21 - 部分机型出现Failed get storage group data错误
webviewSDKScriptError Failed get storage group data;getGlobalStorage:fail:data not found Error: Failed get storage group data at Ke (<anonymous>:4:20578) at <anonymous>:4:23887 at Object.w (<anonymous>:1:2398) at Object.ret.invokeCallbackHandler (<anonymous>:21:28) at <anonymous>:1:67 [图片]
2018-09-28 - 关于JSON解析时 Unicode U+2028 等字符的bug
- 概述 wx.request 对于含有 U+2028 等字符的 Response Body 存在错误的过度处理(注意:该问题只在真机上复现)。 经过查找文章和社区,我发现这是一个反馈较多的问题(参见最后“可能的相关问题”部分),对于特殊字符很多人选择了进行过滤,但这是不对的,正确的做法应当是对其进行合理编码、解码。 - JSON对特殊字符的处理 在详细描述bug之前,我们先了解下JSON处理中特殊字符的正确处理方式。 在 ECMAscript 5.1 ( https://www.ecma-international.org/ecma-262/5.1/#sec-15.12.2 ) 中有这样一句话: JSON uses a more limited set of white space characters than WhiteSpace and allows Unicode code points U+2028 and U+2029 to directly appear in JSONString literals without using an escape sequence. 这意味着 JSON.stringify 和 JSON.parse 都不应当对空格、U+2028、U+2029这三个字符进行转义,例如对于空格: [图片] 可以看到空格未进行转义,同样的道理,U+2028 也不会被转义: [图片] 但对于 '\n' ,是需要转义的: [图片] 因此可以得出结论,在HTTP Response中 U+2028 等字符是不应当被转义的;由于这两个字符被创造也是有其含义和目的,更不应当被过滤掉(如 https://developers.weixin.qq.com/community/develop/doc/8d93389c3bea4acff6e7bb765c3e634f?highLine=2028 和 https://developers.weixin.qq.com/community/develop/doc/f085c4b5f547113dbb032d0f4b46e1b5 ) - wx.request 的问题 在 wx.request 对JSON的处理中,没有正确将 JSON String 解析为 Object,而是将 String进行了返回,同时将 U+2028 和 U+2029 两个字符替代成了 \n 。在这一过程中存在如下问题: 假设wx.request处理JSON异常,应当触发fail回调函数传递具体的错误,给用户明确的意图,而不是调用 success 给出未能成功解析的字符串。(举一个例子,JSON.parse 结果并不一定是Object,如 http://www.mocky.io/v2/5bea7d792f0000df0bda3a4e 这个接口在浏览器用fetch调用返回的就是字符串)。 wx.request应当能正常处理含有U+2028 等字符的Response,应当原样保留这些特殊字符并反馈 JSON.parse 出的结果 但是通过调试可以发现,wx.request对U+2028 等字符进行了错误的处理,在返回的 String 中将其替换成了 \n (注意不是 \\n,前者是一个字符,后者是两个字符),这进一步导致了用户难以通过 JSON.parse 手动解析String数据: [图片] 在上面的例子中我说过 '\n' 是应该被转义为 '\\n' 的,否则在 JSON.parse 的时候就会造成报错。 - 预期表现 wx.reqeust应该正确处理特殊字符 wx.request在JSON解析失败时应当调用fail回调 - 临时解决方案 这是一个会有bug的解决方案,原因不再赘述,仅供参考: [代码]wx.request({[代码][代码] [代码][代码]url: [代码][代码]"http://xxx"[代码][代码],[代码][代码] [代码][代码]success(res) {[代码][代码] [代码][代码]const originData = res.data;[代码] [代码] // 无法处理纯字符串的情况,慎用[代码] [代码] [代码][代码]const data =[代码][代码] [代码][代码]typeof[代码] [代码]originData === [代码][代码]"string"[代码][代码] [代码][代码]? JSON.parse(originData.replace(/\n/g, [代码][代码]"\\n"[代码][代码]))[代码][代码] [代码][代码]: originData;[代码][代码] [代码][代码]console.log(data); [代码][代码]// 替代 res.data[代码][代码] [代码][代码]}[代码][代码]});[代码] - 复现路径 在这个代码片段中 https://developers.weixin.qq.com/s/iam8hTmP7m3S ,可以通过点击不同button分别调用含有 \n 、\u2028、\u2029 和空格的接口,查看console的输出。 注意:必须在真机上才能复现,模拟器不可以 - 可能的相关问题 https://developers.weixin.qq.com/community/develop/doc/8d93389c3bea4acff6e7bb765c3e634f?highLine=2028 https://developers.weixin.qq.com/community/develop/doc/000ca414b64b1099bb1701ce55b800?highLine=2028 https://developers.weixin.qq.com/community/develop/doc/99f032bff8b2a6ccb37ffd4414aa5575?highLine=json%25202028 https://developers.weixin.qq.com/community/develop/doc/000c443ed5c488009dd6b81aa51404?highLine=2028 https://developers.weixin.qq.com/community/develop/doc/f085c4b5f547113dbb032d0f4b46e1b5 https://developers.weixin.qq.com/community/develop/doc/ca057a276c3e316cb5086b0b26a69763 此外这个问题反馈了U+2028与setData使用的bug,我猜测可能与本问题可能存在一定的关联性: https://developers.weixin.qq.com/community/develop/doc/000c443ed5c488009dd6b81aa51404?highLine=2028
2018-11-13 - 小程序自定义组件知多少
自定义组件 why 代码的复用 在起初小程序只支持 Page 的时候,就会有这样蛋疼的问题:多个页面有相同的组件,每个页面都要复制粘贴一遍,每次改动都要全局搜索一遍,还说不准哪里改漏了就出翔了。 组件化设计 在前端项目中,组件化是很常见的方式,某块通用能力的抽象和设计,是一个必备的技能。组件的管理、数据的管理、应用状态的管理,这些在我们设计的过程中都是需要去思考的。当然你也可以说我就堆代码就好了,不过一个真正的码农是不允许自己这么随便的! 所以,组件化是现代前端必须掌握的生存技能! 自定义组件的实现 一切都从 Virtual DOM 说起 前面《解剖小程序的 setData》有讲过,基于小程序的双线程设计,视图层(Webview 线程)和逻辑层(JS 线程)之间通信(表现为 setData),是基于虚拟 DOM 来实现数据通信和模版更新的。 自定义组件一样的双线程,所以一样滴基于 Virtual DOM 来实现通信。那在这里,Virtual DOM 的一些基本知识(包括生成 VD 对象、Diff 更新等),就不过多介绍啦~ Shadow DOM 模型 基于 Virtual DOM,我们知道在这样的设计里,需要一个框架来支撑维护整个页面的节点树相关信息,包括节点的属性、事件绑定等。在小程序里,Exparser 承担了这个角色。 前面《关于小程序的基础库》也讲过,Exparser 的主要特点包括: 基于 Shadow DOM 模型 可在纯 JS 环境中运行 Shadow DOM 是什么呢,它就是我们在写代码时候写的自定义组件、内置组件、原生组件等。Shadow DOM 为 Web 组件中的 DOM 和 CSS 提供了封装。Shadow DOM 使得这些东西与主文档的 DOM 保持分离。 简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装 HTML 组件(类似 vue 组件,将 html,css,js 独立部分提取)。 例如我们定义了一个自定义组件叫[代码]<my-component>[代码],你在开发者工具可以见到: [图片] [代码]#shadow-root[代码]称为影子根,DOM 子树的根节点,和文档的主要 DOM 树分开渲染。可以看到它在[代码]<my-component>[代码]里面,换句话说,[代码]#shadow-root[代码]寄生在[代码]<my-component>[代码]上。[代码]#shadow-root[代码]可以嵌套,形成节点树,即称为影子树(Shadow Tree)。 像这样: [图片] Shadow Tree 拼接 既然组件是基于 Shadow DOM,那组件的嵌套关系,其实也就是 Shadow DOM 的嵌套,也可称为 Shadow Tree 的拼接。 Shadow Tree 拼接是怎么做的呢?一切又得从模版引擎讲起。 我们知道,Virtual DOM 机制会将节点解析成一个对象,那这个对象要怎么生成真正的 DOM 节点呢?数据变更又是怎么更新到界面的呢?这大概就是模版引擎做的事情了。 《前端模板引擎》里有详细描述模版引擎的机制,通常来说主要有这些: DOM 节点的创建和管理:[代码]appendChild[代码]/[代码]insertBefore[代码]/[代码]removeChild[代码]/[代码]replaceChild[代码]等 DOM 节点的关系(嵌套的处理):[代码]parentNode[代码]/[代码]childNodes[代码] 通常创建后的 DOM 节点会保存一个映射,在更新的时候取到映射,然后进行处理(通常包括替换节点、改变内容[代码]innerHTML[代码]、移动删除新增节点、修改节点属性[代码]setAttribute[代码]) 在上面的图我们也可以看到,在 Shadow Tree 拼接的过程中,有些节点并不会最终生成 DOM 节点,例如[代码]<slot>[代码]这种。 但是,常用的前端模版引擎,能直接用在小程序里吗? 双线程的难题 自定义组件渲染流程 双线程的设计,给小程序带来了很多便利,安全性管控力都拥有了,当然什么鬼东西都可以比作一把双刃剑,双线程也不例外。 我们知道,小程序分为 Webview 和 JS 双线程,逻辑层里是没法拿到真正的 DOM 节点,也没法随便动态变更页面的。那在这种情况下,我们要怎么去使用映射来更新模版呢(因为我们压根拿不到 Webview 节点的映射)? 所以在双线程下,其实两个线程都需要保存一份节点信息。这份节点信息怎么来的呢?其实就是我们需要在创建组件的时候,通过事件通知的方式,分别在逻辑层和视图层创建一份节点信息。 同时,视图层里的组件是有层级关系的,但是 JS 里没有怎么办?为了维护好父子嵌套等节点关系,所以我们在 逻辑层也需要维护一棵 Shadow Tree。 那么我们自定义组件的渲染流程大概是: 组件创建。 逻辑层:先是 wxml + js 生成一个 JS 对象(因为需要访问组件实例 this 呀),然后是 JS 其中节点部分生成 Virtual DOM,拼接 Shadow Tree 什么的,最后通过底层通信通知到 视图层 视图层:拿到节点信息,然后吭哧吭哧开始创建 Shadow DOM,拼接 Shadow Tree 什么的,最后生成真实 DOM,并保留下映射关系 组件更新。 这时候我们知道,不管是逻辑层,还是视图层,都维护了一份 Shadow Tree,要怎么保证他们之间保持一致呢? 让 JS 和 Webview 的组件保持一致 为了让两边的 Shadow Tree 保持一致,可以使用同步队列来传递信息。(这样就不会漏掉啦) 同步队列可以,每次变动我们就往队列里塞东西就好了。不过这样还会有个问题,我们也知道 setData 其实在实际项目里是使用比较频繁的,要是像 Component 的 observer 里做了 setData 这类型的操作,那不是每次变动会导致一大堆的 setDate?这样通信效率会很低吧? 所以,其实可以把一次操作里的所有 setData 都整到一次通信里,通过排序保证好顺序就好啦。 Page 和 Component Component 是 Page 的超集 事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含[代码]usingComponents[代码]定义段。 来自官方文档-Component 所以,基于 Component 是 Page 的超集,那么其实组件的渲染流程、方式,其实跟页面没多大区别,应该可以一个方式去理解就差不多啦。 页面渲染 既然页面就是组件,那其实页面的渲染流程跟组件的渲染流程基本保持一致。 视图层渲染,可以参考7.4 视图层渲染说明。 结束语 其实很多新框架新工具出来的时候,经常会让人眼前一亮,觉得哇好厉害,哇好高大上。 但其实更多时候,我们需要挖掘新事物的核心,其实大多数都是在原有的事物上增加了个新视角,从不一样的视角看,看到的就不一样了呢。作为一名码农,我们要看到不变的共性,变化的趋势。
2019-04-01