基于 Taro 的微信小程序填坑指南(bei)

avatar
研发 @字节跳动

作者:大力智能技术团队-前端 九天

前言

最近在基于 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;
});

小程序音频播放:

InnerAudioContext 与 BackgroundAudioManager 的区别:

  • InnerAudioContext 是普通的音频播放,可以有多个实例播放多个音频;BackgroundAudioManager 全局唯一,只能同时播放一个音频
  • InnerAudioContext 在小程序进入后台的时候会被暂停,BackgroundAudioManager 会在手机状态栏有音乐播放组件,微信内有悬窗,在小程序进入后台时依旧会播放
  • BackgroundAudioManager 必须填写标题,可填封面、专辑名、歌手等信息

问题:微信小程序中 scroll-view 的滚动问题

  • 使用自适应方案无法滚动,必须设置 height 才能在垂直方向滚动
  • 当 scroll-view 组件的第一个直接子孙元素设置了 margin-top 时,即使 scroll-view 只有一行也可以滚动。解决方案:使用 padding-top /添加带高度的空白元素

问题:谨慎使用 canvas

在微信中,对于 canvas 的支持存在很多问题,目前已知的问题有:

方案:图片裁剪的方法

给定了一张原图和四边形坐标,裁剪出四边形所框选出的区域作为独立的图片。

最开始的做法是通过小程序的 Canvas 实现,但是微信小程序对于 Canvas 的支持存在问题,因此采用一种折中的方案完成。

  1. 通过四边形坐标,计算出四边形的外接矩形
  2. 通过 background-image 、background-position 和 background-size 定位裁剪的图片区域
  3. 限制元素的尺寸为外界矩形

这种方案有两个需要注意的点:

  1. 裁剪结果是矩形而不是四边形
  2. 需要计算容器尺寸,适配最大宽度