大力智能前端团队 九天
前言
最近在基于 Taro.js 开发微信小程序。由于小程序与 web 存在很大差异,踩了很多坑。
架构
小程序实质就是 hybrid,但是是受限的。js 运行在逻辑层,是一个没有 html 的单纯的 js 解释器,只能通过 API 和视图层、Native 层交互。逻辑层和视图层是两个独立的线程,完全隔离。
对于平台方而言,这种设计极大增加了平台对应用的控制,也减少了各种风险。但是对于开发者而言,很多 h5 的能力在小程序中被阉割甚至不被提供,开发小程序相当于戴着镣铐跳舞。
性能
小程序的性能可以类比 React Native,每次交互都需要通过 native 的 bridge 进行,差于 native 和 h5。
正文
方案:三态抽屉拖动切换实现
需求:
实现一个抽屉浮层,
状态1:高度为屏幕高度的一半
状态2:占满屏幕
状态3:关闭
通过拖动在三个状态之间切换
复制代码
方案与问题:
-
touch 方案
- 方案:通过 onTouchStart/onTouchMove/onTouchEnd/onTouchCancel 捕捉触摸事件;onTouchEnd 时判断状态
- 缺陷:频繁 setState,onTouchMove 时页面有卡顿
-
scroll 方案
- 方案:使用 scrollview,绑定 onScroll 事件;动画效果为 scroll 效果;onScrollEnd 时判断状态
- 缺陷:惯性滑动时,scrollend 事件是手势停止时触发,并非惯性滑动结束时触发,导致无法获取准确的滚动位置(不能接受)
-
微信小程序 wxs 响应事件 developers.weixin.qq.com/miniprogram…
- Taro.js 暂不支持,可考虑 Taro 混合小程序原生组件使用
方案:多页左右滑动切换实现
方案:
-
小程序 Swiper 组件方案
- 方案:使用小程序的 swiper,绑定 chang 事件,设置数据量为所有的页数,但只有三页有数据:前一页/当前页/下一页;翻页时切换当前页,并设置三页数据/清除其他页数据
- 效果:切换效果流畅;多页渲染性能可以接受
方案:定制导航栏实现
方案:
- 通过设置 navigationStyle: “custom” 不显示官方导航栏
- 项目入口 app.ts 获取状态栏和胶囊按钮的布局信息,计算得到导航栏需要的布局数据
- 自定义导航栏组件,通过 getCurrentPages 获取路由信息,判断左侧按钮类型-返回/首页/无
问题:
现象:首页在页面跳转后返回,多出返回按钮
原因:页面跳转时进行了 setState 导致 re-render,然后组件获取到跳转后的路由信息进行渲染
解决:对按钮状态使用 useMemo,使用第一次 render 时的缓存
注意事项:
webview 无效:客户端 6.7.2 版本开始,navigationStyle: custom 对 web-view 组件无效(详见developers.weixin.qq.com/miniprogram…
问题:页面卸载后异步工作仍然会运行
该问题类似于 web 的 SPA 应用,在从当前页面 A 返回到上一页面后,A 页面的异步工作仍然会运行。
解决方法:
- 页面卸载时清除异步操作
- 页面卸载时设置标记变量,异步回调运行时判断标记变量再进行操作
问题:获取页面dom节点的时机
区别于在 web react 环境下第一次 useEffect 能获取到 dom 节点,在第一次 useEffect/useDidShow中,小程序的页面 dom 节点尚未挂载,获取不到。
onLoad -> onShow -> useEffect -> onReady
解决方法:
- setTimeOut 推迟一段时间并轮询
- wx.nextTick 并轮询直到获取到
- onReady/useReady
问题:路由跳转之后(异步)无法获取页面dom节点
路由跳转后,异步的 SelectorQuery.exec 等方法的回调不会执行,比如 react 生命周期/定时器/Promise等情况
例子如下
Taro.navigateTo({
url: '/',
});
setTimeOut(() => {
const query = Taro.createSelectorQuery();
query
.select('#a')
.fields({ node: true })
.exec((res) => {
console.log(res[0].node);
});
}, 0);
复制代码
解决方法:
- 在路由跳转时同步调用 SelectorQuery
- setTimeOut 推迟路由跳转
- onShow 时再处理
问题:小程序环境缺少很多全局方法,如 atob/btoa 等等
解决方法:
- 手写
- 第三方Polyfill
问题:Taro.js 中使用 require(‘crypto’) 会打包 size 很大的 polyfill
因为 webpack 打包时,遇到 require(‘crypto’),会引入对应的 polyfill ,导致包 size 大大增加
情景介绍:
开发小程序时,打包的 size 很大,分析发现是打包了很多 polyfill,如 bn.js。
追踪依赖链,bn.js -> browserify-sign -> crypto-browserify -> node-libs-browser,node-libs-browser 包提供了某些 Node 库供浏览器使用,可能是 crypto 的浏览器环境的 polyfill。
全局搜索,发现使用的公司的图片上传组件中使用了 require(‘crypto’),并且实质上与 crypto 相关的方法并没有实际调用。
移除 require(‘crypto’) 后,size 减少了 600 KB。
解决方法:
- 尽量避免使用 require(‘crypto’)
- 使用替代的第三方库
问题:微信小程序中使用 InnerAudioContext 播放音频,onTimeUpdate 有时不触发
在使用 InnerAudioContext 播放音频时,当进行调整播放进度(seek)/重新播放等操作后,onTimeUpdate 不会触发。
原因分析
音频加载导致 onUpdateTime 失效(比如调用 seek 的时候,触发了音频自动加载)
解决方法:
- 访问 innerAudioContext.paused 可以使 onUpdateTime 生效,而音频加载完成会触发 onCanPlay,所以在 onCanPlay 回调中访问即可
audioManager.onCanplay(() => {
audioManager.paused;
});
复制代码
小程序音频播放:
- Audio:不再维护,功能较少
- InnerAudioContext:普通音频播放
- BackgroundAudioManager:背景音频,全局唯一
- WebAudioContext:Beta中,类似于 Web_Audio_API
- MediaAudioPlayer:用于播放视频解码器 VideoDecoder 输出的音频
InnerAudioContext 与 BackgroundAudioManager 的区别:
- InnerAudioContext 是普通的音频播放,可以有多个实例播放多个音频;BackgroundAudioManager 全局唯一,只能同时播放一个音频
- InnerAudioContext 在小程序进入后台的时候会被暂停,BackgroundAudioManager 会在手机状态栏有音乐播放组件,微信内有悬窗,在小程序进入后台时依旧会播放
- BackgroundAudioManager 必须填写标题,可填封面、专辑名、歌手等信息
问题:微信小程序中 scroll-view 的滚动问题
- 使用自适应方案无法滚动,必须设置 height 才能在垂直方向滚动
- 当 scroll-view 组件的第一个直接子孙元素设置了 margin-top 时,即使 scroll-view 只有一行也可以滚动。解决方案:使用 padding-top /添加带高度的空白元素
问题:谨慎使用 canvas
在微信中,对于 canvas 的支持存在很多问题,目前已知的问题有:
- OffScreenCanvas 的版本支持和文档不符合
- Canvas.createImage 创建的图片,在 IOS 中,onload 事件不会触发
方案:图片裁剪的方法
给定了一张原图和四边形坐标,裁剪出四边形所框选出的区域作为独立的图片。
最开始的做法是通过小程序的 Canvas 实现,但是微信小程序对于 Canvas 的支持存在问题,因此采用一种折中的方案完成。
- 通过四边形坐标,计算出四边形的外接矩形
- 通过 background-image 、background-position 和 background-size 定位裁剪的图片区域
- 限制元素的尺寸为外界矩形
这种方案有两个需要注意的点:
- 裁剪结果是矩形而不是四边形
- 需要计算容器尺寸,适配最大宽度