- 小程序获取某项权限的通用写法
温馨提示: [代码]errMsg: "openSetting:fail can only be invoked by user TAP gesture." [代码] wx.openSetting 不能直接在程序中调用,但是可以通过 wx.showModal 拉起。 【相关链接】 1、 wx.getSetting(Object object) 2、 wx.authorize(Object object) 3、 wx.showModal(Object object) 4、 wx.openSetting(Object object) update at 2022.08.02 再次查看这篇文章发现代码并不好懂,决定修改下 ; 逻辑还是一样的(代码中有注释),只是改变了写法 (1) uni.getSetting -> (2) uni.authorize -> (3) uni.showModal -> (4) uni.openSetting 以获取录音权限为例: 正常情况下,第一次,会提示用户授权,截图如下: [图片] 如果用户选择“允许”,那么一切皆顺 如果用户选择“拒绝”,就比较麻烦了,需要在 [代码]openSetting[代码] 打开,截图如下: [图片] 因为[代码]openSetting[代码]不能直接由程序调用,所以需要由[代码]showModal[代码]拉起,截图如下: [图片] 下面是代码截图: [图片] [图片] 下面是获取某项授权的完整代码 [代码]// 获取微信录音权限 // scopeStr = "scope.record" // modalMsg = "需要您授权获取录音权限" async getWxRecordAuth(scopeStr, modalMsg) { try { let [flag1, flag2] = [true, false] // (1) wx.getSetting 检查用户之前是否已经授权 flag1 = await new Promise((resolve)=>{ wx.getSetting({ success(res) { if (res.authSetting[scopeStr]) { resolve(true) } else { resolve(false) } } }) }) if (!flag1) { // (2) wx.authorize 弹窗请用户授权 flag1 = await new Promise((resolve)=>{ wx.authorize({ scope: scopeStr, success(res) { resolve(true) }, fail() { resolve(false) } }) }) } if (!flag1) { // (3) wx.showModal ,新规范下, // 必须通过按钮或者`wx.showModal`调起`wx.openSetting` flag2 = await new Promise((resolve)=>{ wx.showModal({ title: '授权', showCancel: false, content: modalMsg, success(res) { if (res.confirm) { resolve(true) } else if (res.cancel) { resolve(false) } else { resolve(false) } } }) }) } if (flag2) { // (4) wx.openSetting 请用户打开授权 flag1 = await new Promise((resolve)=>{ wx.openSetting({ success(res) { if (res.authSetting[scopeStr]) { resolve(true) } else { resolve(false) } } }) }) } return flag1 } catch (err) { console.log(err) return false } }, [代码] update at 2022.08.02 下面是之前的内容 小程序获取某项权限 【相关链接】微信小程序:开放能力/用户信息/授权 【相关链接】表单组件/button , [代码]open-type[代码] : [代码]openSetting[代码] uni -> wx , 有些地方写的是 [代码]uni[代码] 直接替换为 [代码]wx[代码] 可以把 [代码]scope.userLocation[代码] 替换为其他值,或者封装一下,变成通用的获取权限方式 封装时,别忘记,把 [代码]"需要您授权获取地理位置权限"[代码] 也封装一下,这个是弹窗的提示信息 提示: 这里的几个方法 promise 返回的是数组,[代码]arr[0][代码] 有值,就是出错了;[代码]arr[1][代码]才是success时返回的结果值,想要获取的结果值都存放在 [代码]arr[1][代码] 中。 [代码]// (1) uni.getSetting -> (2) uni.authorize -> (3) uni.showModal -> (4) uni.openSetting //授权获取地理位置权限 async getAuth_userLocation(){ try { //(1)getSetting const authSetting = await uni.getSetting(); if (authSetting['scope.userLocation']) { return true; } else { //(2)authorize const resAuthorize = await uni.authorize({ scope: 'scope.userLocation' }) //resAuthorize[0]:拒绝,resAuthorize[1]:接受; if (resAuthorize[1]) { return true; } else { //(3)showModal const resShowModal = await uni.showModal({ title: '授权', content: '需要您授权获取地理位置权限' }); //resShowModal[0]: null, resShowModal[1]: `confirm`,`cancel`都定义在"resShowModal[1]"中 if (resShowModal[1]) { if (resShowModal[1].confirm) { //(4)openSetting const resOpenSetting = await uni.openSetting(); //resOpenSetting[0]: null, resOpenSetting[1]: `authSetting`定义在"resOpenSetting[1]"中 if (resOpenSetting[1]) { const authSetting = resOpenSetting[1].authSetting; if (authSetting && authSetting['scope.userLocation']) { return true; } else { return false; } } else { return false; } } if (resShowModal[1].cancel) { return false; } } else { return false; } } } } catch (err) { console.log(err); return false; } }, [代码] 原来的写法 [代码] getCameraAuth(){ const that = this; // 获取摄像头权限 // (1) uni.getSetting -> (2) uni.authorize -> (3) uni.showModal -> (4) uni.openSetting uni.getSetting({ success(res) { const authSetting = res.authSetting; if (authSetting['scope.camera']) { // 已经授权 that.is_camera_auth = true; } else { // 未授权 uni.authorize({ scope: 'scope.camera', success(resSuccess) { // 同意授权 that.is_camera_auth = true; },fail(resFail) { console.log("resFail: ", resFail); // 引导用户授权 uni.showModal({ title: '授权', content: '需要您授权获取摄像头权限', success: function (res) { if (res.confirm) { uni.openSetting({ success(resOpenSetting) { // resOpenSetting: {errMsg: "openSetting:ok", authSetting: {scope.camera: false}} //console.log("resOpenSetting: ", resOpenSetting) const authSetting = resOpenSetting.authSetting if (authSetting && authSetting['scope.camera']) { that.is_camera_auth = true; } else { uni.showToast({icon:'none', title: '您拒绝授权小程序获取摄像头权限', duration: 1500}); } } }); } else if (res.cancel) { uni.showToast({icon:'none', title: '您拒绝授权小程序获取摄像头权限', duration: 1500}); } } }); } }); } } }); }, [代码] 这是原来的嵌套写法,没有上面的 Promise 版本方便;放在这里作为参照。
2022-08-02 - 高阶性能渲染-wxs
我们永远没有资格说放弃,因为这是属于我们的年华,应该开出耀眼的繁花。 这里主要讲解两点使用方式 wxs --> 数据处理使用 wxs --> 拖拽使用 wxs介绍 1.先看看官方如何介绍的 ,如下 点我可以查看更多官方文档地址 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 总的来说,我主要看中它快和方便 (有种在wxml写函数的感觉),并且处理函数内容后渲染不需要去调用setData,可以节省在请求数据返回后写大量的逻辑函数或者遍历方法去处理事务,所以我才拿出来讲解。 wxs数据处理使用 使用我习惯现在utils文件夹里创建一个common.wxs,然后在使用的wxml引入该文件。具体使用也很简单,如下 在你定义的common.wxs文件里,写你需要处理的函数,然后使用export导出 [代码] var getMax = function(array) { var max = undefined; for (var i = 0; i < array.length; ++i) { max = max === undefined ? array[i] : (max >= array[i] ? max : array[i]); } return max; } module.exports = { getMax: getMax }; [代码] 在.wxml文件需要的地方引入该.wxs文件,其中<wxs>标签包含2个重要属性,[代码]module[代码]和[代码]src[代码]<br> module表示你要导出wxs的方法集合,在wxml里面可以使用该方法名去使用具体的函数<br> src表示你.wxs路径位置引入,代码如下 [代码]<wxs module="common" src="../../utils/common.wxs"></wxs> <view> <text>{{common.getMax(arrNumber)}}</text> </view> [代码] 上面代码里[代码]arrNumber[代码]是你页面.js文件里面定义的数据 [代码]data: { arrNumber: [1,2,9,2,1,5,7] } [代码] 就是如此简单,你已经学会了使用wxs处理数据了,接下来我们来点难的,使用wxs做一个可以随便拖动又不影响小程序性能的手势拖拽事件 wxs拖拽使用 你是否还在使用setData控制元素?为什么不能频繁使用setData,点我查看详情然后通过输出手势坐标来移动元素具体位置?如果是建议你该换个方法了,推荐使用wxs,让你小程序性能上一个档次。 解决思路如下: 我们先在页面的.wxml文件书写初始化的样式内容; 给[代码]view[代码]标签绑定手势事件,其中[代码]touchmove[代码]我使用了阻止冒泡事件绑定是为了防止在苹果机中,元素移动会带着屏幕一起移动,导致滑动手势突然中断不流畅, [代码]clickLive[代码]事件我也用了阻止冒泡事件是为了触发子元素的事件防止点击被父元素拦截而使用,代码如下: [代码]<wxs module="comm" src="../../utils/common.wxs"></wxs> <view class="enterLive liveMove" data-maxWidth="{{windowWidth}}" data-maxHeight="{{windowHeight}}" bind:touchstart="{{comm.liveTouchmove}}" catch:touchmove="{{comm.liveTouchmove}}" bind:touchend="{{comm.liveTouchmove}}"> <view class="live-block" catch:tap="{{comm.clickLive}}"> <image class="liveEnter-img" mode="aspectFit" src="https://img0.baidu.com/it/u=401284325,23907343&fm=26&fmt=auto&gp=0.jpg" /> </view> </view> [代码] 我们发现上面的代码有[代码]windowWidth,windowHeight[代码]两个参数,这是在页面的.js中,是计算当前页面的宽高,用于防止滑块滑出视野范围内,导致无法滑动回来,其中下面代码[代码]getApp().globalData.systemInfo[代码]是读取全局文件[代码]app.js[代码]里面拿值的,那里我有获取系统参数设为全局保存, 而[代码]bindEnter[代码]事件是用于待会在[代码].wxs[代码]里面可以触发.js文件内函数示例 [代码]data: { windowWidth: 375, windowHeight: 667, }, ready: function() { let systemInfo = getApp().globalData.systemInfo; let width = 150 * systemInfo.windowWidth / 750; let height = 150 * systemInfo.windowWidth / 750; if (systemInfo) { this.setData({ windowWidth: systemInfo.windowWidth - parseInt(width), windowHeight: systemInfo.windowHeight - parseInt(height) }) } }, bindEnter(e) { wx.showToast({ title: '你点击到我啦~', icon: 'none' }) }, [代码] 接下来我们将书写.wxs文件,处理手势输出内容,使页面的滑块可以平静的滑动; 认真看的同学会发现页面的[代码]touchstart,touchmove,touchend[代码]事件我都是绑定同一个函数[代码]liveTouchmove[代码]这里是我想节约函数名称,统一使用一个函数,然后在输出的回调里面判断当前是什么手势 这里的滑块滑动逻辑是:当输出是touchstart手势时,我们记住当前滑块的[代码]starLeft,starTop[代码]值及坐标[代码]starX,starY[代码]值 当我们判断当前是[代码]touchmove[代码]手势时,我们就用当前的x,y坐标值减去开始starX和starY坐标值,得到一个差值[代码]diffX,diffY[代码] 然后我们使用这个[代码]diffX,diffY[代码]差值分别加上滑块刚开始的[代码]starLeft,starTop[代码]值,即可得到我们滑块当前移动的位置 最后我们通过条件判断当前是否超出屏幕即可,我这里做多一步,就是滑块永远停留左右两边,所以要多算一次滑块停留是偏离那一边,在通过[代码]ownerInstance.selectComponent('.liveMove').setStyle[代码]可以给页面元素添加行内样式。 滑动滑动逻辑已经讲完了,就是这么简单 如果你想通过[代码].wxs[代码]函数触发[代码].js[代码]文件内的函数,这里有提供[代码]ownerInstance.callMethod("bindEnter")[代码]方法给你触发.js里面的函数,其中[代码]ownerInstance[代码]是绑定页面函数的第二个回调参数,[代码]callMethod[代码]是官方提供的一个方法,[代码]bindEnter[代码]是.js文件里自己命名的函数方法名称,整体代码如下:更多wxs内置方法,请查看官方文档 [代码]/** * 滑块计算位置 */ var starLeft = 0; var starTop = 0; var starX = 0; var starY = 0; function liveTouchmove(event, ownerInstance) { // console.log(JSON.stringify(event)) // console.log(JSON.stringify(ownerInstance)) var left = 0, top = 0; var diffX = 0, diffY = 0; if(event.type === 'touchstart') { starLeft = event.currentTarget.offsetLeft; starTop = event.currentTarget.offsetTop; starX = event.changedTouches[0].clientX; starY = event.changedTouches[0].clientY; left = starLeft; top = starTop; } else { diffX = event.changedTouches[0].clientX - starX; diffY = event.changedTouches[0].clientY - starY; left = starLeft + diffX; top = starTop + diffY; var maxWidth = event.currentTarget.dataset.maxwidth; var maxHeight = event.currentTarget.dataset.maxheight; if (left > maxWidth) { left = maxWidth; } if (top > maxHeight) { top = maxHeight; } if (left <= 0) { left = 0; } if (top <= 0) { top = 0; } } if (event.type === 'touchend') { if (maxWidth / 2 < left) { left = "calc(100% - 3% - 152rpx)"; } if (maxWidth / 2 > left) { left = '3%'; } } else { left = left + 'px'; } var instance = ownerInstance.selectComponent('.liveMove'); // 返回组件的实例 instance.setStyle({ left: left, top: top +'px', }); } /** * 点击直播入口 * @param */ function clickLive (event, ownerInstance) { ownerInstance.callMethod("bindEnter"); } module.exports = { liveTouchmove: liveTouchmove, clickLive: clickLive, }; [代码] 总结 wxs确实可以解决我们一些性能问题,和wxml函数调用方便,但是我们也要注意几个问题 目前还不支持原生组件的事件、input和textarea组件的 bindinput 事件 1.02.1901170及以后版本的开发者工具上支持交互动画,最低版本基础库是2.4.4 目前在WXS函数里面仅支持console.log方式打日志定位问题,注意连续的重复日志会被过滤掉。 wxs有自己的语法,我理解为不支持ES6及以上语法更多wxs语法可以查看官方文档 以上内容,包括之前写的文章内容,最终会上传到我的gitee里面,请留意。 文章创作不易,喜欢的记得点赞 本文同步掘金号文章 喜欢的记得去点个赞哦
2021-07-11 - 低功耗蓝牙使用API设计封装函数模式示例
var bluetooth = { RESET: function(){ // 基础数据配置 this.btName = ""; this.deviceId = ""; this.serviceId = ""; this.write_characteristicId = ""; this.read_characteristicId = ""; clearTimeout(this.sendTimer); this.sendTimer = null; }, START: async function () { // 启动蓝牙,判断用户蓝牙是否已开启 }, SEARCH: async function () { // 搜索设备是否在附近,wx.onBluetoothDeviceFound 记得GPS权限,切勿频繁先调用GPS获取用户地理位置再搜索 await this.START(); return new Promise((reslove, reject)=>{ reslove(); }) }, CONNECT: async function () { // 连接设备 // if(self.btName == device.btName) let sameName = self.btName == device.btName ? true : false; // 记录蓝牙连接是否多次同时快速触发 if(this.waitConnect && sameName){ console.log("正在连接,请稍等...") return; } this.waitConnect = true; await this.SEARCH(); return new Promise((reslove, reject)=>{ this.waitConnect = false; reslove(); }) }, SEND: async function (device, data, callback) { // 发送数据 this.passCheck = device.passCheck //这样写,可以兼容连接设备中,发送指令1,但是连接过程中,又发送指令2,那么连接成功后,处理指令2 this.data= device.data this.callback = device.callback await this.CONNECT(); return new Promise((reslove, reject)=>{ device.sendEnd(); callback(); }) }, receiveDataCallback: function(){ // 接收设备返回的数据到前台 callback(value) }, CLOSE: async function () { // 关闭蓝牙线程 }, DISCONNECT: function(){ // 断开指定设备连接 }, STOP: function(){ // 停止搜索监听 }, ERROR: function(){ /*** * ERROR 错误 * err @params {} * err.ercode @params Number * 0 ok 正常 * 10000 not init 未初始化蓝牙适配器 * 10001 not available 当前蓝牙适配器不可用 * 10002 no device 没有找到指定设备 * 10003 connection fail 连接失败 * 10004 no service 没有找到指定服务 * 10005 no characteristic 没有找到指定特征值 * 10006 no connection 当前连接已断开 * 10007 property not support 当前特征值不支持此操作 * 10008 system error 其余所有系统上报的异常 * 10009 system not support Android 系统特有,系统版本低于 4.3 不支持 BLE * 10012 operate time out 连接超时 * 10013 invalid_data 连接 deviceId 为空或者是格式不正确 */ }, } var device = { btName: "btName", deviceId: "AB:CD:EF:00:00:01", // 实践中设备端地址请勿以 00 开头 00:00:00:AB:CD:EF passCheck: new Date().getTime(), // 记录触发跟设备返回是否为同一事件 sendEnd: function(res){ // 用于记录发送数据给设备成功后,设备一直未返回执行相关命令 clearTimeout(bluetooth.sendTimer); bluetooth.sendTimer = setTimeout(() => { clearTimeout(bluetooth.sendTimer); bluetooth.CLOSE(); }, timeout); }, onBLEConnectionStateChange: function(res){ // 用于监听蓝牙设备操作过程中,设备主动断开(例如:突然把设备断电或手动断开蓝牙捕获异常处理) // if(device.passCheck == ) }, capture: function(err){ // 用于获取操作过程中,蓝牙未启动,操作过程中连接失败或连接操作过程中出现的异常 // if(device.passCheck == ) } } // 使用示例 let data = "00 11 22"; bluetooth.START(device); bluetooth.SEARCH(device); bluetooth.CONNECT(device); bluetooth.SEND(device, data, function (value) { console.log("value=>",value) // 用于收到设备返回数据回调处理 if(value){ bluetooth.passCheck = ""; bluetooth.CLOSE(); } }) 针对连接问题,方法推荐: // timeout 针对特殊手机,如果发现首次连接铁定失败,或首次连接超时较多,可以将时间首次连接改到小于一秒,这样第二次连接timeout=10s, // 成功率较多, // 这种做法可以减少因差手机第一次连太久出现用户手动关闭的情况。毕竟一秒能连上的手机都是性能较好,安卓版本较高的手机。 var allowWaitTime = 15 * 1000; var timeout = 10 * 1000; var startCreateTime = new Date().getTime(); function createBLEConnection(){ wx.createBLEConnection({ // 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接 timeout: timeout, // number 否 超时时间,单位ms,不填表示不会超时 deviceId: self.deviceId, // serial: false, // 会闪退的 success(res) { // console.log("createBLEConnection res=>",res) }, fail(err) { // console.log("createBLEConnection err=>",err) let useCreateTime = new Date().getTime() - startCreateTime; // 这样写正常情况下都会按时返回的,例如,请问你的蓝牙 // 连设备多长时间后超时,你可以根据timeout 回答。但这种误差可能造成你无法提供解释的情况,无奈下推荐写个时间计算器, // 时间一到,不管成功与否都弹出失败提示。哪怕最后一秒连上了,也弹出失败。(无奈,无奈,无奈)可以减少与非同行讲解误差。 if (err.errCode == 10012) { // 连接超时,可能 if(useCreateTime >= allowWaitTime) { self.ERROR(err); self.CLOSE(); }else{ setTimeout(()=>{ createBLEConnection(); }, t ); // t 根据测试数据,效果来定 } }else { // (err.errCode == 10003) // (err.errCode == -1) // 10003 底层蓝牙失败,处理大体思路 CLOSE(); START(); CREATE(); } }); } createBLEConnection(); 代码片段示例 https://developers.weixin.qq.com/s/qJ6lORmy7LpS github https://github.com/synctimes163/wxapp-bluetooth
2021-04-26 - JS中的二进制数据处理
前言 在现有的计算机中,二进制常常以字节数组的形式存在于程序当中。例如在C#里面,就用byte[],标准C里面没有byte类型,但可以通过typedef把byte定义为unsigned char的别名,效果是一样的。JS设计之初似乎就没想过要处理二进制,对于字节的概念可以说是非常非常的模糊。如果要表达字节数组,那么似乎只能用一个普通数组来表示。 然而随着业务需求的逐渐发展,出现了WebGL这样的技术。所谓WebGL,就是指浏览器与显卡之间的通信接口。为了满足JavaScript与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。类型化数组(Typed Array)就是在这种背景下诞生的。而类型化数组是建立在ArrayBuffer对象的基础上的。下面介绍一下Arraybuffer。 一、Arraybuffer1.1 基本概念 ArrayBuffer 对象是 ES6 才纳入正式 ECMAScript 规范,是 JavaScript 操作二进制数据的一个接口。ArrayBuffer 对象是以数组的语法处理二进制数据,也称二进制数组。它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写。 ❝ArrayBuffer 简单说是一片内存,但是你不能直接用它。这就好比你在 C 里面,malloc 一片内存出来,你也会把它转换成 unsigned_int32 或者 int16 这些你需要的实际类型的数组/指针来用。这就是 JS 里的 TypedArray 的作用,那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN 上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。❝1.2 基本操作「语法」 new ArrayBuffer(length) 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节;返回值:ArrayBuffer 对象;异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常;「示例」 const buffer = new ArrayBuffer(32); buffer.byteLength; // 32 const v = new Int32Array(buffer); ArrayBuffer.isView(v) // true const buffer2 = buffer.slice(0, 1); 上面代码表示实例对象 buffer 占用 32 个字节。 它有实例属性 byteLength ,表示当前实例占用的内存字节长度。 它拥有一个静态方法isView(),这个方法可以用来判断是否为TypedArray实例或DataView实例。 它拥有实例方法 slice(),用来复制一部分内存,使用方式同数组的slice方法。 除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。 二、视图2.1 TypedArray TypedArray一共包含九种类型,每一种都是一个构造函数。(DataView视图不支持Uint8ClampedArray,其他都支持) 名称描述长度(字节)Int8Array8位有符号整数1Uint8Array8位无符号整数1Uint8ClampedArray8位无符号整型固定数组(数值在0~255之间)1Int16Array16位有符号整数2Uint16Array16位无符号整数2Int32Array32位有符号整数4Uint32Array32 位无符号整数4Float32Array32 位 IEEE 浮点数4Float64Array64 位 IEEE 浮点数8 每一种视图都有一个BYTES_PER_ELEMENT常数,表示这种数据类型占据的字节数。 Int8Array.BYTES_PER_ELEMENT // 1 Uint8Array.BYTES_PER_ELEMENT // 1 Int16Array.BYTES_PER_ELEMENT // 2 Uint16Array.BYTES_PER_ELEMENT // 2 Int32Array.BYTES_PER_ELEMENT // 4 Uint32Array.BYTES_PER_ELEMENT // 4 Float32Array.BYTES_PER_ELEMENT // 4 Float64Array.BYTES_PER_ELEMENT // 8 这 9 个构造函数生成的数组,统称为TypedArray视图。它们很像普通数组,都有length属性,普通数组的操作方法和属性,对TypedArray 数组完全适用。 普通数组与 TypedArray 数组的差异主要在以下方面: [图片] TypedArray和Array之间也可以互相转换 const typedArray = new Uint8Array([1, 2, 3, 4]); const normalArray = Array.apply([], typedArray); 「建立TypedArray视图」 // 创建一个8字节的ArrayBuffer const a = new ArrayBuffer(8); // 创建一个指向a的Int32视图,开始于字节0,直到缓冲区的末尾 const a1 = new Int32Array(a); // 创建一个指向a的Uint8视图,开始于字节4,直到缓冲区的末尾 const a2 = new Uint8Array(a, 4); // 创建一个指向a的Int16视图,开始于字节4,长度为2 const a3 = new Int16Array(a, 4, 2); 上面代码在一段长度为 8 个字节的内存(a)之上,生成了三个视图:a1、a2和a3。 视图的构造函数可以接受三个参数: 第一个参数(必选):视图对应的底层ArrayBuffer对象;第二个参数:视图开始的字节序号,默认从 0 开始;第三个参数:视图包含的数据个数,默认直到本段内存区域结束; 建立了视图以后,就可以进行各种操作了。这里需要明确的是,视图其实就是普通数组,语法完全没有什么不同,只不过它直接针对内存进行操作,而且每个成员都有确定的数据类型。所以,视图就被叫做“类型化数组”。 「TypedArray视图操作」 const buffer = new ArrayBuffer(8); const int16View = new Int16Array(buffer); for (let i = 0; i < int16View.length; i++) { int16View[i] = i * 2; } console.log(int16View) // [0, 2, 4, 6] 上面代码生成一个8字节的ArrayBuffer对象,然后在它的基础上,建立了一个16位整数的视图。由于每个字节占据8位,那么16位就占据了2个字节(1个字节等于8位),所以一共可以写入4个整数,依次为0,2,4,6。 如果在这段数据上接着建立一个8位整数的视图,则可以读出完全不一样的结果。 const int8View = new Int8Array(buffer); for (let i = 0; i < int8View.length; i++) { int8View[i] = i; } console.log(int8View) // [0, 0, 2, 0, 4, 0, 6, 0] 首先整个ArrayBuffer对象会被分成8段。然后,由于x86体系的计算机都采用小端字节序(具体概念理解请自主查询),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。还可以看到下面这个例子 const buffer = new ArrayBuffer(4); const v1 = new Uint8Array(buffer); v1[0] = 10; v1[1] = 3; v1[2] = 11; v1[3] = 8; console.log(v1) // [10, 3, 11, 8] const uInt16View = new Uint16Array(buffer); // [0xa, 0x3, 0xb, 0x8] console.log(uInt16View) // 计算机采用小端字节序 [0x030a, 0x080b] => [778, 2059] 如果一段数据是大端字节序(大端字节序主要用于数据传输),TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序。 2.2 DataView DataView 视图是一个可以从二进制 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。 ❝ 字节顺序,又称端序或尾序(英语:Endianness),在计算机科学领域中,指存储器中或在数字通信链路中,组成多字节的字的字节的排列顺序。字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。在网络应用中,字节序是一个必须被考虑的因素,因为不同机器类型可能采用不同标准的字节序,所以均按照网络标准转化。例如假设上述变量 x 类型为int,位于地址 0x100 处,它的值为 0x01234567,地址范围为 0x100~0x103字节,其内部排列顺序依赖于机器的类型。大端法从首位开始将是:0x100: 01, 0x101: 23,..。而小端法将是:0x100: 67, 0x101: 45,..。❝「语法」 new DataView(buffer [, byteOffset [, byteLength]]) 相关的参数说明如下: buffer:ArrayBuffer 对象 或 SharedArrayBuffer 对象;byteOffset(可选):此 DataView 对象的第一个字节在 buffer 中的字节偏移。如果未指定,则默认从第一个字节开始;异常:此 DataView 对象的字节长度。如果未指定,这个视图的长度将匹配 buffer 的长度;「示例」 const buffer = new ArrayBuffer(16); const view = new DataView(buffer, 0); view.setInt8(1, 68); view.getInt8(1); // 68 如果一次操作(get或者set)两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。DataView的操作方法默认使用大端字节序解读数据,如果需要使用小端字节序解读,必须在操作方法中指定参数为true(get方法的第二个参数和set方法的第三个参数)。 const buffer = new ArrayBuffer(24); const dv = new DataView(buffer); // 1个字节,默认大端字节序 const v1 = dv.getUint8(0); // 小端字节序 const v1 = dv.getUint16(1, true); // 大端字节序 const v2 = dv.getUint16(3, false); // 在第5个字节,以小端字节序写入值为11的32位整数 dv.setInt32(4, 11, true); 对于直接处理ArrayBuffer对象的业务场景不是特别多,特别是写页面比较多的同学。笔者深刻认识并运用的场景,主要是在处理比较复杂且数据量比较大的点云数据,前端接收到的点云数据已经是原始采集数据转换过的二进制数据,前端需要对二进制数据进行解析,运用的解析方法就是上述提到的各种方法。下面介绍一下业务场景中比较常见到的一种二进制表示类型——Blob。 三、Blob3.1 基本介绍 Blob 对象比较常用于文件上传、文件读写操作等。在对文件读写的时候,我们更多的时候只是操作File对象,而File继承了所有Blob的属性。所以在我们看来,File对象可以看作一种特殊的Blob对象。 而Blob 对象与 ArrayBuffer 的区别在于,Blob 对象用于操作二进制文件, ArrayBuffer 用于直接操作内存,所以他们有如下图的关系: [图片] 「语法」 const blob = new Blob(array [, options]); 相关的参数说明如下: array:字符串或二进制对象,表示新生成的Blob实例对象的内容;options(可选):比较常用的属性 type,表示数据的 MIME 类型,默认空字符串;「示例」 const array = ['Hello World! ']; const blob = new Blob(array, {type : 'text/html'}); 「属性和方法」 [图片] 由上图可以看到,Blob对象拥有size和type两个属性,以及多种自有方法。比较常用的方法slice、arrayBuffer等;slice方法主要用来拷贝原来的数据,返回的也是一个Blob实例,这个方法可以用来做切片上传。arrayBuffer方法返回一个 Promise 对象,包含 blob 中的数据,并在 ArrayBuffer 中以二进制数据的形式呈现。 const blob = new Blob([]); blob.slice(0, 1); blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/); 3.2 运用场景通过window.URL.createObjectURL方法可以把一个blob转化为一个Blob URL,并且用做文件下载或者图片显示的链接。 Blob URL所实现的下载或者显示等功能,仅仅可以在单个浏览器内部进行。而不能在服务器上进行存储,亦或者说它没有在服务器端存储的意义。 下面是一个Blob的例子,可以看到它很短 blob:d3958f5c-0777-0845-9dcf-2cb28783acaf 和冗长的Base64格式的Data URL相比,Blob URL的长度显然不能够存储足够的信息,这也就意味着它只是类似于一个浏览器内部的“引用“。从这个角度看,Blob URL是一个浏览器自行制定的一个伪协议。 「文件下载」 [图片] 「图片显示」 [图片] 「切片上传」 [图片] 「本地文件读取」 [图片]
2021-05-10 - 炫酷的wxss动画效果
因为没啥事,研究了下小程序的粒子动画,最后放弃了,实在是头大。去搞了一些花里胡哨的效果,没啥实际用处,就分享玩玩,看能不能提供一些其他灵感啥的。 [图片] 代码片段如下:https://developers.weixin.qq.com/s/VQwYjYm47dgH 使用wxss绘制烟花动画 [图片] https://developers.weixin.qq.com/s/xcJdoMmW7lh3 蜡烛逼真燃烧效果: [图片] https://developers.weixin.qq.com/s/Iom47XmO7rh5 螺旋旋转效果 [图片] https://developers.weixin.qq.com/s/1BnRTXmZ7Rhj 炫酷wxss粒子动画 [图片] https://developers.weixin.qq.com/s/cRpjQXmb7khN 水文章
2020-08-03 - component 使用 radio 原生组件修改样式无效?
Page 页面中可以修改,但是 component 中无法修改 radio .wx-radio-input{ /* 自定义样式.... */ height: 22rpx; width: 22rpx; margin-top: -8rpx; border-radius: 50%; border: 4rpx solid #000; background: transparent; } /* 选中后的 背景样式 (红色背景 无边框 可根据UI需求自己修改) */ radio .wx-radio-input.wx-radio-input-checked{ // border: none; } /* 选中后的 对勾样式 (白色对勾 可根据UI需求自己修改) */ radio .wx-radio-input.wx-radio-input-checked::before{ border-radius: 50%;/* 圆角 */ width: 8rpx; /* 选中后对勾大小,不要超过背景的尺寸 */ height: 8rpx; /* 选中后对勾大小,不要超过背景的尺寸 */ background: #000; transform:translate(-50%, -50%) scale(1); -webkit-transform:translate(-50%, -50%) scale(1); }
2020-07-30 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - Webpack核心原理
Webpack解决的问题 Webpack做的事情简单来说就一件事情打包,先看下边这段简单的代码 [图片] [图片] [图片] 这段代码不能在浏览器中直接运行,因为浏览器不支持运行带有import和export关键字的代码,现代浏览器可以通过<script type=module>来支持,但是ie浏览器不支持,当项目比较大的时候我们对于代码拆分为多个文件的需求有很多,所以我们对于这个问题急需要解决。 webpack就是提供给解决这个问题的一个方案:把关键字转译为普通代码,并把所有文件打包成一个文件。 babel原理 要将代码打包webpack就需要借助babel对代码进行解析、转译等等工作。 babel工作步骤 babel转译代码分为三个步骤: 解读代码生成ast树 遍历ast树修改树节点属性生成新的ast树 通过ast生成代码 简单案例 通过一个简单案例,来理解下babel转换的过程,这个例子是个简单的将let 转化为var的过程: [代码]import { parse } from "@babel/core"; import traverse from "@babel/traverse"; import generate from "@babel/generator"; const code = `let a = 'let'; let b = 2`; const ast = parse(code, { sourceType: "module" }); if (ast) { traverse(ast, { enter: (item) => { if (item.node.type === 'VariableDeclaration') { if (item.node.kind === "let") { item.node.kind = "var"; } } }, }); const result = generate(ast, {}, code); console.log(result.code); } [代码] 先使用parse库将代码转换为ast树,然后通过traverse库遍历ast树,将所有类型声明语句中的类型从var改成let,最终结果为: [图片] babel库 import的几个库就是对应babel的转换过程: 生成ast树 @babel/core 遍历ast树 @babel/traverse ast树生成代码 @babel/generator AST AST是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,将上边的ast对象打印出来格式如下: [图片] ast是一个对象格式,program就是对代码做的整体抽象,例子中有两条声明语句,所以这里body数组中就是两个节点对象。 [图片] type属性表示这个语句的类型,例子中是个变量声明,kind表示这个语句用的是var声明变量。declarations语句由是一个节点数组,因为一个变量声明语句可以声明多个变量,declarations数组中的节点结构如下图: [图片] es6转es5案例 一个频繁使用babel的场景就是将js的新特性转换成低版本的js代码,要将新版本js转换成低版本js需要使用@babel/preset-env库,告诉babel新老版本js之间的转换关系。 [代码]import * as fs from "fs"; import { parse } from "@babel/core"; import * as babel from "@babel/core"; const code = fs.readFileSync("./test.js").toString(); const ast = parse(code, { sourceType: "module" }); if (ast) { const result = babel.transformFromAstSync(ast, code, { presets: ["@babel/preset-env"], }); if (result?.code) { fs.writeFileSync("./test.es5.js", result.code); } } [代码] test.js原代码: [代码]let a = "let"; let b = 2; const c = 3; function sum() { let a = 1, b = 1; return a + b; } [代码] 转义后的代码: [代码]"use strict"; var a = "let"; var b = 2; var c = 3; function sum() { var a = 1, b = 1; return a + b; } [代码] 打包文件 处理文件依赖关系 案例 通过一个简单的例子来了解webpack是怎么将几个文件的代码打包成一个文件。 代码结构 project_1/index.js [代码]import a from "./a.js"; import b from "./b.js"; console.log(a.value + b.value); [代码] project_1/a.js [代码]const a = { value: 1, } export default a [代码] project_1/b.js [代码]const b = { value: 2, } export default b [代码] 打包文件代码 [代码]import * as babel from "@babel/core"; import { parse } from "@babel/parser"; import traverse from "@babel/traverse"; import { readFileSync, writeFileSync } from "fs"; import { resolve, relative, dirname } from "path"; // 设置根目录 const projectRoot = resolve(__dirname, "project_2"); interface Dep { key: string; deps: string[]; code: string; } // 类型声明 type DepRelation = Dep[]; // 初始化一个空的 depRelation,用于收集依赖 const depRelation: DepRelation = []; // 数组! // 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js collectCodeAndDeps(resolve(projectRoot, "index.js")); console.log(depRelation); console.log("done"); /** * * @param filepath 文件绝对地址 */ function collectCodeAndDeps(filepath: string) { const key = getProjectPath(filepath); // 文件的项目路径,如 index.js if (depRelation.find((i) => i.key === key)) { // 注意,重复依赖不一定是循环依赖 return; } // 获取文件内容,将内容放至 depRelation const code = readFileSync(filepath).toString(); // 初始化 depRelation[key] // 将代码转为 AST const transformCode = babel.transform(code, { presets: ["@babel/preset-env"], }); const es5Code = transformCode?.code; if (!es5Code) { return; } const item: Dep = { key, deps: [], code: es5Code }; depRelation.push(item); const ast = parse(code, { sourceType: "module" }); // 分析文件依赖,将内容放至 depRelation traverse(ast, { enter: (path) => { if (path.node.type === "ImportDeclaration") { // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径 const depAbsolutePath = resolve( dirname(filepath), path.node.source.value ); // 然后转为项目路径 const depProjectPath = getProjectPath(depAbsolutePath); // 把依赖写进 depRelation item.deps.push(depProjectPath); collectCodeAndDeps(depAbsolutePath); } }, }); } // 获取文件相对于根目录的相对路径 function getProjectPath(path: string) { return relative(projectRoot, path).replace(/\\/g, "/"); } [代码] 代码思路 depRelation是依赖数组,存储所有的文件依赖,第一个元素是入口文件的依赖,depRelation数据项格式{ deps: [‘依赖的文件路径’], code: ‘文件的源码’ } collectCodeAndDeps是处理文件依赖的主函数,主要步骤是: 判断文件路径是否已经存在依赖数组中,如果已经存在不再重复处理 根据文件路径读取源代码 将源代码准换ast树 通过@babel/preset-env将代码转义然后存在依赖数据项的code属性中 遍历ast树,找到import语句,存入deps属性中,然后取出import路径,递归调用collectCodeAndDeps 打包后代码执行 最终需要通过生成的依赖关系数组对象,生成可以执行的代码,生成最终代码的函数如下: [代码]function generateCode() { let code = ""; code += "var depRelation = [" + depRelation .map((item) => { const { key, code, deps } = item; return `{ key: ${JSON.stringify(key)}, deps: ${JSON.stringify(deps)}, code: function(require, module, exports){ ${code} } }`; }) .join(",") + "];\n"; code += "var modules = {};\n"; code += `execute(depRelation[0].key)\n`; code += ` function execute(key) { if (modules[key]) { return modules[key] } var item = depRelation.find(i => i.key === key) if (!item) { throw new Error(\`\${item} is not found\`) } var pathToKey = (path) => { var dirname = key.substring(0, key.lastIndexOf('/') + 1) var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/') return projectPath } var require = (path) => { return execute(pathToKey(path)) } modules[key] = { __esModule: true } var module = { exports: modules[key] } item.code(require, module, module.exports) return modules[key] } `; return code; } [代码] 通过依赖关系数组拼装依赖对象,将code替换为方法 使用modules对象存储各个文件export出去的内容 自定义require函数替换每个文件中require方法,用modules对象替换每个文件的export方法对象 pathToKey方法处理"./"这种import 最终代码 [代码]import * as babel from "@babel/core"; import { parse } from "@babel/parser"; import traverse from "@babel/traverse"; import { readFileSync, writeFileSync } from "fs"; import { resolve, relative, dirname } from "path"; // 设置根目录 const projectRoot = resolve(__dirname, "project_2"); interface Dep { key: string; deps: string[]; code: string; } // 类型声明 type DepRelation = Dep[]; // 初始化一个空的 depRelation,用于收集依赖 const depRelation: DepRelation = []; // 数组! // 将入口文件的绝对路径传入函数,如 D:\demo\fixture_1\index.js collectCodeAndDeps(resolve(projectRoot, "index.js")); function generateCode() { let code = ""; code += "var depRelation = [" + depRelation .map((item) => { const { key, code, deps } = item; return `{ key: ${JSON.stringify(key)}, deps: ${JSON.stringify(deps)}, code: function(require, module, exports){ ${code} } }`; }) .join(",") + "];\n"; code += "var modules = {};\n"; code += `execute(depRelation[0].key)\n`; code += ` function execute(key) { if (modules[key]) { return modules[key] } var item = depRelation.find(i => i.key === key) if (!item) { throw new Error(\`\${item} is not found\`) } var pathToKey = (path) => { var dirname = key.substring(0, key.lastIndexOf('/') + 1) var projectPath = (dirname + path).replace(\/\\.\\\/\/g, '').replace(\/\\\/\\\/\/, '/') return projectPath } var require = (path) => { return execute(pathToKey(path)) } modules[key] = { __esModule: true } var module = { exports: modules[key] } item.code(require, module, module.exports) return modules[key] } `; return code; } writeFileSync("dist_2.js", generateCode()); console.log(depRelation); console.log("done"); /** * * @param filepath 文件绝对地址 */ function collectCodeAndDeps(filepath: string) { const key = getProjectPath(filepath); // 文件的项目路径,如 index.js if (depRelation.find((i) => i.key === key)) { // 注意,重复依赖不一定是循环依赖 return; } // 获取文件内容,将内容放至 depRelation const code = readFileSync(filepath).toString(); // 初始化 depRelation[key] // 将代码转为 AST const transformCode = babel.transform(code, { presets: ["@babel/preset-env"], }); const es5Code = transformCode?.code; if (!es5Code) { return; } const item: Dep = { key, deps: [], code: es5Code }; depRelation.push(item); const ast = parse(code, { sourceType: "module" }); // 分析文件依赖,将内容放至 depRelation traverse(ast, { enter: (path) => { if (path.node.type === "ImportDeclaration") { // path.node.source.value 往往是一个相对路径,如 ./a.js,需要先把它转为一个绝对路径 const depAbsolutePath = resolve( dirname(filepath), path.node.source.value ); // 然后转为项目路径 const depProjectPath = getProjectPath(depAbsolutePath); // 把依赖写进 depRelation item.deps.push(depProjectPath); collectCodeAndDeps(depAbsolutePath); } }, }); } // 获取文件相对于根目录的相对路径 function getProjectPath(path: string) { return relative(projectRoot, path).replace(/\\/g, "/"); } [代码]
2021-01-20 - 一个RequestTask.abort()引发的悲剧
背景介绍 我司有一款健康记录的微信小程序产品,为了程序的健壮性,前辈开发者们在产品开发初期就引入了[代码]wx.request[代码]响应超时,自动重发的逻辑。本月产品迎来重大迭代,用户量开始增加。sentry中突然有一个错误日志断断续续出现,日志显示前端收到了接口的response,但是response中没有数据。而后端确认接口是有数据的,多么诡异的问题,唯一有迹可循的是,接口报错时,是接口超时没响应后重发的第二次请求 [图片] 开始排查 作为公司唯一的实习生,我荣获了周末加班排查这个bug的殊荣,打开我的大宝剑,错了,打开我的macbook,打开IDE,我发现了前辈们在接口重发逻辑中写的一个我不认识的东西 [代码]this.requestTask.abort() [代码] [图片] 马上打开官方文档查看 [图片] 文档让我心里慌的一比,没办法,只能自己去尝试 [图片] 举个栗子🌰 为了胜利我把重发的核心逻辑剥离出来放在一个单页面的小程序中,大概就是这样👇 [代码] data: { requestTask: null, retryRequest: null, retryCount: 0, timeout: 50 // 为了测试我把接口超时的时间改成了50ms }, test: function() { if(this.retryCount>3) { clearTimeout(this.retryRequest) return false } this.retryRequest = setTimeout(() => { this.retryCount++ this.requestTask.abort() // 中止上一个请求 this.test() // 调用自己发起下一次请求 }, 50) this.requestTask = wx.request({ url: 'api', success: (res) => { console.log('in success:', res) clearTimeout(this.retryRequest) }, fail: (res) => { console.log('in fail:', res) } }) } [代码] RequestTask.abort()分析 来,请大家猜猜下面红色的error是代码错误了还是中断成功了? [图片] 恭喜,这是请求中止成功啦。鼓掌!!! abort()函数执行成功(请求被中止),会进入fail回调和complete回调,如果errMsg == “request:fail abort”,就表示之前的请求被中止了。至于为什么会有红色的报错(没有任何意义),这是爱的鼓励,不要问为什么。 [图片] 重现问题 我一遍又一遍的点击着小程序开发者工具的编译按钮,发现在我点击十次之内,一定会出现重发多个请求后,有一个请求得到响应,其他请求被取消,而在success中打印res,发现里面的data不见了,就是下面这样👇 [图片] 推测一下 [图片] abort()函数是已经封装好的函数,是一个异步的函数 原先的逻辑中,abort被执行,但是并不知道请求是否已经彻底终止,就发起了下一个请求 [代码]this.requestTask.abort() this.test() [代码] 就在终止请求的时,下一个请求响应,底层开始处理请求的响应,上一次的终止逻辑被停了。最后,上一个来不及终止的请求也得到了响应,各种巧合导致其中一个得到响应的请求,success回调中打印不出来data 改动一下 在定时器setTimeout中执行requestTask.abort(),在fail的回调中,只有判断出res.errMsg === 'request:fail abort’的情况下,表示上一个请求已经彻底终止。重发下一次请求 [代码] test: function() { this.retryRequest = setTimeout(() => { this.requestTask.abort() }, 50) this.requestTask = wx.request({ url: 'api', success: (res) => { console.log(res) clearTimeout(this.retryRequest) }, fail: (res) => { if (res.errMsg === 'request:fail abort') this.test() }, complete: function (res) {}, }) } [代码] 鸣谢 [图片]
2019-10-28