【前端 | 实践】记一次简单的分页请求

为什么做分页

从网络层面上看:
数据量过大,会造成报文长度过大,从而导致网络传输速度变慢

从渲染层面上看:
一次性插入过多DOM节点,会增加页面的渲染压力

怎么做分页

跟前端相关的分页逻辑主要可以分为以下几个阶段:

  • 监听滚动
  • 发送、接收请求
  • 处理、更新数据

1、监听滚动

首先,我们需要设置 触发分页请求的条件:即 页面被滚动到底部
与之相关的几个物理量:

  • document.documentElement.scrollTop:页面在竖直方向上滚动过的距离(scrollTop)
  • document.documentElement. clientHeight:屏幕窗口的绝对高度(windowHeight)
  • document.documentElement.scrollHeight:页面中所有内容的高度(包括由于溢出导致的视图中不可见的内容)(scrollHeight)

当满足以下条件时,可以判定页面滚动到了底部

scrollTop + windowHeight >= scrollHeight
复制代码

这部分的代码如下:

const THROTTLE_WAIT = 20; // 节流的等待间隔
​
useEffect(() => {
    const listenScroll = throttle(() => {
      const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; 
      const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
      const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
​
        // 到达页面底部
        if (scrollTop + windowHeight >= scrollHeight) {
          getMoreData();
        }
    }, THROTTLE_WAIT);
​
    window.addEventListener('scroll', listenScroll);
    
    return () => {
      window.removeEventListener('scroll', listenScroll);
    }
  }
}, [])
复制代码

2、发送、接收请求

分页请求的”分页“逻辑,需要前后端协同处理:

  • 有序:主要依靠后端实现
  • 不重复:前后端协同处理

之所以可能出现数据的重复,是因为:在两次请求之间可能会有新数据的插入

常见的解决方案如下:

  • 方案1:前端请求第start到第start+size条数据,后端筛选出size条数据并返回,前端依靠Id做去重
  • 方案2:前端发送当前页面上 最后一条数据的id,后端只返回 排在这条数据之后的数据,从而保证不重复
  • 方案3:逆序显示,能保证不重复且数据完整,但违反直觉(最新的数据在列表最底部)

目前采用第一种方案

在发送请求这一阶段,要特别注意的是:避免短时间内重复发送请求
由上述代码可以发现,触及页面底部 会触发getMoreData方法的执行
由于接收请求/渲染数据需要一定的时间,所以需要限制这段时间内 不再发送分页请求

可以通过在redux中 维护一个布尔类型的loading变量:

  • 发送请求时,将loading置为true
  • 请求成功or失败,将loading置为false

在每一次发送分页请求之前,先进行条件判断:

const PAGE_SIZE = 10;
​
const needGetMoreData = (isEnd: boolean, loading: boolean) => !isEnd && !loading;
​
const getMoreData() = () => {
  if (needGetMoreData(reduxState.data.isEnd, reduxState.loading)) {
    // 发送分页请求
    props.dispatch(getList({
      start: reduxState.data.list.length,
      size: PAGE_SIZE,
    }))
  }
}
复制代码

注意:
分页请求中,给参数start赋值时,必须使用当前渲染列表的长度

3、处理、更新数据

3.1、数据去重

我们希望将去重的逻辑封装成一个公用的方法,将标识字段 交给业务方去指定
思路如下:

  • 维护一个哈希表hashMap,记录当前list中的所有id
  • 遍历 新请求得到的list,拿出每个元素的id
  • 若hashMap中存在id,就过滤掉这个元素
  • 若hashMap中不存在这个id,就把这个元素加入列表中,并更新hashMap

代码如下:

function removeDuplicate<T>(
  originList: T[]= [],
  hashMap: Map<string, number>,
  prop: keyof T
): T[] {
  const newList: T[] = [];
  originList.forEach((item) => {
    const id = String(item[prop]);
    if (hashMap.get(id) === undefined) {
      newList.push(item);
      hashMap.set(id, 1);
    }
  })
  return newList;
}
复制代码

为了应对不同的业务场景:

  • 定义泛型变量T,用于捕获列表的类型(在调用的时候传入)
  • 使用keyof关键字去定义prop的类型,用于限制prop必须是类型T中的一个字段名称

3.2、数组拼接

初始化redux的state:

const reduxState = {
  loading: false;
  data: {
    list: [];
    isEnd: false;
  },
  hashMap: new Map(),
}
复制代码

在reducer中 定义更新逻辑:

const UNIQUE_ID = 'orderId'; // 用于做数据去重的标识字段
​
function reducer(state = reduxState, action) {
  const update = {};
  let newList: List[];
  
  switch (action.type) {
    case GET_LIST_REQ:
      update.loading = true;
      break;
    case GET_LIST_SUCCESS:
      update.loading = false;
      newList = removeDuplicates<List>(action.data?.list || [], state.hashMap, UNIQUE_ID);
      update.data = action.data;
      update.data.list = [...state.data.list, ...newList];
      break;
    case GET_LIST_FAIL:
      update.loading = false;
      break;
    default:
      break;
  }
​
  return {
    ...state, ...update,
  };
}
复制代码

需要注意的地方:
(1)update语句的赋值顺序
请求成功时,我们先用action.data去覆盖update.data,再用解构运算符 做数组的拼接
目的是 确保数组list以外的数据能够全部更新(例如:isEnd变量)
(2)reducer返回值的书写
要把更新的数据放在后面:

return {
  ...state, ...update
}
复制代码

写在最后

鉴于笔者只是一名未毕业的学生,实践经验非常有限,有写的不对or不好的地方欢迎大家在评论区批评指正!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享