version: 3.5.8
platform: h5、weapp
最近有个需求,需要给所有的列表页面加上下拉刷新的功能。记录下使用到的ScrollView组件踩到的坑和解决方案。
下拉刷新
taro框架如果需要在某个页面开启下拉刷新,首先需要在页面配置文件中添加配置:
ts
// xxx/index.config.ts
export default {
navigationBarTitleText: '页面标题',
// 加上这一行
enablePullDownRefresh: true,
};
添加该配置文件后需要重新启动taro。一般页面中,只需要再对下拉刷新的事件回调做出响应即可:
tsx
//class 版本
class PageA extends Component{
// 添加该生命周期响应下拉事件
onPullDownRefresh(){
//调用Taro.stopPullDownRefresh 停止下拉效果
fetcher().then(()=>Taro.stopPullDownRefresh())
}
}
// hooks版本
const PageB=()=>{
usePullDownRefresh(()=>{
//调用Taro.stopPullDownRefresh 停止下拉效果
fetcher(params).then(()=>Taro.stopPullDownRefresh())
});
}
当然如果需要主动触发下拉效果,则可以这么写:
tsx
const handlerPullDownRefresh=()=>{
Taro.startPullDownRefresh();
fetcher().then(()=>Taro.stopPullDownRefresh())
}
只需要注意必须显示调用Taro.stopPullDownRefresh
才能停止整个页面下的下拉效果。
如果页面上存在taro官方的ScrollView组件,那么默认情况下你会发现:
- ScrollView之外的部门可以正常下拉刷新。
- ScrollView手指下滑正常。
- ScrollView手指往上滑时,触发下拉刷新,不再滚动。
这个问题无法参考taro官方提供的某个解决方案。究其原因,还是滚动穿透。之前没有过类似的需求,所以内部的组件库中滚动组件是这样修复这个问题的:
tsx
class XXXScrollView extends Component<JdyScrollViewProps> {
render() {
// H5开启下拉刷新,scrollView的上滑手势失效
return (
<ScrollView {...this.props} onTouchMove={(e) => {
e.stopPropagation();
}}>
{children}
</ScrollView>
);
}
}
直接阻止滚动组件的滚动事件冒泡,改成这样的效果:
- 滚动区正常滚动。
- 滚动区无法触发页面下拉刷新。
由于滚动区一般会占用页面大部分空间,滚动区内无法触发该效果是无法忍受的,好在社区提供了一种解决方案。
我们简单调整一下:
tsx
interface Props extends ScrollViewProps {
}
const Index: FC<Props> = (props) => {
const scrollTop = useRef(0);
return (
<ScrollView
{...props}
onTouchMove={(e) => {
if (scrollTop.current !== 0) {
e.stopPropagation();
}
}}
onScroll={(e) => {
scrollTop.current = e.detail.scrollTop;
}}
>
{children}
</ScrollView>
);
};
现在可以做到:
- h5端一切正常。
- 微信小程序端scrollView部分无法触发下拉刷新,但是可以正常滑动。
微信小程序适配
首先讲下结论:小程序下使用原生scrollView是很难做到页面下拉刷新的。这里只适配scrollView的下拉刷新。
taro官方已经提供了小程序端的滚动区下拉刷新参数,只需要给scrollView组件设置:refresherEnabled、refresherTriggered、onRefresherRefresh即可开启scrollView的下拉刷新。遵循依赖注入的原则,我们这样拓展:
tsx
const Index: FC<Props> = (props) => {
const { children, refreshFetcher = Promise.resolve } = props;
const scrollTop = useRef(0);
const [refreshStatus, setStatus] = useState(false);
return (
<ScrollView
{...props}
// 醒目注入
refresherEnabled={props.refresherEnabled}
refresherTriggered={refreshStatus}
onRefresherRefresh={() => {
/**
提供了refresherTriggered参数后需要显示修改才能触发组件的下拉刷新。
*/
setStatus(() => true);
refreshFetcher().finally(() => setStatus(() => false));
}}
onTouchMove={(e) => {
if (scrollTop.current !== 0) {
e.stopPropagation();
}
}}
onScroll={(e) => {
scrollTop.current = e.detail.scrollTop;
}}
>
{children}
</ScrollView>
);
};
然后我们这样调用:
tsx
<PullDownScrollView
className='flex-column'
scrollY
style={{ height: '100%' }}
onScrollToLower={() => fetchList(filterParams)}
refresherEnabled
refreshFetcher={() => fetchList(filterParams, true)}
>
这样,即可在微信小程序端实现:
- 下拉页面其他部分,整个页面下拉刷新。
- 从滚动组件顶部下拉,滚动组件出现下拉刷新。
- h5端不会出现滚动组件的下拉刷新,只存在整个页面下拉刷新。
当然,如果需要保持一致,taro提供了一些下拉刷新的参数可以进行调整;逻辑上可以实现隐藏滚动组件的下拉刷新并且手动触发(Taro.startPullDownRefresh)页面的下拉刷新的。不过除了样式问题还有页面的过渡问题,比如下拉滚动区只有滚动区会往下过渡,此时手动触发页面刷新界面交互会非常混乱。
scrollView 中存在tab的情况
这个是一个非常特殊的情况,它的组件逻辑是:
tsx
<ScrollView>
<Tab> </Tab>
</ScrollView>
在这种情况下会触发bug:
- 进入页面,展示tab A,tab A高度 100px,此时滚动区可滚动高度100px。
- 切换到tab B,tab B高度 200px,此时滚动区可滚动高度300px,出现白色不可选的幽灵区。
这个bug的原因非常简单,就是微信小程序会计算position 不是static的节点,而taro的tab切换实现是把其他tab的position换成absolute,然后给一个很大的left等参数隐藏。当然我们没办法修改。
网上提供了两种解决方法:
- 修改组件逻辑,把ScrollView组件放到tab组件内,有多少个tab就有多少个ScrollView组件。
- 不再使用ScrollView,使用View自己实现一个CustomScrollView。
但是这里不得不夸一下taro官方,只要你看过react的官方文档,就应该知道修改一个组件的key值,会强制该组件重新渲染。依靠该思路,当我们给ScrollView尝试设置key值时,组件的jsdoc提示我们:
taro官方早就提供过解决方案了。这里我们做一下优化:
tsx
const isWeapp=()=>Taro.getEnv() === Taro.ENV_TYPE.WEAPP;
const [curIndex,setIndex]=useState(defaultIndex);
const modifiedScrollKey=isWeapp()?curIndex:'someThingNotChanged';
return <ScrollView
key={modifiedScrollKey}
>
{...}
</ScrollView>
最终效果
- h5端所有情况正常。
- 微信小程序端所有情况正常(除了有两种下拉方式)。
最终的封装代码:
tsx
import { FC, memo, useRef, useState } from 'react';
import { ScrollView, View, ScrollViewProps } from '@tarojs/components';
import Taro from '@tarojs/taro';
interface Props extends ScrollViewProps {
/** 仅针对微信小程序的下拉刷新适配 */
refreshFetcher?: () => Promise<any>;
}
/**
* @description taro自身缺陷,同时开启页面下拉刷新时,滚动区无法上去,
* 根据issue13697的方案进行polyfill。
* 请注意小程序的下拉刷新和想象中的有些不同!需要额外注入参数实现
* @link https://github.com/NervJS/taro/issues/13697
*/
const Index: FC<Props> = (props) => {
const { children, refreshFetcher = Promise.resolve } = props;
const scrollTop = useRef(0);
const [refreshStatus, setStatus] = useState(false);
return (
<ScrollView
{...props}
refresherEnabled={props.refresherEnabled}
refresherTriggered={refreshStatus}
onRefresherRefresh={() => {
setStatus(() => true);
refreshFetcher().finally(() => setStatus(() => false));
}}
onTouchMove={(e) => {
if (scrollTop.current !== 0) {
e.stopPropagation();
}
}}
onScroll={(e) => {
scrollTop.current = e.detail.scrollTop;
}}
>
{children}
</ScrollView>
);
};
export default memo(Index);