前言

搜索列表页是大家经常接触的页面,如果做好一个列表页其实是不容易的,因为细节的地方很多;最近做了很多列表搜索页面,关于页面有倒计时并且刷新状态的需求做了不少,总结出一点经验,与大家分享,希望能对大家有所帮助。

关于首屏渲染

同步接口渲染页面

  • 方案一:页面是接口同步请求,信息合并,渲染页面。即从A接口获取一个基本信息list,然后依赖A接口的数据,再请求B接口,B接口返回的数据和A接口的数据进行融合,渲染页面;

方案一逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//list是融合接口A和接口B之后的数据
const [list, setList] = useState<IGood[]>([]);

getResFloor().then((resA) => {
goodsUpdate(resA);
});
});
//融合接口数据,设置list
goodsUpdate(list).then((resB) => {
let bridgeObj = {}
list.map((item, index) => (
item = Object.assign(item, resB[item.id])
))
setList(list);
});
});
list.map((item, index) => (
<li key={item.id}>
<Good item ={item} refresh={refresh}/>
</li>
))

  • 方案二:从A接口获取基本信息list后,直接用A接口的list渲染页面,同时拿A接口数据去请求B接口,等B接口数据回来后,将B接口的数据更新渲染页面;

方案二的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//list是基本信息;objCurr是依赖基本信息获取到的实时信息
const [list, setList] = useState<IGood[]>([]);
const [objCurr, setObjCurrent] = useState<{}>({});
getResFloor().then((resA) => {
setList(resA);
goodsUpdate(resA);
});
});
//融合接口数据,设置list
goodsUpdate(list).then((resB) => {
setObjCurrent(resB);
});
});
list.map((item, index) => (
<li key={item.id}>
<Good listCurrItem={objCurr[item.id]} item ={item} refresh={refresh}/>
</li>
))

比较方案一和方案二,不难发现,方案二的处理方式更好,因为提高了首屏的渲染速度,在A基本信息接口数据回来后就直接渲染页面,不再等待B接口回来一起渲染;减少了B接口的等待时间,页面上依赖B接口的数据可以先给默认值或者为空;这样加载页面的时候展示给用户的就是有基本的信息,一些实时信息等B接口返回后再渲染。

倒计时开启

  1. 常规操作倒计时,即在Good组件上开启:这样页面上每个卡片都有自己的倒计时,互不干扰,相互独立;
  • 缺点:页面卡片很多时,会开启很多倒计时,影响性能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    // Good.tsx中
    function Countdown(props: Props) {
    let cd: any = null;
    const startTimeStr = forMatTime(props.startTime)
    const [strTime, setSecond] = useState(startTimeStr);
    const { auctionStatus} = props;
    let time = props.remainTime;

    const tick = () => {
    time = time - 1000;
    if (time > -1000){
    if (auctionStatus === 1){
    const strTimeMe = forMatRemainTime(time)
    setSecond(strTimeMe)
    }
    }else{
    clearInterval(cd);
    cd = null
    props.refresh()
    }
    };

    useEffect(() => {
    clearInterval(cd);
    cd = null
    const { remainTime} = props;
    if(remainTime > 0){
    cd = setInterval(tick, 1000);
    }else{
    if(auctionStatus < 2){
    setTimeout(()=>{props.refresh()},600)
    }else{
    setSecond(forMatTime(props.endTime))
    }
    }
    return () => {clearInterval(cd);cd = null}
    }, [props.remainTime]);

    return (
    <div className="broke-countdown">
    {props.auctionStatus === 1 ? (<span style={{ paddingRight: 5 }}>距离结束预计:</span>) : (props.auctionStatus === 0 ? (<span style={{ paddingRight: 5 }}>开始时间:</span>) : (<span style={{ paddingRight: 5 }}>结束时间:</span>))}
    <em>
    {strTime}
    </em>
    </div>
    );
    }
  1. 在获取到数据之后,开启一个倒计时:
  • 优点:页面只开启一个倒计时,大大节约性能;
  • 缺点;如果页面数据很多,每一秒轮循一次列表可能造成倒计时设置一秒但是实际时间大于1秒,且数据很多的时候也会有性能问题;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    const timerW = setInterval(() => {
    let Ids:any[] = [];
    listRef.current.forEach((item: any) => {
    if (item.auctionStatus === 1 || item.auctionStatus === 0) {
    item.remainTime -= 1000
    if (item.remainTime <= 0) {
    Ids.push(item.paimaiId)
    }
    }
    })
    if (Ids.length !== 0){
    getGoodStatus(Ids).then((result: any) => {
    if(result && result.length > 0) {
    result.map((item:any) => {
    listRef.current = listRef.current.map((_:any)=> {
    if(_.id === item.id) {
    _ = Object.assign({}, _, item)
    return _
    }
    return _
    })
    })
    }
    })
    }
    setList([...listRef.current])
    }, 1000)

获取当前屏

在M端列表数据很长的时候,不管是单独开启倒计时还是页面只开启一个倒计时,都会有性能问题,所以当数据超过1000条时,一般我们需要获取当前屏幕数据做处理,而不是操作整个列表数据。
获取当前屏幕的优点显而易见,它大大减少了数据操作的长度,但是也有缺点,因为你需要不断的去获取当前屏幕的dom,或者说视窗窗口的index下标,这也额外的增加了不必要的非业务逻辑;所以我们建议当已知列表的长度可能超过1000条数据时,再开启获取当前屏方法;

  1. 卡片高度固定:如果列表的每个卡片高度固定,直接计算的list的父盒子top值除以单个卡片高度;计算出当前屏幕内的index;此方法简单,性能好;缺点是一旦需求卡片高度不固定,就不可以使用;
1
2
3
4
5
6
7
//获取视窗窗口起始index
const content = document.querySelector('.index__main')
if (content) {
const { top } = content.getBoundingClientRect()
let INDEX_ = Math.floor((top)/this.ITEM_HEIGHT)
this.WINDOW_ITEM_INDEX = INDEX_ < 0 ? 0 : INDEX_
}
  1. 卡片高度不固定:如果卡片高度不固定,可以进行代码修改,并不复杂;即修改代码中WINDOW_ITEM_INDEX值即可;(逻辑:获取当前list的父盒子top值,按照卡片可能的最小及最大高度,分别计算出可能在屏幕内的minIndex及maxIndex;截取总list的minIndex和maxIndex获得lessList,滚动事件里面不断的for循环lessList,比较lessList中每一项—–需要提前给每一个dom添加上唯一id,使用id获取dom,并计算dom是否在窗口内——-得到窗口内的dom下标i,最终的WINDOW_ITEM_INDEX = i + minIndex;即得到当前屏幕的index)
  • 代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //在滚动函数中执行
    const content = document.querySelector('.index__main')
    let minIndex = 0;
    let maxIndex = allPageList.length -1;
    let WINDOW_ITEM_INDEX = 0;
    let lessList = allPageList
    if (content) {
    const { top } = content.getBoundingClientRect()
    minIndex = Math.floor(top/ITEM_HEIGHT_Max)
    maxIndex = Math.floor(top/ITEM_HEIGHT_Min)
    lessList = allPageList.slice(minIndex, maxIndex)

    for (var i = 0; i < this.lessList.length; i++) {
    let id = i + minIndex + '_' + lessList[i].paimaiId
    let elem = document.getElementById(id);
    if (elem) {
    var bottom = elem.getBoundingClientRect().bottom;
    var clientHeight = (document.documentElement || document.body).clientHeight;
    if (bottom > 0 && bottom < clientHeight) {
    WINDOW_ITEM_INDEX = i + minIndex
    break;
    }
    }
    }
    }

根据视窗窗口优化列表操作

获取到当前屏幕是index之后,关于倒计时我们可以只开启当前屏幕的,屏幕之外的倒计时就不再开启,在页面滚动的时候不断的更新要开启的倒计时list即可

需要注意的是,当屏幕之外的数据停止倒计时后,再开启的时候,倒计时就不能用之前的剩余时间,需要重新获取数据,拿到当前剩余时间再开启倒计时;所以当页面滚动的时候不仅需要不断的更新当前那段list需要开启倒计时,同时需要不断的请求新的数据以更新倒计时时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
this.timer = setInterval(() => {
//为了性能考虑,不全部启动倒计时,只启动可视窗口的倒计时
const {paiListData, auctionType, sortField, recommendList} = this.state
let countArr = []
const content = document.querySelector('.index__main')
const contentItem = document.querySelector('#idid')
if(contentItem){
const { height } = contentItem.getBoundingClientRect()
this.ITEM_HEIGHT = height > 100 ? -height : -160
}
if (content) {
const { top } = content.getBoundingClientRect()
let index = Math.floor((top+3300)/this.ITEM_HEIGHT)
let INDEX_ = index < 0 ? 0 : index
countArr = paiListData.slice(INDEX_, INDEX_+40 )
for(let i=0;i<countArr.length; i++){
let fId = i+INDEX_
let sId = countArr[i].id
let ii = fId+'_'+sId
if(this.countdownFunc[ii]){
const {callback, self} = this.countdownFunc[ii];
callback(self);
}
}
let nowRefreshI = Math.floor((top+600)/this.ITEM_HEIGHT)
let nowRefreshIndex = nowRefreshI < 0 ? 0 : nowRefreshI
//如果检查到当前屏幕和批量更新数据的index有差别(3个),则重新请求更新数据,避免因30秒内滚动页面,当前屏更新延迟
if(nowRefreshIndex == 0 && this.WINDOW_ITEM_INDEX != 0){
this.refreshViewData(nowRefreshIndex)
}else{
if( Math.abs(nowRefreshIndex-this.WINDOW_ITEM_INDEX) > 2 ){
this.refreshViewData(nowRefreshIndex)
}
}
}


//

}, 1000)

数据缓存

最后说到列表就不能不提起数据缓存,因为列表在点击进入详情的时候,返回都要定位到之前的位置,并且当页面滚动很多的时候不可能把数据全部重新加载一遍,所以就需要缓存。
本文章不建议列表使用localStory缓存数据,代替使用session。

  1. 点击卡片,需要记住当前的top值及当前列表数据

    1
    2
    3
    4
    5
    6
    7
    8
    //
    const content = document.querySelector('.index__main')
    if (content) {
    const { top } = content.getBoundingClientRect()
    window.sessionStorage.setItem('pageTop', JSON.stringify(top));
    window.location.href = url
    }
    window.sessionStorage.setItem('list', JSON.stringify(this.list));
  2. 当页面返回有缓存的时候,使用缓存

    //
    let mainkey = sessionStorage.getItem('list')
    if(window.sessionStorage && mainkey){
     let top = Number(sessionStorage.getItem('pageTop'));
     let ListDataStr = sessionStorage.getItem(mainkey);
     let ListData = JSON.parse(ListDataStr);
     this.setState({
       list: ListData,
       isLoading: false
     },()=> {this.toJump(top)})
    }
    
    //使用完缓存之后,将缓存清除
    toJump = (top: number): void => {
     let scrollElement = document.querySelector('#app') // 对应id的滚动容器
     if(scrollElement) {
           scrollElement.scrollTop = -top
           sessionStorage.removeItem('list');
           sessionStorage.removeItem('pageTop');
           // sessionStorage.clear();
     }
    }