- 你可能不知道的mpvue性能优化技巧
最近一直在折腾[代码]mpvue[代码]写的微信小程序的性能优化,分享下实战的过程。 先上个优化前后的图: [图片] 可以看到打包后的代码量从[代码]813KB[代码]减少到[代码]387KB[代码],Audits体验评分从[代码]B[代码]到[代码]A[代码],效果还是比较明显的。其实这个指标说明不了什么,而且轻易就可以做到,更重要的是优化小程序运行过程中的卡顿感,请耐心往下看。 常规优化 常规的Web端优化方法在小程序中也是适用的,而且不可忽视。 一、压缩图片 这一步最简单,但是容易被忽视。在tiny上在线压缩,然后下载替换即可。 [图片] 我这项目的压缩率高达[代码]72%[代码],可以说打包后的代码从[代码]813KB[代码]降到[代码]387KB[代码]大部分都是归功于压缩图片了。 二、移除无用的库 我之前在项目中使用了Vant Weapp,在[代码]static[代码]目录下引入了整个库,但实际上我只使用了[代码]button[代码],[代码]field[代码],[代码]dialog[代码]等几个组件,实在是没必要。 所以干脆移除掉了,微信小程序自身提供的[代码]button[代码],[代码]wx.showModal[代码]等一些组件基本可以满足需求,自己手写一下样式也不用花什么时间。 在这里建议大家,在微信小程序中,尽量避免使用过多的依赖库。 不要贪图方便而引入一些比较大的库,小程序不同于[代码]Web[代码],限制比较多,能自己写一下就尽量自己写一下吧。 小程序的优化 咱们首先得看一下官方优化建议,大多是围绕这个建议去做。 一、开启Vue.config._mpTrace = true 这个是[代码]mpvue[代码]性能优化的一个黑科技啊,可能大多数同学都不知道这个,我在官方文档都没有搜到到这个配置,我真的是服了。 我能找到这个配置也是Google机缘巧合下看到的,出处:mpvue重要更新,页面更新机制进行全面升级 具体做法是在[代码]/src/main.js[代码]添加[代码]Vue.config._mpTrace = true[代码],如: [代码]Vue.config._mpTrace = true Vue.config.productionTip = false App.mpType = 'app' [代码] 添加了[代码]Vue.config._mpTrace[代码]属性,这样就可以看到console里会打印每500ms更新的数据量。如图: [图片] 如果数据更新量很大,会明显感觉小程序运行卡顿,反之就流畅。因此我们可以根据这个指标,逐步找出性能瓶颈并解决掉。 二、精简data 1. 过滤api返回的冗余数据 后端的api可能是需要同时为iOS,Android,H5等提供服务的,往往会有些冗余的数据小程序是用不到的。比如api返回的一个[代码]文章列表[代码]数据有很多字段: [代码]this.articleList = [ { articleId: 1, desc: 'xxxxxx', author: 'fengxianqi', time: 'xxx', comments: [ { userId: 2, conent: 'xxx' } ] }, { articleId: 2 // ... }, // ... ] [代码] 假设我们在小程序中只需要用到列表中的部分字段,如果不对数据做处理,将整个[代码]articleList[代码]都[代码]setData[代码]进去,是不明智的。 小程序官方文档: 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。 可以看出,内存是很宝贵的,当[代码]articleList[代码]数据量非常大超过1M时,某些机型就会爆掉(我在iOS中遇到过很多次)。 因此,需要将接口返回的数据剔除掉不需要的,再[代码]setData[代码],回到我们上面的[代码]articleList[代码]例子,假设我们只需要用[代码]articleId[代码]和[代码]author[代码]这两个字段,可以这样: [代码]import { getArticleList } from '@/api/article' export default { data () { return { articleList: [] } } methods: { getList () { getArticleList().then(res => { let rawList = res.list this.articleList = this.simplifyArticleList(rawList) }) }, simplifyArticleList (list) { return list.map(item => { return { articleId: item.articleId, author: item.author // 需要哪些字段就加上哪些字段 } }) } } } [代码] 这里我们将返回的数据通过[代码]simplifyArticleList[代码]来精简数据,此时过滤后的[代码]articleList[代码]中的数据类似: [代码][ {articleId: 1, author: 'fengxianqi'}, {articleId: 2, author: 'others'} // ... ] [代码] 当然,如果你的需求中是所有数据都要用到(或者大部分数据),就没必要做一层精简了,收益不大。毕竟精简数据的函数中具体的字段,是会增加维护成本的。 PS: 在我个人的实际操作中,做数据过滤虽然增加了维护的成本,但一般收益都很大,因次这个方法比较推荐。 2. data()中只放需要的数据 [代码]import xx from 'xx.js' export default { data () { return { xx, otherXX: '2' } } } [代码] 有些同学可能会习惯将[代码]import[代码]的东西都先放进[代码]data[代码]中,再在[代码]methods[代码]中使用,在小程序中可能是个不好的习惯。 因为通过[代码]Vue.config._mpTrace = true[代码]在更新某个数据时,我对比放进data和不放进data中的两种情况会有差别。 所以我猜测可能是data是会一起更新的,比如只是想更新[代码]otherXX[代码]时,会同时将[代码]xx[代码]也一起合起来[代码]setData[代码]了。 3. 静态图片放进static 这个问题和上面的问题其实是一样的,有时候我们会通过[代码]import[代码]的方式引入,比如这样: [代码]<template> <img :src="UserIcon"> </template> <script> import UserIcon from '@/assets/images/user_icon.png' export default { data () { return { UserIcon } } } </script> [代码] 这样会导致打包后的代码,图片是[代码]base64[代码]形式(很长的一段字符串)存放在[代码]data[代码]中,不利于精简data。同时当该组件多个地方使用时,每个组件实例都会携带这一段很长的[代码]base64[代码]代码,进一步导致数据的冗余。 因此,建议将静态图片放到[代码]static[代码]目录下,这样引用: [代码]<template> <img src="/static/images/user_icon.png"> </template> [代码] 代码也更简洁清爽。 看一下做了上面操作的前后对比图,使用体验上也流畅了很多。 [图片] 三、swiper优化 小程序自身提供的[代码]swiper[代码]组件性能上不是很好,使用时要注意。参考着两个思路: 【优化】解决swiper渲染很多图片时的卡顿 想请教一下小程序swiper组件的问题 在我使用时,由于需求原因,动态删掉swiper-item的思路不可行(手滑时会造成抖动)。因此只能作罢。但仍然可以优化一下: 将未显示的[代码]swiper-item[代码]中的图片用[代码]v-if[代码]隐藏到,当判断到current时才显示,防止大量图片的渲染导致的性能问题。 四、vuex使用注意事项 我之前写过的一篇mpvue开发音频类小程序踩坑和建议里面有讲如何在小程序中使用[代码]vuex[代码]。但遇到了个比较严重的性能问题。 1. 问题描述 我开发的是一个音频类的小程序,所以需要将播放列表[代码]playList[代码],当前索引[代码]currentIndex[代码]和当前时长[代码]currentTime[代码]放进[代码]state.js[代码]中: [代码]const state = { currentIndex: 0, // playList当前索引 currentTime: 0, // 当前播放的进度 playList: [], // {title: '', url: '', singer: ''} } [代码] 每次用户点击播放音频时,都会先加载音频的播放列表[代码]playList[代码],然后播放时更新当前时长[代码]currentTime[代码],发现有时候播音频时整个小程序非常卡顿。 注意到,音频需每秒就得更新一次[代码]currentTime[代码],即每秒就做一次[代码]setData[代码]操作,稍微有些卡顿是可以理解的。但我发现是播放列表数据比较多时会特别卡,比如playList的长度是100条以上时。 2. 问题原因 我开启[代码]Vue.config._mpTrace = true[代码]后发现一个规律: 当[代码]palyList[代码]数据量小时,[代码]console[代码]显示造成的数据量更新数值比较小;当[代码]playList[代码]比较大时,[代码]console[代码]显示造成的数据量更新数值比较大。 PS: 我曾尝试将playList数据量增加到200条,每500ms的数据量更新达到800KB左右。 到这里基本可以确定一个事实就是:更新[代码]state[代码]中的任何一个字段,将导致整个[代码]state[代码]全量一起[代码]setData[代码]。在我这里的例子,虽然我每次只是更新[代码]currentTime[代码]这个字段的值,但依然导致将state中的其他字段如[代码]playList[代码],[代码]currentIndex[代码]都一起做了一次[代码]setData[代码]操作。 3. 解决问题 有两个思路: 精简state中保存的数据,即限制[代码]playList[代码]的数据不能太多,可将一些数据暂存在[代码]storage[代码]中 [代码]vuex[代码]采用[代码]Module[代码]的写法能改善这个问题,虽然使用时命名空间造成一定的麻烦。vuex传送门 一般情况下,推荐使用后者。我在项目中尝试使用了前者,同样能达到很好的效果,请继续看下面的分享。 五、善用storage 1.为什么说要善用storage 由于小程序的内存非常宝贵,占用内存过大会非常卡顿,因此最好尽可能少的将数据放到内存中,即[代码]vuex[代码]存的数据要尽可能少。而小程序的[代码]storage[代码]支持单个 key允许存储的最大数据长度为 [代码]1MB[代码],所有数据存储上限为 [代码]10MB[代码]。 所以可以将一些相对取用不频繁的数据放进[代码]storage[代码]中,需要时再将这些数据放进内存,从而缓解内存的紧张,有点类似Windows中[代码]虚拟内存[代码]的概念。 2.storage换内存的实例 这个例子讲的会有点啰嗦,真正能用到的朋友可以详细看下。 上面讲到[代码]playList[代码]数据量太多,播放一条音频时其实只需要最多保证3条数据在内存中即可,即[代码]上一首[代码],[代码]播放中的[代码],[代码]下一首[代码],我们可以将多余的播放列表存放在[代码]storage[代码]中。 PS: 为了保证更平滑地连续切换下一首,我们可以稍微保存多几条,比如我这里选择保存5条数据在vuex中,播放时始终保证当前播放的音频前后都有两条数据。 [代码]// 首次播放背景音频的方法 async function playAudio (audioId) { // 拿到播放列表,此时的playList最多只有5条数据。getPlayList方法看下面 const playList = await getPlayList(audioId) // 当前音频在vuex中的currentIndex const currentIndex = playList.findIndex(item => item.audioId === audioId) // 播放背景音频 this.audio = wx.getBackgroundAudioManager() this.audio.title = playList[currentIndex].title this.audio.src = playList[currentIndex].url // 通过mapActions将播放列表和currentIndex更新到vuex中 this.updateCurrentIndex(index) this.updatePlayList(playList) // updateCurrentIndex和updatePlayList是vuex写好的方法 } // 播放音频时获取播放列表的方法,将所有数据存在storage,然后返回当前音频的前后2条数据,保证最多5条数据 import { loadPlayList } from '@/api/audio' async function getPlayList (courseId, currentAudioId) { // 从api中请求得到播放列表 // loadPlayList是api的方法, courseId是获取列表的参数,表示当前课程下的播放列表 let rawList = await loadPlayList(courseId) // simplifyPlayList过滤掉一些字段 const list = this.simplifyPlayList(rawList) // 将列表存到storage中 wx.setStorage({ key: 'playList', data: list }) return subPlayList(list, currentAudioId) } [代码] 重点是[代码]subPlayList[代码]方法,这个方法保证了拿到的播放列表是最多5条数据。 [代码]function subPlayList(playList, currentAudioId) { let tempArr = [...playList] const count = 5 // 保持vuex中最多5条数据 const middle = parseInt(count / 2) // 中点的索引 const len = tempArr.length // 如果整个原始的播放列表本来就少于5条数据,说明不需要裁剪,直接返回 if (len <= count) { return tempArr } // 找到当前要播放的音频的所在位置 const index = tempArr.findIndex(item => item.audioId === currentAudioId) // 截取当前音频的前后两条数据 tempArr = tempArr.splice(Math.max(0, Math.min(len - count, index - middle)), count) return tempArr } [代码] [代码]tempArr.splice(Math.max(0, index - middle), count)[代码]可能有些同学比较难理解,需要仔细琢磨一下。假设[代码]playList[代码]有10条数据: 当前音频是列表中的第1条(索引是0),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]0[代码],没有[代码]上一首[代码],有4个[代码]下一首[代码] 当前音频是列表中的第2条(索引是1),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]1[代码],有1个[代码]上一首[代码],3个[代码]下一首[代码] 当前音频是列表中的第3条(索引是2),截取前5个:[代码]playList.splice(0, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] 当前音频是列表中的第4条(索引是3),截取第1到6个:[代码]playList.splice(1, 5)[代码] ,此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] 当前音频是列表中的第5条(索引是4),截取第2到7个:[代码]playList.splice(2, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]2[代码],有2个[代码]上一首[代码],2个[代码]下一首[代码] … 当前音频是列表中的第9条(索引是[代码]8[代码]),截取后5个:[代码]playList.splice(4, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]3[代码],有3个[代码]上一首[代码],1个[代码]下一首[代码] 当前音频是列表中的最后1条(索引是[代码]9[代码]),截取后的5个:[代码]playList.splice(4, 5)[代码],此时[代码]currentAudio[代码]在这5个数据的索引是[代码]4[代码],有4个[代码]上一首[代码],没有[代码]下一首[代码] 有点啰嗦,感兴趣的同学仔细琢磨下,无论当前音频在哪,都始终保证了拿到当前音频前后的最多5条数据。 接下来就是维护播放上一首或下一首时保证当前vuex中的[代码]playList[代码]始终是包含当前音频的前后2条。 播放下一首 [代码]function playNextAudio() { const nextIndex = this.currentIndex + 1 if (nextIndex < this.playList.length) { // 没有超出数组长度,说明在vuex的列表中,可以直接播放 this.audio = wx.getBackgroundAudioManager() this.audio.src = this.playList[nextIndex].url this.audio.title = this.playList[nextIndex].title this.updateCurrentIndex(nextIndex) // 当判断到已经到vuex的playList的边界了,重新从storage中拿数据补充到playList if (nextIndex === this.playList.length - 1 || nextIndex === 0) { // 拿到只有当前音频前后最多5条数据的列表 const newList = getPlayList(this.playList[nextIndex].courseId, this.playList[nextIndex].audioId) // 当前音频在这5条数据中的索引 const index = newList.findIndex(item => item.audioId === this.playList[nextIndex].audioId) // 更新到vuex this.updateCurrentIndex(index) this.updatePlayList(newList) } } } [代码] 这里的[代码]getPlayList[代码]方法是上面讲过的,本来是从api中直接获取的,为了避免每次都从api直接获取,所以需要改一下,先读storage,若无则从api获取: [代码]import { loadPlayList } from '@/api/audio' async function getPlayList (courseId, currentAudioId) { // 先从缓存列表中拿 const playList = wx.getStorageSync('playList') if (playList && playList.length > 0 && courseId === playList[0].courseId) { // 命中缓存,则从直接返回 return subPlayList(playList, currentAudioId) } else { // 没有命中缓存,则从api中获取 const list = await loadPlayList(courseId) wx.setStorage({ key: 'playList', data: list }) return subPlayList(list, currentAudioId) } } [代码] 播放上一首也是同理,就不赘述了。 PS: 将vuex中的数据精简后,我所做的小程序在播放音频时刷其他页面已经非常流畅啦,效果非常好。 六、动画优化 这个问题在mpvue开发音频类小程序踩坑和建议已经讲过了,感兴趣的可以移步看一眼,这里只写下概述: 如果要使用动画,尽量用css动画代替wx.createAnimation 使用css动画时建议开启硬件加速 最后 大致总结一下上面所讲的几个要点: 开发时打开[代码]Vue.config._mpTrace = true[代码]。 谨慎引入第三方库,权衡收益。 添加数据到data中时要克制,能精简尽量精简。 图片记得要压缩,图片在显示时才渲染。 vuex保持数据精简,必要时可先存storage。 性能优化是一个永不止步的话题,我也还在摸索,不足之处还请大家指点和分享。 欢迎关注,会持续分享前端实战中遇到的一些问题和解决办法。
2019-05-15 - 干货
<view>{{~~(1.999)}} </view> 这样可以取整; 注意:是向下取整的 即 <view>{{~~(1.999)}} </view> 最终渲染为 <view>1</view> 感谢 寒雪 提醒 . 为了防止setData一次传输大量的代码导致页面卡顿或者报错,个人建议使用下面的做法: 滚动加载或者点击加载更多数据的时候,我们一般会采取分页的形式,后端一次会给我10条数据或者5条 给我们的数据一般都是数组 page({ data:{ mrData:[] } }) 我们把从后端拿到的数据这样做 【我是第一次获取的数据是一个数组】 当用户滚动加载的时候 从后台获取到第二次数据 【我是第一次获取的数据,我是第二次获取的数据】 具体写法 let that = this; this.setData({ [[代码]mrData[${that.data.mrData.length}][代码]]:后台给的数组 }) 不知道为什么 我写上 ``不显示了 记住是利用 ES6字符串模板 其转化后为 this.setData({ [‘mrData[0]’]:后台给的数组 }) 循环数据的话 <block wx:for=’{{mrData}}’ wx:key> <block wx:for=’{{item}}’ wx:key wx:for-item=‘it’ wx:for-index=‘ind’> {{it}} </block> </block> 这样就可以了 这样做的好处就是 每次修改只修改部分 这样就解决了 每次滚动加载的是 我们都要重新赋值大量的数据 我还是粘贴一下代码的好 如下: [图片] [图片] [图片] 渲染后的页面出现的效果 如下 [图片] 点击第一个 小浪花 [图片] 成功改变了 nice 一般的写法是这样的 不知道和你写的是否一样 [图片] [图片] 从后台获取数据后 然后把这个数据 push到原来的大数组中 然后再setData 每次setData的数据都会增大 最后超过限制导致页面卡顿影响性能 有可能还会报错 还有就是 第一次写文章 不知道这样写是否有人能看懂 ,希望大家能由此举一反三,看到类似的问题可以想到这样的方法 虽然有些繁琐 但是真的能优化性能,如果能帮到你 希望动动小手指 给我点个赞吧~ O(∩_∩)O哈哈~ 默默无闻的余小浪
2019-05-22 - 【优化】小程序优化-代码篇
本文主要是从代码方面跟大家分享我自己在开发小程序的一些做法,希望能帮到一些同学。 前言 不知道大家有没有这种体会,刚到公司时,领导要你维护之前别人写的代码,你看着别人写的代码陷入了深深的思考:“这谁写的代码,这么残忍” [图片] 俗话说“不怕自己写代码,就怕改别人的代码”,一言不和就改到你吐血,所以为了别人好,也为了自己好,代码规范,从我做起。 项目目录结构 在开发之前,首先要明确你要做什么,不要一上来就是干,咱们先把项目结构搭好。一般来说,开发工具初始化的项目基本可以满足需求,如果你的项目比较复杂又有一定的结构的话就要考虑分好目录结构了,我的做法如下图: [图片] component文件夹是放自定义组件的 pages放页面 public放公共资源如样式表和公共图标 units放各种公共api文件和封装的一些js文件 config.js是配置文件 这么分已经足以满足我的需求,你可以根据自己的项目灵活拆分。 配置文件 我的项目中有个config.js,这个文件是用来配置项目中要用到的一些接口和其它私有字段,我们知道在开发时通常会有测试环境和正式环境,而测试环境跟正式环境的域名可能会不一样,如果不做好配置的话直接写死接口那等到上线的时候一个个改会非常麻烦,所以做好配置是必需的,文件大致如下: [图片] 首先是定义域名,然后在config对象里定义接口名称,getAPI(key)是获取接口方法,最后通过module暴露出去就可以了.引用的时候只要在页面引入 import domain from ‘…/…/config’;,然后wx.request的时候url的获取方式是domain.getAPI(’’) 代码健壮性、容错性 例子 代码的健壮性、容错性也是我们应该要考虑的一点,移动端的项目不像pc端的网络那么稳定,很多时候网络一不稳定就决定我们的项目是否能正常运行,而一个好的项目就一定要有良好的容错性,就是说在网络异常或其它因素导致我们的项目不能运行时程序要有一个友好的反馈,下面是一个网络请求的例子: [图片] 相信多数人请求的方式是这样,包括我以前刚接触小程序的时候也是这样写,这样写不是说不好,而是不太严谨,如果能够正常获取数据那还好,但是一旦请求出现错误那程序可以到此就没法运行下去了,有些比较好的会加上faill失败回调,但也只是请求失败时的判断,在请求成功到获取数据的这段流程内其实是还有一些需要我们判断的,一般我的做法是这样: [图片] 在请求成功后小程序会进行如下判断: 判断是否返回200,是则进行一下步操作,否则抛出错误 判断数据结构是否完整,是则进行一下步操作,否则抛出错误 然后就可以在页面根据情况进行相应的操作了。 定制错误提示码 可以看到上面的截图的错误打印后面会带一个gde0或gde1的英文代码,这个代码是干嘛用的呢,其实是用来报障的,当我们的小程序上线后可能会遇到一些用户发来的报障,一般是通过截图发给我们,之前没有做错误提示码的时候可能只是根据一句错误提示来定位错误,但是很多时候误提示语都是一样的,我们根本不知道是哪里错了,这样一来就不能很快的定位的错误,所以加上这样一个提示码,到时用户一发截图来,我们只要根据这个错误码就能很快的定位错误并解决了,错误提示码建议命名如下: 不宜过长,3个字母左右 唯一性 意义明确 像上面gde表示获取草稿失败,后面加上数字表示是哪一步出错。 模块化 我们组内的大神说过, 模块化的意义在义分治,不在于复用。 之前我以为模块化只是为了可以复用,其实不然,无论模块多么小也是可以模块化,哪怕只是一个简单的样式也一样,并是不为了复用,而是管理起来方便。 很多同学经常将一些公共的样式事js放在app.wxss和app.js里以便调用,这样做其实有一个坏处,就是维护性比较差,如果是比较小的项目还好,项目一大问题就来了。而且项目是会迭代的,不可能总是一个人开发,可能后面会交接给其他人开发,所以会造成的问题就是: app.wxss和app.js里的内容只会越来越多,因为别人不确定哪些是没用的也不敢删,只能往里加东西,造成文件臃肿,不利于维护。 app.wxss和app.js对于每个页面都有效,可读性方面比较差。 所以模块化的意义就出来了,将公共的部分进行模块化统一管理,也便于维护。 样式模块化 公共样式根据上面的目录结构我是放在public里的css里,每个文件命名好说明是哪个部分的模块化,比如下面这个就表示一个按钮的模块化 [图片] 前面说过模块化不在于大小,就算只是一个简单的样式也可以进行模块化,只要在用到的地方import一下就行了,就知道哪里有用到,哪里没有用到,清晰明了。 js模块化 js模块化这里分为两个部分的模块化,一部分是公共js的模块化,另一部分是页面js的模块化即业务与数据的拆分。 公共js模块化 比较常用的公共js有微信登录,弹窗,请求等,一般我是放在units文件夹里,这里经微信弹窗api为例: [图片] 如图是在小程序中经常会用到的弹窗提示,这里进行封装,定义变量,只要在页面中引入就能直接调用了,不用每次都写一大串。比如在请求的时候是这样用的 [图片] toast()就是封装的弹窗api,这样看起来是不是清爽多了! 业务与数据模块化 业务与数据模块化就是指业务和数据分开,互不影响,业务只负责业务,数据只负责数据,可以看到页面会比普通的页面多了一个api.js [图片] 这个文件主要就是用来获取数据的,而index.js主要用来处理数据,这样分工明确,相比以往获取数据和处理数据都在一个页面要好很多,而且我这里获取数据是返回一个promise对象的,也方便处理一些异步操作。 组件化 组件化相信大家都不陌生了,自从小程序支持自定义组件,可以说是大大地提高了开发效率,我们可以将一些公共的部分进行组件化,这部分就不详细介绍,大家可以去看文档。组件化对于我们的项目来说有很大的好处,而且组件化的可移植性强,从一个项目复用到另一个项目基本不需要做什么改动。 总结 这篇文章通过我自己的一些经验来给大家介绍如何优化自己的代码,主要有以下几点 分好项目目录结构 做好接口配置文件 代码健壮性、容错性的处理 定制错误提示码方便定位错误 样式模块化和js模块化 组件化 最后放上项目目录结构的代码片段,大家可以研究一下,有问题一起探讨:https://developers.weixin.qq.com/s/1uVHRDmT7j6l
2019-03-07 - 小程序canvas的那些事
背景 业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二: 服务端合成:直接返回给前端图片URL 客户端合成:客户端利用canvas绘制 在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。 下面主要谈谈**客户端(canvas)**合成的过程。 实现思路 小程序端发起请求,获取需动态展示的数据; 利用canvas绘制画布; 导出图片保存到相册。 小技巧&那些坑 理想很丰满,现实很骨感。 实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。 promise化 画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。 [代码] let promise1 = new Promise((resolve, reject) => { this.getData(resolve, reject) }); let promise2 = new Promise((resolve, reject) => { this.getSystemInfo(resolve, reject) }); Promise.all([promise1, promise2]).then(() => { this.drawCanvas() }).catch(err => { console.log(err) }); [代码] 自适应 1、为了在各个机型下保持大小自适应,需要计算出缩放比: [代码] getSystemInfo(resolve, reject) { try { const res = wx.getSystemInfoSync() //缓存系统信息 systemInfo = res //这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375 zoom = res.windowWidth / 750 * 1 resolve() } catch (e) { // Do something when catch error reject("获取机型失败") } } [代码] 2、绘制时进行按缩放比进行缩放,如: [代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom) [代码] 绘制网络图片 经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制: [代码]wx.downloadFile({ url: imgUrl, success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom) } } }) [代码] 绘制base64图片 因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。 解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持): 1、调用fileManager.writeFile存储base64到本地; 2、绘制本地图片。 实现代码如下: [代码]// 先获得一个文件实例 fileManager = wx.getFileSystemManager() // 把图片base64格式转存到本地,用于canvas绘制 fileManager.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`, data: self.data.qrcode, encoding: 'base64', success: () => { //此处需先调用wx.getImageInfo,方可绘制成功 wx.getImageInfo({ src: `${wx.env.USER_DATA_PATH}/qrcode.png`, success: () => { //绘制二维码 ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom) ctx.draw() } }) } }) [代码] 保存到本地相册 wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。 [代码]canvas2Img(e) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum'] === undefined) { //呼起授权界面 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { save() } }) } else if (res.authSetting['scope.writePhotosAlbum'] === false) { //引导拒绝过授权的用户授权 wx.showModal({ title: '温馨提示', content: '需要您授权保存到相册的权限', success: res => { if (res.confirm) { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { save() } } }) } } }) } else { save() } } }) function save() { wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg', success: res => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showModal({ content: '保存成功', showCancel: false, confirmText: '确认' }) }, fail: res => { if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') { wx.showModal({ content: '保存到相册失败', showCancel: false }) } } }) }, fail: () => { wx.showModal({ content: '保存到相册失败', showCancel: false }) } }) } [代码] 导出清晰图片 wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。 原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。 这里应该乘以设备像素比,实现如下: [代码]wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg' )} [代码] 特殊字体的绘制 研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。 实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。 [代码]drawNum(num, x, y, w, h) { return new Promise(function (resolve, reject) { //这里存储0-9的图片CDN链接 let numMap = [] wx.downloadFile({ url: numMap[num], success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom) resolve() } }, fail: () => { reject() } }) }) } [代码] 安卓机型图片绘制锯齿化问题 测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。 总结 个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点: 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等); 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。 ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。
2019-03-07 - 微信小程序开发常见问题汇总
1、域名必须是https 非https的域名不被微信小程序允许。 2、input组件placeholder字体颜色 卸载placeholder-class里面的color并不生效,需要写在placeholder-style里面就可以了。 3、wx.navigateTo无法跳转到带tabbar的页面 带有tabbar的页面,必须使用wx.switchTab进行跳转。 4、tabbar在切换时页面数据无法刷新 tabbar的实现可能是显示和隐藏view,所以,不会一直调用page.onLoad()方法,可以尝试把代码逻辑写在page.onShow()里面。 5、如何获取shareTickets 获取shareTickets需要在app.onLaunch或者app.onShow里面才能获取到,而不是page.onShow,请一定要注意。 注:建议在app.onShow里面去获取,app.onLaunch不是一直会执行。 6、getPhoneNumber获取手机号 目前该接口针对非个人开发者,且完成了认证的小程序开放。个人开发者是没办法调用这个API的。 7、wx.previewImage图片预览 预览图片URL必须是https开头,不能是本地图片。 8、wx.playVoice音频播放 必须保证音频文件已经在本地,比如在wx.startRecord后,可以获取到filePath。或者提前调用wx.downloadFile来下载资源文件,然后再播放。 9、API老版本兼容 可以用wx.canIUse或者wx.getSystemInfoSync来进行判断,老版本给出相应提示即可。 10、获取系统信息 wx.getSystemInfo,可得到系统语言、屏幕宽高、微信版本号、操作系统、设备像素比、客户端甚础库版本等信息。 11、如何去掉自定义button灰色的圆角边框 主要是button的伪元素设置了样式,去掉即可:button::after{ display: none;}。 12、回到页面顶部 主要是button的伪元素设置了样式,去掉即可:button::after{ display: none;}。 13、input textarea是APP的原生组件,z-index层级最高 有做过搜索框的同学,可能会遇到iOS下面,设置icon的z-index后,依然无法显示。建议做显示隐藏效果:点击之前是一个view,点击之后隐藏view,显示input。 14、小程序如何冷启动 小程序的机制,是在退出五分钟内进入,就会显示的是退出前的页面,如果你希望进入小程序都相当于冷启动的方式,直接进入主页面。你可以在page的onUnload里面里面set一个值,然后在app的onShow的时候判断这个值,然后决定是否跳到首页。 15、一段文字如何换行 小程序中唯一可以实现换行的标签组件是text。 注:text中不支持<br>,只能使用n进行换行。 16、设置最外层标签的margin-bottom在iOS下不生效 margin-bottom在安卓和开发工具里面都正常,就是在iOS下不起效,建议改成padding-bottom。 17、小程序中canvas的图片不支持base64格式 base64格式图片,在开发工具里面可以正常显示,真机上没有显示。建议修改成带https开头的url形式。 微信小程序开发视频教程分享:https://www.sucaihuo.com/video/222-0-0
2019-03-05 - 适配刘海屏和全面屏的一些小心得
今年开始各路刘海和全面屏手势的手机已经开始霸占市场,全面屏和刘海屏的适配也必须提上日程。 相信大家也一定会有第一次将未适配的小程序放到全面屏或刘海屏手机上的尴尬体验。 尤其是在导航栏设置为custom时,标题与胶囊对不齐简直逼死强迫症。。 微信官方也没有出一个官方的指导贴帮助开发者。 这里仅总结一下个人关于这个问题的一些处理方式,如有疏漏烦请指正补充。 适配的关键在两个位置即额头和下巴,头不用说自然是关于刘海的。 小程序的头的高度主要分为2个部分 1.statusBarHeight 该值可以在app onLaunch 调用wx.getSystemInfoSync() 获取到 a)刘海 高度44 [图片] b)无刘海 ios高度20 安卓各不相同 [图片] 2.胶囊高度 即下图高度 [图片] 在查阅社区问答后了解到小程序给到的策略是ios在模拟器下统一是44px,ios在真机下统一是40px(感谢指正@bug之所措 ),而安卓下统一是48px,因此我们又可以在wx.getSystemInfoSync() 中获取到系统之后得到胶囊高度。 总的导航栏高度即这两个高度之合。本人项目中是将导航做成组件并给到slot,方便各个页面配置。 开发者工具 1.02.1810190 及以上版本支持在 app.json 中声明 usingComponents 字段,在此处声明的自定义组件视为全局自定义组件,在小程序内的页面或自定义组件中可以直接使用而无需再声明。 目前小程序还支持在单个页面配置custom,也可以配合使用~ 另一个需要关注的则是底部,参考的文章是 https://www.jianshu.com/p/a1e8c7cf8821 重点是在于在全面屏的手机的底部需要流出34px的空白给到全面屏返回手势操作,此外由于全面屏屏幕圆边还可能使一些按钮或功能无法正常使用。 那么首先如何判断是否是全面屏呢?个人的做法是判断屏幕高度是否大于750,iphone的plus系列高度在736,正好在这个范围之内,当然750不一定准确,如果出现疏漏烦请补充。 涉及到底部的主要是弹出的操作菜单、tabBar和底部定位的按钮等。这里做了一个简单的代码片段。 https://developers.weixin.qq.com/s/fnU0n8mv7o5M 希望能够帮助到大家,也欢迎交流~
2019-01-03 - 小程序使用async/await 来处理异步
小程序使用async/await 来处理异步 regeneratorRuntime https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/runtime.js 详细看代码片段 https://developers.weixin.qq.com/s/FlIPCOmw7P3c //引入编译模块 const regeneratorRuntime =require("../libs/regeneratorRuntime.js") const promisify = require('../libs/promisify.js'); //微信 API,转成返回 Promise 的接口 云开发API的有Promise 风格 不需要再转了 const getUserInfo= promisify(wx.getUserInfo); const login = promisify(wx.login); wx.cloud.init(); Page({ data: { }, onLoad: async function () { console.log(this) const user = await getUserInfo(); console.log("onLoad user",user) const code = await login(); console.log("onLoad code",code); const res = await wx.cloud.database().collection('todos').where({ _openid: 'xxx' }).get(); console.log("onLoad res", res) }, onShow: async function(){ const user = await getUserInfo(); console.log("onshow", user) } })
2018-11-10 - 小程序改造成async/await模式
补充:以下是原生用法: https://developers.weixin.qq.com/community/develop/article/doc/00028cbc2e04e0ddf549d535351c13 简单两步: 1、把这个文件下载并引用进来: https://github.com/facebook/regenerator/blob/master/packages/regenerator-runtime/runtime.js 2、在使用时声明一下: const regeneratorRuntime = require('./lib/runtime.js') 然后就可以使用async/await了。 补充如下:以上方案已经过期作废,小程序原生支持async/await了,(es6转es5别勾)
2020-04-01 - 模态窗以及弹出层相关ui布局的一个小技巧
场景:在web开发以及一般的小程序中遇到模态窗或者弹出层等ui布局的时候,会有在遮罩层上滑动引发下面页面的滑动这一问题。在小程序上这一问题可能还会导致下拉刷新或上拉加载的回调触发。 现有解决方案:1.手动阻止滑动事件(这样需要做在弹层上其他事件的兼容处理)2.特殊处理布局(当有弹出层的时候最外一层的高度设为100vh并overflow:hidden)。 优化方案:弹出层以及内部的组件都用cover-view以及cover-image实现。 原理:cover-view是原生组件,当用它实现铺满整个页面的遮罩层时,滑动事件是发生在原生视图层面上的,不会触发在webview渲染层页面的滑动,自然就不会出现上述问题。
2019-01-08 - 微信朋友圈选图效果
中秋快乐,帮社区里的小伙伴写的选图效果,仿造的微信朋友圈选图,长按拖动排序主要是用交互监听来实现的。还有几个需要处理的地方。另外在真机上的动画速度总感觉快那么一丢丢。没做预览图,详情见代码片段咯。安卓机效果没试过。。 今天是9月23,这两天接受定制改动~ 第一次发经验分享。。轻点喷。。 哦,对了,这里 wx.createSelectorQuery 有个bug。。如果选择的基础库版本在2.0.9以上,第一次打开开发者工具,要先直接点一次编译,不然会出错。。真机上没问题 代码片段:wechatide://minicode/RFLmzDmL7I2a 有小伙伴反馈,在片段上面增加内容会乱位,是因为没计算顶部偏移量,方便懒人,完善了下 新的片段:wechatide://minicode/fOTEM3mI7l3B 终于拿到安卓机器试了,果然卡得很。。要控制频率。这很尴尬,太频繁的话,安卓会卡,太不频繁呢,刚开始移动那阵子会卡。。有同学反馈,说点快了会出问题,于是又加了点击频率的限制 现在的代码片段:wechatide://minicode/xBDdCom17R3c(要重新拿安卓机测试了再看看。。) 附上gif: [图片]
2018-10-24