- 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 手把手教你避开组件cover-view的那些坑
案例背景: 最近在开发城市地铁图项目,具体功能有规划路线、定位最近地铁站、以及显示整个城市的地铁网状图等功能。根据需求,在实现的时候在地铁线路图上需要添加定位按钮及线路弹框来展示位置信息以及地铁站详情信息。 遇到的问题: 在地铁图调研初期,原计划实现渲染方案是采用svg来绘制,但是调研后发现小程序原生API不支持svg。同时,我们在开源中找到一个svg的框架库来实现绘制,但是开发初期发现遇到很多无法实现的需求和性能问题。在对开源库的代码跟踪后,发现绘制方案也是canvas的方式,于是我们决定使用原生canvas的方案来支持地铁图。但是呢,又遇到一些问题,那么我们来看看几个具体的点: 1) view在canvas上无法正常显示。 在canvas上使用view来添加图片和弹框时,发现图片以及弹框在canvas的下面,不能正常显示图片。 查看文档发现canvas、map、video等原生组件使用的是native实现的,默认显示在小程序的最上层,所以就把view换成cover-view或者cover-image。 使用view效果: [代码]<!-- 线路 -->[代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"sublines sublines-icon"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]image[代码] [代码]class[代码][代码]=[代码][代码]'sublinesIcon'[代码] [代码]src[代码][代码]=[代码][代码]"/static/img/ic_sublines.png"[代码][代码] [代码][代码]bindtap[代码][代码]=[代码][代码]'clickSublines'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{lineIconShow}}"[代码][代码]></[代码][代码]image[代码][代码]> [代码][代码]</[代码][代码]view[代码][代码]>[代码] [图片] 替换成cover-view效果: [代码]<!-- 线路 -->[代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"sublines sublines-icon"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-image[代码] [代码]class[代码][代码]=[代码][代码]'sublinesIcon'[代码] [代码]src[代码][代码]=[代码][代码]"/static/img/ic_sublines.png"[代码][代码] [代码][代码]bindtap[代码][代码]=[代码][代码]'clickSublines'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{lineIconShow}}"[代码][代码]></[代码][代码]cover-image[代码][代码]> [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码] [图片] 但是使用cover-view又遇到了层级和样式的问题。 2)canvas上使用cover-image添加图片,图片设置position:absolute;页面上的图片显示在canvas画线的下方,导致定位按钮不能正常使用。后来把position该换成fixed解决来层级的问题。效果如下所示: [代码].locationIcon {[代码][代码] [代码][代码]width: 3rem;[代码][代码] [代码][代码]height: 3rem;[代码][代码] [代码][代码]position: fixed;[代码][代码] [代码][代码]bottom: 3rem;[代码][代码] [代码][代码]left: 0.7rem;[代码][代码]}[代码][图片] 3)在页面上实现一个弹框时,根据UI图需要实现一个底边线和底边小三角形。通过border给块级元素设置底边线或者css实现三角箭头,单边border设置无效。最终采用了height为1px的cover-view或者图片来代替。 设置单边border效果: [代码]<!-- 起终点设置弹框 -->[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"sdMark"[代码] [代码]style[代码][代码]=[代码][代码]'top:{{tapClient.y}}px;left:{{tapClient.x}}px;'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{sdMarkShow}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkContent'[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStart'[代码][代码]>设为起点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickEnd'[代码][代码]>设为终点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStationDetail'[代码][代码]>站点详情</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码] [图片] 修改后的代码: [代码]<!-- 起终点设置弹框 -->[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"sdMark"[代码] [代码]style[代码][代码]=[代码][代码]'top:{{tapClient.y}}px;left:{{tapClient.x}}px;'[代码] [代码]wx-if[代码][代码]=[代码][代码]"{{sdMarkShow}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkContent'[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStart'[代码][代码]>设为起点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'line'[代码][代码]></[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickEnd'[代码][代码]>设为终点</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'line'[代码][代码]></[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]'sdMarkItem'[代码] [代码]bindtap[代码][代码]=[代码][代码]'clickStationDetail'[代码][代码]>站点详情</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-view[代码] [代码]class[代码][代码]=[代码][代码]"icon"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]cover-image[代码] [代码]class[代码][代码]=[代码][代码]'icArrow'[代码] [代码]src[代码][代码]=[代码][代码]'/static/img/ic_arrow.png'[代码][代码]></[代码][代码]cover-image[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码][代码] [代码][代码]</[代码][代码]cover-view[代码][代码]>[代码] 最终的效果: [图片] 踩坑总结:canvas层级较高,使用cover-view或者cover-image在canvas做操作。单边border相关的操作使用图片或者块级元素来代替。 查看相关API文档: cover-view相关文档:https://developers.weixin.qq.com/miniprogram/dev/component/cover-view.html 欢迎体验和吐槽:"腾讯位置服务-地铁图"插件:https://developers.weixin.qq.com/community/servicemarket/sq_ocVuQ4joORnwn_bzpxTd8Mq8wZ3g/service/detail/0008cc3058c5c8042dc89d7db54415
2019-07-17 - 小程序录音实时波形图
首先,做这个不需要把MP3转pcm。 结果就是,转了pcm也不知道怎么出波形。搞了我好几天。。。 但是微信现在不需要引入js-mp3库就可以转,仅仅记录一下,代码如下: var audioCtx = wx.createWebAudioContext() audioCtx.decodeAudioData(frameBuffer, audioBuffer => { let float32Array = audioBuffer.getChannelData(0) ...} AudioContext.createAnalyser(),这是浏览器的接口,微信的WebAudioContext暂时没有这个。 页面js: const that = this; var recorderManager = wx.getRecorderManager() //把frameBuffer转一下,再转成普通Array,传给voice组件 recorderManager.onFrameRecorded((res) => { const { frameBuffer } = res let uint8Array = new Uint8Array(frameBuffer) that.setData({ voiceLine: new Array(...uint8Array) }) }) const options = { duration: 40000, numberOfChannels: 1, format: 'mp3', frameSize: 0.01, //这里设置很小,就会只取一帧就触发onFrameRecorded事件 // sampleRate: 44100, encodeBitRate: 16000, } recorderManager.start(options) 页面wxml:引入组件,传递数据 组件voice.wxml: 组件voice.wxss: .canvas { position: fixed; top: 0; left: 10px; right: 60; bottom: 0; width: 300px; height: 100%; } 组件js: var voiceLine = []; var MaxValue = 0; Component({ /** * 组件的属性列表 */ properties: { voice: Array }, /** * 组件的初始数据 */ data: { }, lifetimes: { // 组件刚刚被创建时执行 attached() { wx.createSelectorQuery().in(this) .select('#canvas') .fields({ node: true, size: true, }) .exec(this.init.bind(this)) }, //删除该组件绑定的所有事件 detached() { } }, pageLifetimes: { show: function () { }, hide: function () { // 页面被隐藏 }, resize: function (size) { // 页面尺寸变化 } }, observers: { 'voice': function (voice) { if (!voice instanceof Array || voice.length === 0) { return; } // 显示单一波形,调试用 //解开注释可以查看每个波形,慢慢找规律 // this.renderVoice(voice); // return; let voiceLen = voice.length, plantValue = 0, //常见数据 serialValueNum = 0 //连续计数器 for (var i = 0; i < voiceLen; i++) { //连续9个表示该帧空了 if (serialValueNum > 9) { console.log('总长:', voiceLen, '常见数据:', plantValue, '干了:', serialValueNum) if (voiceLine.length > 60) { voiceLine.shift() } voiceLine.push(0) this.renderCanvas(); return; } if (plantValue != voice[i]) { plantValue = voice[i] serialValueNum = 0 } else { serialValueNum++; } } // 按长度显示 let middleNum = voice.length if (middleNum > MaxValue) { MaxValue = middleNum console.log('MaxValue:', MaxValue); } //长度<100抛弃数据 if (middleNum < 100) { console.log('长度<100抛弃数据:', middleNum, voice); middleNum = 100 } if (voiceLine.length > 60) { voiceLine.shift() } voiceLine.push(middleNum) this.renderCanvas(); }, }, /** * 组件的方法列表 */ methods: { init(res) { const width = res[0].width const height = res[0].height const canvas = res[0].node const ctx = canvas.getContext('2d') const dpr = wx.getSystemInfoSync().pixelRatio canvas.width = width * dpr canvas.height = height * dpr ctx.scale(dpr, dpr) this.ctx = ctx this.width = width this.height = height }, renderVoice(voiceLine) { if (typeof this.ctx === 'undefined') { return } const width = this.width const height = this.height const ctx = this.ctx ctx.clearRect(0, 0, width, height) var len = voiceLine.length, barHeight = 1, q = 0, left = 0 let plantValue = 0, //常见数据 serialValueNum = 0 //连续计数器 console.log(voiceLine); ctx.strokeStyle = 'blue' for (var i = 0; i < len; i++) { if (i == len - 1 && serialValueNum > 9) { //连续5个很罕见 console.log('总长:', len, '常见数据:', plantValue, '干了:', serialValueNum) } if (plantValue != voiceLine[i]) { plantValue = voiceLine[i] serialValueNum = 0 } else { serialValueNum++; } q = i % height left = parseInt(i / height) * 60 + 60 barHeight = (voiceLine[i] / 255) * 60 // 绘制向上的线条 ctx.beginPath(); ctx.moveTo(left, q); ctx.lineTo(left + barHeight, q); ctx.stroke(); } }, renderCanvas() { if (typeof this.ctx === 'undefined') { return } const width = this.width const height = this.height const ctx = this.ctx ctx.clearRect(0, 0, width, height) var len = voiceLine.length, barHeight = 1, t_arr = [] let now_V, min = 100, max = MaxValue - min; ctx.strokeStyle = 'blue' ctx.lineWidth = 3; for (var i = 0; i < len; i++) { now_V = voiceLine[i] == 0 ? 0 : voiceLine[i] - min if (now_V == 0) { ctx.beginPath(); ctx.moveTo(3, i * 8 - 4); ctx.lineTo(3, i * 8 + 4); ctx.stroke(); continue; } t_arr.push(now_V) barHeight = (now_V / max) * 60 // 绘制向上的线条 ctx.beginPath(); ctx.moveTo(0, i * 8); ctx.lineTo(barHeight, i * 8); ctx.stroke(); } t_arr.sort((a, b) => a - b); console.log('Min', t_arr[0], 'Max', t_arr[t_arr.length - 1]) } } }) 原理:说白了,传过来的一串数据,当有大声音时,会突然变长。 当完全静音时,会连续出现85或170这个值。如果是转Int8Array,会有-86这个值。 至于为什么,上面代码解开注释可以观察单个波形。 [图片] 最后成品的案例在:“艺匠人”小程序->新建作品。 感兴趣的童鞋搜一下玩玩吧~
2023-02-07 - cover-view实现input标签功能
html代码: [代码]<cover-view class='items'> <cover-view class='name'>设备名</cover-view> <cover-view class='display' bindtap='inputNameState'> {{nameInfo}} </cover-view> <cover-view class='border'></cover-view> <input type="text" focus="{{nameTrue}}" bindinput="inputName"></input> </cover-view> [代码] js代码: [代码]inputNameState(){ this.setData({ nameTrue: true }) }, inputName(e){ this.setData({ nameInfo: e.detail.value }) } [代码] 实际效果 [图片]
2019-05-13 - 微信小程序用mqtt.js连阿里物联网设备,为什么老是断开,小程序切到后台后马上就跟物联网设备断开?
微信小程序使用mqtt.js连阿里物联网设备,为什么连接上物联网设备后不定时的断开连接,另外小程序切换到后台后就会马上跟物联网设备断开连接,主要代码如下(劳烦各位大佬解惑): import mqtt from'../../utils/mqtt.js'; const aliyunOpt = require('../../utils/aliyun/aliyun_connect.js'); let that = null; Page({ data:{ client:null,//记录重连的次数 reconnectCounts:0,//MQTT连接的配置 options:{ protocolVersion: 4, //MQTT连接协议版本 clean: false, reconnectPeriod: 1000, //1000毫秒,两次重新连接之间的间隔 connectTimeout: 30 * 1000, //1000毫秒,两次重新连接之间的间隔 resubscribe: true, //如果连接断开并重新连接,则会再次自动订阅已订阅的主题(默认true) clientId: '', password: '', username: '', }, aliyunInfo: { productKey: '**********', //阿里云连接的三元组 ,请自己替代为自己的产品信息!! deviceName: '**********', //阿里云连接的三元组 ,请自己替代为自己的产品信息!! deviceSecret: '****************', //阿里云连接的三元组 ,请自己替代为自己的产品信息!! regionId: 'cn-shanghai', //阿里云连接的三元组 ,请自己替代为自己的产品信息!! pubTopic: '/*********/**********/thing/event/property/post', //发布消息的主题 subTopic: '/*********/**********/thing/service/property/set', //订阅消息的主题 }, }, onLoad:function(){ that = this; let clientOpt = aliyunOpt.getAliyunIotMqttClient({ productKey: that.data.aliyunInfo.productKey, deviceName: that.data.aliyunInfo.deviceName, deviceSecret: that.data.aliyunInfo.deviceSecret, regionId: that.data.aliyunInfo.regionId, port: that.data.aliyunInfo.port, }); console.log("get data:" + JSON.stringify(clientOpt)); let host = 'wxs://' + clientOpt.host; console.log("get data:" + JSON.stringify(clientOpt)); this.setData({ 'options.clientId': clientOpt.clientId, 'options.password': clientOpt.password, 'options.username': clientOpt.username, }) console.log("this.data.options host:" + host); console.log("this.data.options data:" + JSON.stringify(this.data.options)); this.data.client = mqtt.connect(host, this.data.options); this.data.client.on('connect', function (connack) { wx.showToast({ title: '连接成功' }) }) that.data.client.on("message", function (topic, payload) { console.log(" 收到 topic:" + topic + " , payload :" + payload) wx.showModal({ content: " 收到topic:[" + topic + "], payload :[" + payload + "]", showCancel: false, }); }) //服务器连接异常的回调 that.data.client.on("error", function (error) { console.log(" 服务器 error 的回调" + error) }) //服务器重连连接异常的回调 that.data.client.on("reconnect", function () { console.log(" 服务器 reconnect的回调") }) //服务器连接异常的回调 that.data.client.on("offline", function (errr) { console.log(" 服务器offline的回调") }) },
2022-07-21 - mqtt协议,经常掉线重新连接,有什么方法能维持连接状态?
小程序无论前台还是后台,经常会掉线并且重新连接,而且经常重新连接失败,怎么才能维持长时间连接,不掉线
2022-04-02 - 微信小程序中关于mqtt在小程序切后台后长连接强制断开后重连的问题?
在使用mqtt.js客户端连接阿里云物联网平台后,当小程序在前台(或者息屏)时基本都正常。一旦小程序切刀后台,5秒后微信将断开小程序的长连接。当小程序再回到前台时,mqtt的客户端无法自动重连(有时候会耗时很长可能重连成功),这样的话体验就很不好。不知道大家是如何做的?
2020-06-09 - 微信小程序用EMQ X实现mqtt通讯
写在前面:我使用的是Window Server 2012 R2服务器系统,所有网站都是在IIS下面跑,突然接触到微信小程序智能硬件通讯这一块,就得用到wss访问,如何去配置后端响应的服务呢?其实很简单。如何让微信小程序使用wss://www.abc.com/mqtt,这里必须要配置一个响应的服务端,这里要用到EMQ X了。 1.下载EMQ X,访问以下地址,选择下载相应的版本 下载地址:https://www.emqx.cn/downloads#broker [图片] 2.下载完毕后,解压缩包 [图片] 3.在当前文件夹运行,按住shift,鼠标右键,在此处打开命令窗口,再去根据以下指令去操作,输入指令后回车。 [图片] emqx start #开启EMQX服务 emqx stop 关闭EMQX服务 3.登录EMQ后台 访问:http://www.4jll.com:18083/#/websocket 这里域名替换为你自己的域名,后台默认登录名:admin,密码:public,在里面可以修改用户密码。 [图片] 4.找到工具-》Websocket [图片] 主机地址替换成你自己的域名,默认情况下,连接是以ws连接,端口为8083。 连接成功后,可以订阅主题,2个设备同时订阅一个主题,就能实现2个设备之间的通讯。 [图片] 如何开启SSL连接,使用wss访问呢?要开启SSL,需要绑定SSL证书,下面讲一下,如何配置SSL,端口为8084,我这里显示连接成功了,请看我下面操作。 [图片] 用你自己的域名SSL证书文件,直接替换目录下的cert.pem,key.pem文件,就能和我一样使用wss访问8084端口的mqtt服务了。 [图片] 默认从域名服务商下载的SSL证书为,cert.pem,和key.key格式,我们要使用转换工具把.key转换成.pem,当时我就卡壳在这里了,后来找到了最简单的转换办法。 在线转换地址为:https://www.myssl.cn/tools/merge-pem-cert.html [图片] 输入打开.key文件,复制粘贴进去,然后合成PEM文件,换完成后,下载.pem格式文件,按我上面说的的方法直接对应去替换,就能开启wss访问了,目前wss访问的是8084端口。 如何用443端口去访问wss,请看我的上一篇贴子,上面有详细的讲解过程。
2021-03-15