第一次做小程序相关项目,技术栈是React+TS+Taro3。由于我之前完全没接触过小程序,也没有可以任何可参考的工程和代码,开发过程中基本把能踩的坑全踩了,细细想想还是对小程序不了解导致的,可以说是一头包,磕磕绊绊项目终于上线。趁着项目完结,开始总结复盘下项目中出现的一些问题.
代码格式化
Taro 默认会对所有单位进行转换。在 Taro 中书写尺寸按照 1:1 的关系来进行书写,即从设计稿上量的长度 100px,那么尺寸书写就是 100px,当转成微信小程序的时候,尺寸将默认转换为 100rpx,当转成 H5 时将默认转换为以 rem 为单位的值。开发倒是方便,直接copy蓝湖设计稿尺寸。
但有些情况下不想将px转换为rpx,想使用固定的px值。Taro的默认配置会对所有的 px
单位进行转换,有大写字母的 Px
或 PX
则会被忽略。可以采用以下两种方法:
- 书写了行内样式,编译时就无法做替换。
- 在px单位中添加一个大写字母,例如 Px 或者 PX 这样,则会被转换插件忽略。
直接copy之前项目的prettier和eslint,想直接套用现有的代码开发规范,发现prettier中的postcss-pxtorem会将PX之类大写转为px,翻遍了prettier和taro的issue也没啥好的方法,为了跳过这个转换需要添加忽略配置,在使用PX时在手动添加prettier-ignore,但是如果添加的多就相当烦。或者将prettier更改为Beautify。
css
View {
/* prettier-ignore */
font-size: 16PX;
}
这两者均不是最好的选择,Beautify插件被弃用,设置prettier添加忽略配置又太过麻烦.但是这个项目中刚好有比较的多场景下不想转换成rpx,幸亏使用taro脚手架创建项目时自带了一个.editorconfig
,在保证统一的代码格式规则情况下,也能满足团队开发使用。
生命周期
刚接触小程序时,因为不了解小程序的生命周期,出来不少的问题.小程序原本的生命周期叠加上React的生命周期,有时候实在弄不清楚对应的顺序.
实测生命周期对应hooks触发的顺序:useLoad -> useDidShow -> useLayoutEffect -> useEffect -> useReady
- useLoad:等同于页面的
onLoad
生命周期钩子。 - useDidShow:页面显示/切入前台时触发。等同于
componentDidShow
页面生命周期钩子。 - useReady:从此生命周期开始可以使用
createCanvasContext
或createSelectorQuery
等 API 访问小程序渲染层的 DOM 节点。
和其他Web的React环境不同,在web react 环境下第一次 useEffect 能获取到 dom 节点,在第一次useEffect/useDidShow中,小程序的页面 dom 节点尚未挂载,获取不到。
而Taro的虚拟DOM节点,存在于逻辑层,因此不携带节点尺寸信息.受限于小程序平台的实现机制,如果需要获取节点的尺寸、定位等与渲染有关的信息,需要使用Taro.createSelectorQuery API
来获取节点,需要配合在onReady生命周期中获取到节点信息。
但是开发时偶现在onReady中拿不到对应的节点信息,存在获取节点失败情况。多触发几次微信小程序开发工具的重新编译有一定的概率复现这个问题。解决方案就是在获取节点时添加上使用Taro.nextTick或setTimeout等方法增加延时。但在onReady在页面初次渲染完成时触发,代表页面已经准备妥当,可以和视图层进行交互,这种情况下拿不到节点就很奇怪.
由于项目的数据获取要求实时性,并不使用常规请求的方式,而是需要使用部门基于websocket封装的API,请求时需要先进行订阅后会才会返回对应数据。在页面组件和子组件中都是在useDidShow下先进行配置请求,useDidHidden进行销毁请求。
但在一些特定的场景下,切换页面发现useDidShow并不会进行触发,造成页面数据不更新的问题。
场景如下:问题页面为tabBar页面,在当前页切换SwiperItem,可正常切换到对应的SwiperItem轮播项,但是内部的数据并也不会变化。
因为每个SwiperItem页面都携带了大量的组件非常占用内存,保留全部的轮播项在低端机器上容易出现内存不足的问题,甚至无法在做到同时保留3个连续的SwiperItem.因此只有切换到对应的轮播项才进行条件渲染,非激活状态下销毁SwiperItem其他轮播项。
xml
<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-0 | B | A.onHide(), B.onLoad(), B.onShow() |
A-0 | B(再次打开) | A.onHide(), B.onShow() |
A-0 | A-1 | A-1.useEffect(), A.useEffect() |
C | A-0 | C.onUnload(), A-0.onShow() A.onShow() |
发现A页面将切换SwiperItem之后发现作为子组件的SwiperItem组件的onShow
生命周期并不会进行触发,仅仅会触发useEffect。
在不设置条件渲染的情况下,正常情况的跳转入A页面或者首次进入A页面都会触发全部的SwiperItem组件的onShow
生命周期。而SwiperItem组件切换并不触发onShow
生命周期.
因此需要修改订阅逻辑,在useEffect中也添加订阅,当然只执行useEffect和useDidShow其中一个即可,在useDidShow配置下属性,执行之后就不再执行useEffect.
获取节点信息
在tabber页面上跳转到一个配置tabber显示列表数据的设置页面,在该页面可以添加上一个页面的列表显示项,列表显示项由几个文本数据和一个Canvas组件构成,但是回到目标页面后,发现这个新的列表项已经被渲染,文本信息已经出现,但是新的Canvas却没有渲染出来。
通过分析代码后,当在配置页面添加列表显示项时,tabber页面就已经通过rudex拿到存储列表显示项目开始渲染,tabber页面获取到了对应的数据,该Canvas组件内部接受到了渲染的数据,但是始终拿不到node。
javascript
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
}
使用的createSelectorQuery本质是在对wx的select API做的一层封装,通过查询文档发现SelectorQuery API只能获取到当前页面的节点信息。当在配置页面自然就无法获取tabBar页面的node节点
通过在返回tabBar页面,在useDidShow和useDidHide配置刷新组件列表相关逻辑,当组件内部数量or信息变更时重新渲染组件列表,重新渲染的过程中就又能拿到对应的node节点,这样也避免了没有数据更新的情况,页面切换也会导致组件会更新的问题。
javascript
const show = useRef<boolean>(false)
useDidShow(()=>{
// 判断重新刷新组件列表逻辑
show.current = true
},[])
useDidHidden(()=>{
show.current = false
},[])
function App(){
return {show&&<View></View>}
}
获取页面信息
获取页面信息是非常常见的需求,在页面跳转之后能获取跳转页的页面信息,从而进行特定的渲染。但是在开发过程中,有时候就会发现拿到的页面信息有问题。
IOS上在支付后跳转或者扫码后的跳转就很容易出现这种问题。页面通过 useRouter() 获取参数,获取到的却是上一个的页面信息。切换成Taro小程序的Taro.getCurrentInstance() 获取当前页面实例,发现获取到的却也是上一个页面的信息。
scss
// 上一页面数据
const router = useRouter()
// getCurrentInstance().router 和 useRouter 返回的内容也一样
根据官方文档useLoaded在此生命周期中通过访问 options
参数或调用 getCurrentInstance().router
,可以访问到页面路由参数。 getCurrentInstance().router
其实是访问小程序当前页面 onLoad
生命周期参数的快捷方式。
怀疑是触发这些API进行页面跳转之后,小程序相关生命周期错乱导致的。所以换了另一种方式,直接去拿页面栈信息,获取栈顶的数据。
ini
const pages = Taro.getCurrentPages();
const currentPage = pages?.[pages?.length - 1];
const params = currentPage.options || {};
Canvas的使用
由于项目的页面需要展示的数据比较多,页面的节点数量也非常的多. 数据实时更新,交互上切换数据导致频繁的创建销毁,这些会带来比较严重的性能问题,尤其是在低端安卓设备上.
出于性能优化的目的,将一部分的用于展示数据的组件改造为Canvas.使用过程中发现存在一系列的问题. 一开始绘制Canvas会出现拿不到node的问题,总结下发现完成正常的绘制需要满足3个条件:
- 必须使用 Taro 提供的 Canvas
- Canvas 中必须包含类型 type="2d"
- 在useEffect中使用需要设置Taro.nextTick或者合理setTimeout,否则无法获得 canvas node 节点。
ini
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。
常规的阻止滚动穿透的方案如下:
- 为需要阻止滚动穿透的组件设置overflow:hidden,position:fixed,设置top值
- 外层的容器为ScrollView,出现弹出层配置scroll为false
- 为touchMove配置e.preventdefault(),e.stoppropagation()
在原生小程序中处理滚动穿透可以通过设置catchtouchmove,但是Taro3在小程序逻辑层实现了一套事件系统,包括事件触发和事件冒泡。但在小程序模板中绑定的事件都是以 bind
的形式,所以不能使用e.stopPropagation()
阻止滚动穿透.
当然也可通过配置css属性的方式进行处理,在需要进行阻止滚动穿透的场景下添加overflow:hidden,position:fixed,超出一屏的情况还需要额外记录下top值,已处理移除样式后页面跳到顶部的问题.有点麻烦了.
Taro提供了一个额外的属性catchMove,在处理ScrollView组件和固定高度的View非常好用.
sql
添加属性后View组件会绑定 catchtouchmove事件
<View catchMove></View>
场景 | 阻止滚动穿透的方法 |
---|---|
ScrollView组件 | 在ScrollView组件外层嵌套一个<View catchMove></View> 组件 |
高度固定且不会产生滚动的View组件 | 在View组件外层嵌套一个<View catchMove></View> 组件 |
会产生滚动的View组件 | 添加catchMove后,View本身不能进行滚动,肯定不能用这个方案 |