乐趣区

一次react滚动列表的实践—兼容ios安卓

一、背景
近期项目改版,对原有的 h5 页面进行了重新设计,数据呈现变成了瀑布流。希望新版兼容 ios 和安卓两端的情况下,无限制的刷新加载数据。大致效果如下:
![图片描述][1]
整个页面分 4 部分:

顶部导航
步数状态卡片
用户信息卡片
滚动列表

期望效果:列表滚动到用户信息卡片消失后,展示另一个吸顶的导航栏。
效果如下:
![图片描述][2]

分析可以发现,首先我们要做的就是适配 iPhone X,其次我们需要监听列表的滚动高度,在 pc 和安卓上监听滚动事件是没有问题的,但是 ios 上滚动过程中不会触发 scroll 事件,而是滚动结束后会触发 onscrollend 事件,这就不能满足实时监听高度的要求。经过简单调研,决定站在巨人的肩膀上,通过 iscroll、better-scroll 等 js 库实现。这两个库都是解决各种滚动兼容的 js 库,很多常见的轮播、picker 组件都是基于这些库封装的。顺便说一句,还有个库也不错(simulation-scroll-y)
二、进入正题
1. 适配 iPhone X
PhoneX 的适配,在 iOS 11 中采用了 viewport-fit 的 meta 标签作为适配方案;viewport-fit 的默认值是 auto。react app 的渲染内容都在 id 为 root 的 div 里面。我们给这个 div 加上 iphoneX 的 safe-area-inset 属性即可。更多相关内容,这篇文章写的挺详细
<meta name=’viewport’ content=’width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover’ />
#root{
height:100vh;
padding-top: constant(safe-area-inset-top);
padding-left: constant(safe-area-inset-left);
padding-right: constant(safe-area-inset-right);
padding-bottom: constant(safe-area-inset-bottom);
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}

2. 页面
页面结构不多说,比较基础。div.container-> div#wrapper->div.list->div.list-item 值得注意的是,wrapper 需要设置绝对定位。同时,通过 transform: translateZ(0); 开启硬件加速,浏览器在渲染时会通过 GPU 进行渲染。有效缓解安卓端滚动卡顿的问题,类似的 css 还有不少,css 硬件加速不要滥用,否则会导致不该使用 gpu 的 layer 使用 gpu,占用内存过高,导致页面卡顿,甚至黑屏,一般情况下,给不同的硬件加速元素添加一个不同的 z -index 属性可以解决。-webkit-overflow-scrolling: touch 使 ios 滚动顺滑。// 初始化 BScroll 伪代码,生产慎用:
import BScroll from ‘better-scroll’;

this.myScroll = new BScroll(‘#wrapper’, {
mouseWheel : true,
// 无需 scrollbar
scrollbar : false,
// propType 属性设置为 3 在惯性动画期间也触发 onscroll 事件
probeType : 3,
// 允许滚动列表内可点击、touch
click : true,
tap : true,
// 上拉加载,正值自动触发加载
pullUpLoad : {
threshold: 50
}
});
一开始,我将 better-scroll 初始化代码放在 container 组件的 componentDidMount 函数中,但由于初始数据也在这个函数获取,导致当返回较慢的时候初始化的 #wrapper 没有内容,此时需要手动点击加载更多才展现数据,不符合预期。所以考虑将初始化代码放到 list 组件渲染完成之后的 componentDidUpdate 函数中。list 组件渲染完成后,就可以初始化我们的滚动类,这里使用的 better-scroll,iscroll 使用类似。具体参考上面链接。

#wrapper {
position:absolute;
top:0;
left:0;
width:100vw;
overflow:auto;
height: 100vh;
transform: translateZ(0);
z-index: 33;
-webkit-overflow-scrolling: touch
}
具体的,可以将初始化代码放在 list 组建的 container 组件的 handleScrollRefresh 函数。这个函数作为 props 传到 list 组件,在 list 组件的 componentDidUpdate 钩子里面执行:container 组件:
handleScrollRefresh () {
if (this.myScroll) {
this.myScroll.refresh();
console.log(‘refreshed ‘);
} else {
console.log(‘initialized’);
this.myScroll = new BScroll(‘#wrapper’, {
…// 初始化参数
});
this.myScroll.on(‘scroll’,this.handleScroll, 10);
this.myScroll.on(‘pullingUp’, this.loadMore);
}
}
list 组件:
componentDidUpdate () {
if (this.props.onRefresh) {
this.props.onRefresh();
}
}
网上很多滚动卡顿的情况,大都是加载数据后没有执行 refresh 导致的。同时,加载数据成功后我们需要调用 scroll 的 finishPullUp 方法。下次上拉才能继续加载数据。这样,每当加载新的数据后,list 组件就会执行 componentDidUpdate,此时就调用了 scroll 的 finishPullUp、refresh 函数,使用起来无比顺滑。
三、优化

和大多数滚动处理一样,better-scroll 的 scroll 事件也会频繁触发,这对性能还是有一定影响的,毕竟我们不需要过于频繁的执行回调函数。
throttle (func, delay) {
let lastTime = null;
return function () {
let context = this;
let args = arguments;
let now = new Date().getTime();
if (!lastTime || (now – lastTime) > delay) {
lastTime = now;
func.apply(context, args);
}
};
};
不想写直接使用 lodash 也可以:
// 不精准的每秒十次
this.myScroll.on(‘scroll’, this.throttle(this.handleScroll, 100));

函数绑定,不传参的情况下在 constructor 中绑定 this。而不是在 render 中使用 this.xxx.bind(this)。
list 图片大小限制,本次由于部分列表 item 图片过大,在安卓上导致黑屏的问题出现。排查了很久才发现这个问题。通过在图片 url 拼接参数限制大小解决了这个问题。

最后
感觉写得好乱,做事情和写文章果然是两回事。。。

退出移动版