- 借助云开发实现小程序模版消息推送(不用搭建服务器就可以实现微信消息推送)
上一节给大家将了借助云开发实现小程序支付功能,那么我们就要想了,能不能借助云开发实现小程序消息推送功能呢? 还别说,云开发还真能实现推送的功能。 一直关注我的同学肯定知道老师之前也写过借助java后台实现小程序消息推送的文章。 我们借助java后台虽然也能轻松的实现消息推送。但是呢?用java开发后台推送,必须要搭建服务器,学习java代码,部署java代码当然你就是做java开发的,或者学习过java,这没什么。 但是作为小程序开发人员来说,用java显得太重了。 传送门: 《借助小程序云开发实现小程序支付功能(含源码)》 《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 下面就来教大家如何借助云开发实现小程序模版消息的推送功能。 老规矩,先看效果图 [图片] 下面来讲实现步骤 一,定义推送的云函数 由于我们的云推送功能只能在云函数里调用,所以我们这里必须要在云函数里实现推送功能。 1,首先我们定义一个云函数push0524。 如果你还不知道如何使用云开发,如何定义云函数,去翻下老师之前的文章。有写的。 [图片] 把完整的代码贴给大家 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init() // 云函数入口函数 exports.main = async(event, context) => { console.log(event) return sendTemplateMessage(event) } //小程序模版消息推送 async function sendTemplateMessage(event) { const { OPENID } = cloud.getWXContext() // 接下来将新增模板、发送模板消息、然后删除模板 // 注意:新增模板然后再删除并不是建议的做法,此处只是为了演示,模板 ID 应在添加后保存起来后续使用 const addResult = await cloud.openapi.templateMessage.addTemplate({ id: 'AT0002', keywordIdList: [3, 4, 5] }) const templateId = addResult.templateId //新增的模版id const sendResult = await cloud.openapi.templateMessage.send({ touser: OPENID, templateId, formId: event.formId, page: 'pages/index/index', data: { keyword1: { value: '云开发实现推送', }, keyword2: { value: '2019 年 5 月 24 日', }, keyword3: { value: '编程小石头', }, } }) //删除模版id await cloud.openapi.templateMessage.deleteTemplate({ templateId, }) return sendResult } [代码] 上面代码所实现的就是 1,创建模版,拿到模版id 2,使用模版ID,填充模版消息,发送模版 3,删除模版。 我们正常开发时,模版都是在小程序后台获取到的。这里是为例演示方便。所以正常开发时,只需要实现第二步就行了。 推送的关键代码就是这个方法: cloud.openapi.templateMessage.send 通常我们定义完push0524云函数以后,如果直接调用的话,会报错误的。 [图片] 来看下这个错误,看到红色框里的permission就知道,肯定是权限的问题。所以我们在定义完云函数以后,要在push0524云函数下面添加权限配置页面。如下图 [图片] 重要的就是这个: “templateMessage.send”, 推送权限。因为推送是云开发给我们提供的,我们这里调用时,必须配置相关权限,才能使用的。 到这里我们的推送功能就实现了。下面我们来验证下。 二,验证云开发推送 验证其实很简单,和我们之前的《5行代码实现微信小程序模版消息推送 (含推送后台和小程序源码)》 类似。只不过一个是在java后台推送,一个是在小城里推送。下面我们简单写个小程序里验证推送的demo。 功能很简单 1,获取formid,因为推送必须有formid的 2,点击调用push0524实现推送 [图片] 简单的贴下代码 [图片] [图片] 需要注意的一点:我们测试时,必须要真机测试。因为模拟器没法获取到formid的。 [图片] 我们在推送成功的success回调中打印下log。如果log中出现,send:ok字样,就代表我们推送成功了。来看下推送成功的效果。 微信聊天列表接收到了消息提醒 [图片] 消息内容 [图片] 到这里我们就用云开发实现完整的消息推送功能了。是不是很简单。 有任何关于编程的问题都可以加老师微信 2501902696(备注小程序)也可以找老师索要完整源码。 编程小石头码农一枚,非著名全栈开发人员。分享自己的一些经验,学习心得,希望后来人少走弯路,少填坑 视频讲解地址:https://edu.csdn.net/course/detail/24770
2019-06-11 - mpvue开发音频类小程序踩坑和建议
前言 这是我第一次开发小程序,开发的产品是音频类的,在大佬的建议下采用了[代码]mpvue[代码],一周时间把功能都做出来,由于不太熟悉mpvue和微信小程序,足足用了一周时间来改bug才出来一个能用的版本,在这里整理分享下我开发时遇到的一些问题和给出一些建议。 [图片] 在[代码]Linux[代码]上开发小程序 在公司电脑装了双系统,日常用的是[代码]Ubuntu[代码]系统,Linux或Mac的开发环境对前端相对来说会友好一些。微信小程序官方的开发者工具只有[代码]Windows[代码]和[代码]Mac[代码]版本,所以这就尴尬了。 不过还好,发现已经有大神在GitHub上做了Linux的支持,推荐给大家:Linux微信web开发者工具。 根据教程安装使用即可,使用时就用[代码]./bin/wxdt[代码]命令打开。不过用了几天后面觉得不太方便,就索性切回Windows系统用官方最新的版本了。 封装wx.request为Promise [代码]wx.request[代码]用于发起http请求,但平时习惯了Promise的写法,所以还是封装一下这个方法为Promise的形式。 我看很多小程序会使用fly这个库。 但个人觉得发起请求不需要那么强大的功能,小程序本身就应该是一个轻量级的东西,引入一个库可能会导致项目打包变大,可能让小程序更卡,所以本着能自己写就自己写吧的心态,索性自己封装一下算了。 在[代码]src/utils[代码],新建一个[代码]request.js[代码]: [代码]const apiUrl = 'https://your server.com/api/' const request = (apiName, reqData, isShowLoading = true) => { // 某些请求可能不需要显示loading if (isShowLoading) { wx.showLoading({ title: '正在努力加载中', mask: true }) } return new Promise(function (resolve, reject) { wx.request({ url: apiUrl + apiName, method: 'POST', data: reqData, header: { 'content-type': 'application/json' // 默认值 }, success (res) { if (res.data.code === 0) { // 与后端约定code=0时才是正常的 resolve(res) } else { reject(res) } }, fail (err) { reject(err) }, complete (res) { wx.hideLoading() } }) }) } export default request [代码] 当然这是个简化版的,我实际项目中还会在初始化时加入一些[代码]token[代码]之类的参数,大家能看明白是这样封装成Promise的就可以啦。 使用vant-weapp 小程序已经支持了npm安装,但不太会弄。还是按网上方法,将项目clone下来放进static目录下。 [代码]git clone https://github.com/youzan/vant-weapp.git [代码] 然后将[代码]vant-weapp[代码]的[代码]dist[代码]目录拷贝到项目的static目录下(尽可能精简,删掉一些奇奇怪怪的如[代码].github[代码]的东西,所以直接使用dist目录),改名为[代码]vant[代码](也可以不改名)。全局使用时,可以在[代码]app.json[代码]引入: [代码] "usingComponents": { "van-button": "/static/vant/button/index", "van-field": "/static/vant/field/index" }, [代码] 注意:需要打开微信开发者工具中的ES6转ES5功能 一开始以为使用起来和web端的没啥差别,但没想到那么麻烦。比如:在vue中是可以使用[代码]v-model[代码]的,但在mpvue中的小程序中不能使用,只能 [代码]<van-field :value="password" type="password" @change="pwdChange" input-class="myClass" /> [代码] 而且不能随意灵活添加class修改组件的样式,需要vant组件支持提供外部样式才可修改,比如上面的[代码]van-field[代码]是通过[代码]input-class[代码]来添加样式控制的,很不方便。而且某些内部样式由于没有外部样式表,根本改不了。 综上: 在微信小程序使用第三方组件库不太方便,样式修改比较麻烦,如果产品是有UI设计时,尽量不使用,有时候自己实现样式可能更快,而且项目体积更小。 使用vuex mpvue官方的快速模板中是将vuex放在[代码]counter[代码] 这个page目录下,可能习惯了vue官方写法的很多同学(包括我)不太喜欢,所以最好就改为vuex官方的写法。 在src目录下建一个[代码]store[代码]的文件夹,分别建以下文件: [图片] 项目不太复杂时不建议使用modules,使用起来比较麻烦。 贴一下[代码]index.js[代码]的代码,其他的[代码]actions.js[代码],[代码]getters.js[代码]按官方的写法就好啦。 [代码]import Vue from 'vue' import Vuex from 'vuex' import * as actions from './actions' import * as getters from './getters' import state from './state' import mutations from './mutations' import createLogger from 'vuex/dist/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ actions, getters, state, mutations, strict: debug, plugins: debug ? [createLogger()] : [] }) [代码] [代码]vuex/dist/logger[代码]是vuex在开发环境可以自动打印日志的工具,debug比较方便,建议使用。 然后在[代码]src/main.js[代码]引入: [代码]import Vue from 'vue' import App from './App' import store from '@/store' Vue.config.productionTip = false App.mpType = 'app' Vue.prototype.$store = store const app = new Vue({ store }) app.$mount() [代码] 这样就可以在项目中正常使用啦,完全支持[代码]mapState[代码],[代码]mapActions[代码],[代码]mapGetters[代码]的写法,比如在[代码]pages/index/index.vue[代码]中使用: [代码]<script> import { mapState, mapActions } from 'vuex' export default { computed: { ...mapState(['myAudio']) }, methods: { ...mapActions(['myActions']) }, created () { this.myActions() //调用vuex中的方法 } } </script> [代码] 踩坑指南 其实大多数坑可能是mpvue的,很多情况也是自己不熟悉小程序生命周期导致的一些奇奇怪怪的bug。 mpvue是支持小程序原生组件的 mpvue会将[代码]div[代码]编译为小程序中的[代码]view[代码]。一开始我不了解,以为用了mpvue后就不能使用小程序原生支持的组件了,比如[代码]swiper[代码],[代码]scroll-view[代码]等,小程序是支持的,可以放心使用哈哈。 npm run build后样式丢失 本来在开发环境正常的,然后准备发版[代码]npm run build[代码]后发现样式丢失了。然后重新[代码]npm start[代码]排查问题,样式还是丢失的。内心此时是mmp的:npm run build丢失就算了,我没改什么东西重新npm start后为什么还是丢失,之前还是正常的呀? 刚开始怀疑是缓存什么的问题,删掉的dist目录,重启开发者工具,甚至重启电脑都试了一下,这是我遇到的超级诡异的问题之一。 冷静下来想到:之前的版本是正常的,一定是新版本引入了什么导致了打包样式的丢失。于是回滚版本一个个build排查问题,最后找到了原因:在一个page中引入了其他page,即在页面中import另一个页面。 在我这里的具体例子是:我在[代码]pages/index/index.vue[代码] 中想做底部共用一个tabbar,页面根据tabbar的值来显示对应的子级页面:[代码]pages/page1/index.vue[代码]和[代码]pages/page2/index.vue[代码]。 所以我将这两个页面当做子组件来引入了:[代码]import Page1 from '@/pages/page1'[代码],一开始没有问题,等重启项目,或者build后就发现样式丢失了。 这可能是mpvue打包机制的一个限制,即[代码]页面不能将另一个页面当子组件来引用[代码],否则会导致样式丢失。 背景音频的src无法读取 项目中希望用户退出小程序后依然能播放音频,所以用到了背景音频的api: wx.getBackgroundAudioManager()。 [代码]this.audio = wx.getBackgroundAudioManager() this.audio.src = 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E061FF02C31F716658E5C81F5594D561F2E88B854E81CAAB7806D5E4F103E55D33C16F3FAC506D1AB172DE8600B37E43FAD&fromtag=46' this.audio.title = '此时此刻' //注意必填 this.audio.epname = '此时此刻' this.audio.singer = '许巍' this.audio.coverImgUrl = 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000' [代码] [代码]title[代码]和[代码]src[代码]赋值后会直接播放音频,后面的几个属性建议也填上,因为播放背景音频时微信是有个界面需要封面图和歌手名称等的。 如果想要获取当前正在播放的音频src,本来以为通过[代码]this.audio.src[代码]来获取就可以了但是有bug。 在开发者工具中是可以正常获取的,即开发时是没问题的,但在真机上返回的是[代码]undefined[代码],因此不能用[代码]this.audio.src[代码]来获取当前播放的音频url,得用一个变量来存这个数据。 直接使用音频的currentTime可能渲染不及时 currentTime用于显示当前的播放进度,但我用在子组件中时经常更新不及时,打印是正常的,但试图渲染不及时,有时候需要点击一下才能重新渲染,这可能是mpvue使用时才会遇到。 所以建议还是项目自身维护一套背景音频的变量比较好一点,比如放在[代码]vuex[代码]中。监听[代码]BackgroundAudioManager.onTimeUpdate()[代码]方法每次赋值到自身维护的变量中。 音频的onCanplay方法不一定每个音频都会触发 一开始我监听在[代码]onCanplay[代码]方法,将音频的时长信息[代码]duration[代码]赋值到vuex中存起来,但发现[代码]onCanplay[代码]有时候是不会触发的,比如重新赋值src播放下一首时,很尴尬。 所以不要太依赖onCanplay这个方法,还好目前直接使用[代码]audio.duration[代码]好像不会出现像上面的[代码]currentTime[代码]渲染不及时的问题,所以就这样用着先。 音频播放结束,即onStop后,不能再通过audio.play()的方法重新播放,得重新赋值src 正常来说,音频播放结束后,音频的src是不变的,再次[代码]play()[代码]应该是可以的。但在小程序中偏偏不行,得重新赋值src才能重新播放,这应该是小程序的一个bug。。。 所以需要判断一下暂停和停止的情况,用不同的办法播放。正常来说,音频暂停时[代码]currentTime[代码]是不为0的,而结束时[代码]currentTime[代码]会为0。 所以可以通过[代码]currentTime[代码](最好是自己维护的变量)来判断暂停和停止的情况:如果currentTime不为0,表示是暂停的情况,可以用[代码]play()[代码],如果小于等于0,则重新赋值src播放: [代码]if (currentTime) { this.audio.play() } else { this.audio.src = 'xx.mp3' } [代码] mpvue不支持直接在template上直接绑定函数 这个是mpvue文档上有写的,不过一开始并不是很理解,也踩坑了,所以在这里提一下,避免不知道的同学踩坑找半天。 [代码]<template> <div v-for="(item, index) in list" :key="index">{{ formatItem(item) }}</div> </template> <script> export default { data () { return{ list: [1, 2, 3] } }, methods: { formatItem (item) { return `我是${item}` } } } </script> [代码] 上面的代码应该是日常vue中比较常用的,就是将数据传参给方法做一些处理,这个在mpvue中是不支持的,会被编译成一个空字符串。 小程序中可放心使用css3的一些特性 比如高斯模糊 [代码]filter: blur(50px); [代码] 如果要使用动画,尽量用[代码]css[代码]动画代替[代码]wx.createAnimation[代码] 在实际使用时,[代码]wx.createAnimation[代码]做动画其实很卡,性能很差,所以在需要使用动画时,建议尽量使用css做动画。 在小程序中是支持css动画的,[代码]transition[代码],[代码]animation[代码],[代码]@keyframes[代码]这些特性都支持。 比如做一个div一直旋转的动画,大家可以对比一下两个版本: [代码]wx.createAnimation[代码]版本 原理:通过setInterval()不断更新div的旋转位置 [代码]<template> <div class="cover" :animation="animationData"></div> </template> <script> export default { data () { return { animationData: '', animation: '', rotateCount: 0, timer: '' } }, components: { }, methods: { startRotate () { this.timer = setInterval(() => { this.rotateAni(++this.rotateCount) }, 100) }, rotateAni (n) { if (!this.animation) { return } // 每100毫秒旋转10度 this.animation.rotate(10 * n).step() this.animationData = this.animation.export() } }, onShow () { // 页面从隐藏到显示时才执行 if (!this.animation) { this.animation = wx.createAnimation() this.startRotate() } }, onReady () { // 第一次初始化时会执行 if (!this.animation) { this.animation = wx.createAnimation() this.starRotate() } }, onHide () { // 页面隐藏时会执行,避免频繁的setData操作,将定时器停掉 this.timer && clearInterval(this.timer) }, beforeDestroy () { // 页面卸载,也停掉定时器 this.timer && clearInterval(this.timer) } } </script> <style scoped lang="scss"> .cover { left: 20px; bottom: 70px; border-radius: 50%; background: #fff; position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.2); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; z-index: 10000; } </style> [代码] 使用css的[代码]@keyframes[代码]做旋转动画 [代码]<template> <div class="cover" :style="coverStyle"></div> </template> <script> export default { } </script> <style scoped lang="scss"> // 定义一个动画名为 rotate @keyframes rotate { 0%, 100% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .cover { left: 20px; bottom: 70px; border-radius: 50%; background: #fff; position: absolute; width: 50px; height: 50px; background: rgba(0, 0, 0, 0.2); box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.5); overflow: hidden; z-index: 10000; // 使用动画 animation: rotate 4s linear infinite; } </style> [代码] 用js写的动画需要控制好setInterval的间隔时间和旋转角度,比较难调。而用css写动画很简单,性能比js好,代码量也很少。 使用css动画时建议开启硬件加速 为了动画更流畅,想尽办法做优化,虽然不知道有没效果,反正用了再说[手动滑稽]。 可以用will-change和transform: translate3d(0,0,0)开启硬件加速。我也不太会用,具体用法大家自行百度Google。 [代码]will-change: auto; transform: translate3d(0, 0, 0); [代码] iPhoneX需要底部导航条预留34px(68rpx)的高度。 由于小程序中不能设置[代码]viewport-fit=cover[代码],所以也就没有web中的安全区域说法,目前主流的做法是通过[代码]wx.getSystemInfoSync()[代码]判断是否是ipx,若是则给页面底部撑高34px。 [代码]const res = wx.getSystemInfoSync() if (res.model.indexOf('iPhone X') >= 0) { this.isIpx = true } [代码] 注意是用[代码]res.model.indexOf('iPhone X')[代码],在开发者工具的iPhone X中,model是全等于[代码]iPhone X[代码]的,但在真机中往往拿到的值是[代码]iPhone X GZxxx[代码],即后面可能会带一串东西,所以用[代码]indexOf[代码]才是比较稳的,而且对[代码]iPhone XR[代码]等机型也适用。 由于还有其他安卓机的全面屏,不太可能一一判断,而且某些安卓全面屏是没有用iPhone底部的工具条的(不存在冲突的情况),所以我们只判断iPhone X的情况就可以了,其他全面屏就不需要给底部预留了。 至于全面屏布局的适配,需要用[代码]flex[代码]布局或者获取屏幕宽高来慢慢调了,建议最好用flex布局自适应处理。 for循环中的子组件click事件无法触发 [代码]Page -> 父组件 -> 子组件[代码],在子组件click后[代码]$emit[代码]一个事件出来,发现无法触发。 这个bug一开始没有出现,但偶然[代码]npm run build[代码]出现的,然后排查原因,后面即使回滚所有版本再npm start也还会出现。好像不触发则已,一发就不可收拾,这又是一个大坑,搜issue和加群问人,当晚下班回家研究到1点多都没有解决。 第二天继续研究,感觉可能是框架的原因,最后尝试升级一下mpvue版本,没想到就正常了。直接使用quick-strat项目的[代码]mpvue[代码]版本是 2.0.0,[代码]mpvue[代码]和[代码]mpvue-template-compiler[代码]升级到最新[代码]2.0.6[代码]就解决了。 事后查看mpvue版本记录,果然是框架本身原因,并且找到了issue。 npm run build后代码报错,再build一次可能报另一些错 解决: 没找到原因,可能是引入vant导致的,打包时丢失了部分文件。多build几次,或者重启下小程序开发者工具就正常了。 mpvue中created() 钩子会在页面初始化时全部一起触发,尽量不要用 小程序生命周期的理解 进入已销毁的page组件时依次触发: onLoad,onShow,onReady,beforeMount,mounted 第一次进入已销毁的子组件时依次触发: onLoad,onReady,beforeMount,mounted 第二次进入已销毁的子组件时依次触发: onLoad,onShow,onReady 再次进入 未被销毁的page组件、子组件时只触发: onShow mpvue文档中建议尽量不要使用小程序的生命周期,这个应该是为了让项目更好地适应支付宝小程序和头条小程序等,所以才这样建议大家尽量不要使用某一个小程序自身的api。 如果你们的小程序只是微信小程序(不考虑兼容其他平台小程序),我建议直接用小程序的生命周期,而不要用mpvue的生命周期,坑太多了。比如mpvue的created周期,初始化时所有的page都会执行,所以created这个周期是不能用了。 onUnload不触发 小程序中与平常web开发不同的是,它的页面会被缓存。举个例子: 从[代码]page1[代码]跳转到[代码]page2[代码],再从[代码]page2[代码]返回[代码]page1[代码],此时的[代码]page1[代码]还没销毁,不会触发[代码]onLoad[代码]再重新渲染,而是直接使用之前的数据。从性能上来说,单纯的返回不应该再请求api获取数据重新渲染,这是对的,符合我们的预期。 而有时候,从[代码]page2[代码]返回[代码]page1[代码]时,我们希望[代码]page1[代码]是重新获取数据渲染的。比如在[代码]page2[代码]做了一个退出登录的操作,此时再返回[代码]page1[代码]时,还是会看到之前的数据。实际上我们的预期是:由于已经退出登录了,[代码]page1[代码]的数据应该被销毁了。 在平常的web开发中,遇到上面的问题,我们可能是不管缓存,每次返回[代码]page1[代码]都再次请求api渲染最新的数据,牺牲掉部分性能从而保证逻辑的正确性。 在mpvue中我也尝试这样干了:想在[代码]page1[代码]的[代码]onUnload()[代码]生命周期中销毁数据,但是没有成功。即使在[代码]page2[代码]退出登录时,采用[代码]wx.reLaunch()[代码]重新刷一遍,[代码]page1[代码]的[代码]onUnload()[代码]生命周期也没有执行。所以[代码]onUnload()[代码]是有可能不执行的,建议慎用。 最后还是得想办法做到在[代码]page2[代码]控制[代码]page1[代码]的数据销毁或保留。想到这里,[代码]vuex[代码]就不自觉浮现在眼前了,如果page1的数据是通过vuex来控制的,那么我在page2就可以用vuex来灵活管理其他页面的数据了。 如果page2做退出登录操作时,就让page1的数据销毁,如果是不退出登录正常返回,page1的数据还是正常,做到灵活控制。 个人平时web开发很少用[代码]vuex[代码],因为项目比较简单不用那么复杂的全局数据传递。但在小程序中,建议全局使用[代码]vuex[代码]来控制所有数据(当然是得根据需求来用)。 总结 第一次开发小程序就直接上了mpvue,可能有些坑已经很多同学总结过了,有些坑可能是不熟悉而导致的,但自己没有去踩过一遍可能不够深刻。 有两种坑会比较难啃: 框架本身的问题,如mpvue2.0.0出现的子组件无法触发事件的问题。 开发者工具和真机运行环境不一致导致的坑。 遇到真机和开发者工具不一致的情况,可按以下步骤排查: 有可能是缓存,可以杀掉之前的版本再跑起来 手机微信版本太低,可能api不支持,用[代码]wx.canIUse[代码]打印一下 手机端某些属性不支持读取,比如上面的[代码]this.audio.src[代码],可以在真机打印调试一下 代码在手机端运行有报错,可以在手机端开启调试,看一下log 微信设计上的坑,百度下是否有相关的案例和解决办法 而遇到mpvue框架的问题可以: 去搜一下[代码]mpvue[代码]的issue看有没相关解决办法 尽量使用最新版本的框架,可能某些问题已经修复了的。实在解决不了的,建议想办法绕过,换一种方法来实现。 希望对大家有所帮助。
2019-03-12 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11