记一次Taro微信小程序开发经历

2,398 阅读12分钟

第一次做小程序相关项目,技术栈是React+TS+Taro3。由于我之前完全没接触过小程序,也没有可以任何可参考的工程和代码,开发过程中基本把能踩的坑全踩了,细细想想还是对小程序不了解导致的,可以说是一头包,磕磕绊绊项目终于上线。趁着项目完结,开始总结复盘下项目中出现的一些问题.

代码格式化

Taro 默认会对所有单位进行转换。在 Taro 中书写尺寸按照 1:1 的关系来进行书写,即从设计稿上量的长度 100px,那么尺寸书写就是 100px,当转成微信小程序的时候,尺寸将默认转换为 100rpx,当转成 H5 时将默认转换为以 rem 为单位的值。开发倒是方便,直接copy蓝湖设计稿尺寸。

但有些情况下不想将px转换为rpx,想使用固定的px值。Taro的默认配置会对所有的 px 单位进行转换,有大写字母的 Px 或 PX 则会被忽略。可以采用以下两种方法:

  1. 书写了行内样式,编译时就无法做替换。
  2. 在px单位中添加一个大写字母,例如 Px 或者 PX 这样,则会被转换插件忽略。

直接copy之前项目的prettier和eslint,想直接套用现有的代码开发规范,发现prettier中的postcss-pxtorem会将PX之类大写转为px,翻遍了prettier和taro的issue也没啥好的方法。为了跳过这个转换需要添加prettier忽略配置,在使用PX时在手动添加prettier-ignore,但是如果添加的多就相当烦。或者将prettier更改为Beautify。

View {
  /* prettier-ignore */
  font-size: 16PX;
}

这两者均不是好选择,Beautify插件被弃用,设置prettier添加忽略配置又太过麻烦.但是这个项目中刚好有比较的多场景下不想转换成rpx,幸亏使用taro脚手架创建项目时自带了一个.editorconfig,在保证统一的代码格式规则情况下,也能满足团队开发使用。

生命周期

page-lifecycle.2e646c86.png

刚接触小程序时,因为不了解小程序的生命周期,出了不少的问题.小程序原本的生命周期叠加上React的生命周期,有时候实在弄不清楚对应的顺序.

实际开发实在是就没那么多时间去收集这些信息,写了几个对应生命周期的hooks后进行测试,对应hooks触发的顺序:useLoad -> useDidShow -> useLayoutEffect -> useEffect -> useReady

  1. useLoad:等同于页面的 onLoad 生命周期钩子。
  2. useDidShow:页面显示/切入前台时触发。等同于 componentDidShow 页面生命周期钩子。
  3. useReady:从此生命周期开始可以使用createCanvasContextcreateSelectorQuery等API访问小程序渲染层的DOM节点。

和其他Web的React环境不同,在web react 环境下第一次 useEffect 能获取到 dom 节点,在第一次useEffect/useDidShow中,小程序的页面 dom 节点尚未挂载,获取不到。

而Taro的虚拟DOM节点,存在于逻辑层,因此不携带节点尺寸信息.受限于小程序平台的实现机制,如果需要获取节点的尺寸、定位等与渲染有关的信息,需要使用Taro.createSelectorQuery API 来获取节点,需要配合在onReady生命周期中获取到节点信息。

Q1:获取不到节点信息

开发时偶现在onReady中拿不到对应的节点信息,存在获取节点失败情况。多触发几次微信小程序开发工具的重新编译有一定的概率复现这个问题。解决方案就是在获取节点时添加上使用Taro.nextTick或setTimeout等方法增加延时。但在onReady在页面初次渲染完成时触发,代表页面已经准备妥当,可以和视图层进行交互,这种情况下拿不到节点就很奇怪.

约定在开发过程中将获取节点信息操作统一将放到useEffect里面,包裹一层Taro.nextTick.

本以为这一块不会再出什么问题,但是还是在这个API上翻了船。

场景如下:在tabber页面上跳转到一个配置tabber显示列表数据的设置页面,在该页面可以添加上一个页面的列表显示项,列表显示项由几个文本数据和一个Canvas组件构成,但是回到目标页面后,发现这个新的列表项已经被渲染,文本信息已经出现,但是新的Canvas却没有渲染出来。

    const query = Taro.createSelectorQuery()
    let { pixelRatio } = systemInfoSync
    query.select('#' + id).boundingClientRect()
    query.select('#' + id).fields({ node: true, id: true, size: true })
    query.exec((res) => {
       // res输出为null,data为对应数据
      console.log(res,data)
      const canvas = res[1]?.node
    }
    
    
    <View>
    {data.map((item)=>{
        // 文本数据可正常渲染
        <View>{item.text1}</View>
        // Canvas无法进行渲染
        <View><Canvas /></View>
    })}
    </View>

通过分析代码后,当在配置页面添加列表显示项时,tabber页面就已经通过rudex拿到存储列表显示项目,tabber页面获取到了对应的数据,该Canvas组件内部接受到了渲染的数据,但是始终拿不到node。

使用的Taeo.createSelectorQuery本质是在对wx的select API做的一层封装,通过查询文档发现SelectorQuery API只能获取到当前页面的节点信息。当在配置页面自然就无法获取tabBar页面的node节点

通过在返回tabBar页面,在useDidShow和useDidHide配置刷新组件列表相关逻辑,当组件内部数量or信息变更时重新渲染组件列表,重新渲染的过程中就又能拿到对应的node节点,这样也避免了没有数据更新的情况,页面切换也会导致组件会更新的问题。

const show = useRef<boolean>(false)

useDidShow(()=>{
    // 判断重新刷新组件列表逻辑
    show.current = true
},[])

useDidHidden(()=>{
    show.current = false
},[])

function App(){
    return {show&&<View></View>}
}

Q2:生命周期不触发

由于项目的数据获取要求实时性,并不使用常规请求的方式,而是需要使用部门基于websocket封装的API,请求时需要先进行订阅后会才会返回对应数据。在页面组件和子组件中都是在useDidShow下先进行配置请求,useDidHidden进行销毁请求。

但在一些特定的场景下,切换页面发现useDidShow并不会进行触发,造成页面数据不更新的问题。

场景如下:问题页面为tabBar页面,在当前页切换SwiperItem,可正常切换到对应的SwiperItem轮播项,但是内部的数据并也不会变化。

因为每个SwiperItem页面都携带了大量的组件非常占用内存,保留全部的轮播项在低端机器上容易出现内存不足的问题,甚至无法在做到同时保留3个连续的SwiperItem.因此只有切换到对应的轮播项才进行条件渲染,非激活状态下销毁SwiperItem其他轮播项。

<View>
    <Swiper>
        <SwiperItem> 
            { activeItem===0 && <View></View>}  
        </SwiperItem>  
        <SwiperItem>
            { activeItem===1 && <View></View>}
        </SwiperItem>  
        <SwiperItem>
            { activeItem===2 && <View></View>}
        </SwiperItem>
         <SwiperItem>
            { activeItem===3 && <View></View>}
        </SwiperItem>
       // ......以下组件省略,大概就这一结构
    </Swiper>
</View>

分析了下切换页面的触发的生命周期(A-0,A-1,A-2为A页面对应的SwiperItem组件,A,B页面均为TabBar页面,C页面为跳转到的分包页面)

当前页面切换后页面触发的生命周期(按顺序)
A-0BA.onHide(), B.onLoad(), B.onShow()
A-0B(再次打开)A.onHide(), B.onShow()
A-0A-1A-1.useEffect(), A.useEffect()
CA-0C.onUnload(), A-0.onShow() A.onShow()

发现A页面将切换SwiperItem之后发现作为子组件的SwiperItem组件的onShow生命周期并不会进行触发,仅仅会触发useEffect。

在SwiperItem不设置条件渲染的情况下,正常情况的跳转入A页面或者首次进入A页面都会触发全部的SwiperItem组件的onShow生命周期,符合切换页面的文档描述。而正常状态下,SwiperItem组件之间切换就是不触发onShow生命周期. image.png

因此需要修改订阅逻辑,在useEffect中也添加订阅,当然只执行useEffect和useDidShow其中一个即可,在useDidShow配置下属性,执行之后就不再执行useEffect.

function useQuotationQuery(pageId: string,....) {
  const justDoneAlone = useRef<boolean>(false)

   // 初始化请求
  function initialize() {
  }

  // 处理进入页面,切换tabBar页面,回退页面
  useDidShow(() => {
    justDoneAlone.current = true
    initialize()
  })

  useDidHide(() => {
    // 销毁请求
  })

  // 处理swiper页条件渲染切换
  useEffect(() => {
    if (!justDoneAlone.current) {
      initialize()
      return () => {
        // 销毁请求
      }
    }
  }, [])
}

获取页面信息

获取页面信息是非常常见的需求,在页面跳转之后能获取跳转页的页面信息,从而进行特定的渲染。但是在开发过程中,有时候就会发现拿到的页面信息有问题。

IOS上在支付后跳转或者扫码后的跳转就很容易出现这种问题。页面通过 useRouter() 获取参数,获取到的却是上一个的页面信息。切换成Taro小程序的Taro.getCurrentInstance() 获取当前页面实例,发现获取到的却也是上一个页面的信息。

// 上一页面数据
const router = useRouter()
// getCurrentInstance().router 和 useRouter 返回的内容也一样  

根据官方文档useLoaded在此生命周期中通过访问 options 参数或调用 getCurrentInstance().router,可以访问到页面路由参数。 getCurrentInstance().router 其实是访问小程序当前页面 onLoad 生命周期参数的快捷方式。

怀疑是触发这些API进行页面跳转之后,小程序相关生命周期错乱导致的。所以换了另一种方式,直接去拿页面栈信息,获取栈顶的数据。

const pages = Taro.getCurrentPages();
const currentPage = pages?.[pages?.length - 1];
const params = currentPage.options || {};

Canvas的使用

由于项目的页面需要展示的数据比较多,页面的节点数量也非常的多. 数据实时更新,交互上切换数据导致频繁的创建销毁,这些会带来比较严重的性能问题,尤其是在低端安卓设备上.

image.png 出于性能优化的目的,将一部分的用于展示数据的组件改造为Canvas.使用过程中发现存在一系列的问题. 一开始绘制Canvas会出现拿不到node的问题,总结下发现完成正常的绘制需要满足3个条件:

  1. 必须使用 Taro 提供的 Canvas
  2. Canvas 中必须包含类型 type="2d"
  3. 在useEffect中使用需要设置Taro.nextTick或者合理setTimeout,否则无法获得 canvas node 节点。
useEffect(() => {
    Taro.nextTick(() => {
      const query = wx.createSelectorQuery();
      query
        .select('#myCanvas')
        .fields({ node: true, size: true })
        .exec((res) => {
          const canvas = res[0].node;
        });
    });
  }, []);
  
<Canvas type="2d" className="canvas" canvasId="canvas-1" />

滚动穿透

小程序里面有比较多需要设置遮罩层和弹出层的场景,不进行处理很容易出现滚动穿透问题,尤其是在IOS。

常规的阻止滚动穿透的方案如下:

  1. 为需要阻止滚动穿透的组件设置overflow:hidden,position:fixed,设置top值
  2. 外层的容器为ScrollView,出现弹出层配置scroll为false
  3. 为touchMove配置e.preventdefault(),e.stoppropagation()

在原生小程序中处理滚动穿透可以通过设置catchtouchmove,但是Taro3在小程序逻辑层实现了一套事件系统,包括事件触发和事件冒泡。但在小程序模板中绑定的事件都是以 bind 的形式,所以不能使用e.stopPropagation()阻止滚动穿透.

当然也可通过配置css属性的方式进行处理,在需要进行阻止滚动穿透的场景下添加overflow:hidden,position:fixed,超出一屏的情况还需要额外记录下top值,已处理移除样式后页面跳到顶部的问题.感觉有点麻烦了.

Taro提供了一个额外的属性catchMove,在处理ScrollView组件和固定高度的View非常好用.

// 添加属性后View组件会绑定 catchtouchmove事件
<View catchMove></View>
场景阻止滚动穿透的方法
ScrollView组件在ScrollView组件外层嵌套一个<View catchMove></View>组件
高度固定且不会产生滚动的View组件在View组件外层嵌套一个<View catchMove></View>组件
会产生滚动的View组件添加catchMove后,View本身不能进行滚动,肯定不能用这个方案

Input

Input作为非常常见的组件,本以为就做个搜索框和提交信息的输入,场景很简单肯定没啥问题,然后就又双叒叕翻车了。

遇到了以下问题:

  1. 当空Input时foucs,placeholder的字体样式会发生变化
  2. 输入Input文本之后,移除光标栏的focus,样式也会发生变化
  3. IOS场景下Input组件手动聚焦时光标位置不在文字最右边

相对比较细节的问题,但还是比较影响用户体验的。

原本以为是没有对设置input:focus和input的样式进行设置导致的问题,在查阅文档发现input组件作为一个元素组件,字体是系统字体,是无法设置font-family。那focus状态下和正常这两者的差异又是从何而来?通过微信开发工具也只能看到个font-family:UICTFontTextStyleBody。

使用textarea也能满足用户的输入需求,通过使用maxlength限制最大输入长度,show-confirm-bar:false处理下键盘上方的完成按钮,基本能处理掉这些问题。

问题3属于Taro框架自身问题,详情见github.com/NervJS/taro…

axios版本

项目中使用了一部分的http请求,常规操作使用axios进行处理,然后就报错了。 引入一个低于0.26.1版本的axios,配置下适配器就好了

import axios from "axios";  // 0.25.0版本
import mpAdapter from 'axios-miniprogram-adapter'

const service = axios.create({
	adapter: mpAdapter,
	baseURL: BASEURL,
	timeout: 6000
})

// 配置请求拦截和响应拦截基本没变化

跳转H5页面

项目有跳转H5需求,在H5页面上进行操作之后需要将H5的改动带回到小程序。 跳转到一个新的页面,将H5的路径传给WebView,返回页面的操作会触发onMessage,在这个时间点将操作保存下来,使用storage存储信息已完成对应的通讯。

  <View>
      <WebView src={h5url} onMessage={receiveH5Info} ></WebView>
  </View>

总结

统计了下出现的问题,一方面是没啥小程序开发经验,另一方面对于小程序的相关认知还不够(就是文档/代码看的少)。痛定思痛,好好看文档去了。也请各位大佬多多指教!