最近写项目时候,遇到了处理如何长列表的问题。记录一下优化方案。
分页
优点
能够快速跳转,方便回溯定位 分页更多用在B 端产品,假设使用分页,哪怕你已经浏览了十多多条数据,想回到某一数据的位置,只需要输入页码就好。如果是懒加载或者虚拟列表,你还需要手动滑动去寻找某一数据,速度非常慢。
缺点
用户不体验好 想要看新的数据,还需要手动去点击页码切换。
实现
前端
这里以react+antd为例
import { Table, message } from 'antd';
import { useState, useEffect } from 'react';
import axios from 'axios';
// 后端接口。查询用户发贴记录(查询用户文章列表)
const getPostsByCollectingAction = async (params) => {
return await axios.get('/api/users/collect', {
params
});
}
const columns = [
{
title: '帖子标题',
dataIndex: 'title',
render: (text) => <div className="title">{text}</div>
},
{
title: '收藏时间',
dataIndex: 'created',
},
];
const Collections = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
// 总条数
const [total, setTotal] = useState(0);
// 当前页码
const [current, setCurrent] = useState(1);
// 每页数据条数
const [pageSize, setPageSize] = useState(10);
const fetchData = async () => {
setLoading(true);
const { data, code, msg, total } = await getPostsByCollectingAction({
page: current - 1,
limit: pageSize
});
if (code === 200) {
setData(data.map((item) => {
const { pid, title, created } = item;
return {
key: pid,
title,
created,
}
}));
setTotal(total);
} else {
message.open({
type: 'error',
content: msg,
});
}
setLoading(false);
}
const handleOnChange = (page, pageSize) => {
setPageSize(pageSize);
setCurrent(page);
}
useEffect(() => {
fetchData();
}, [current, pageSize])
return (
<Table
columns={columns}
dataSource={data}
loading={loading}
pagination={{
current,
pageSize,
total,
onChange: handleOnChange,
showSizeChanger: true
}}
/>
);
}
export default Collections;
后端
后端接口要支持分页。新增两个参数page和limit。
参数:
| 参数名 | 必选 | 类型 | 说明 |
|---|---|---|---|
| page | 是 | string | 分页 |
| limit | 否 | string | 每页返回多少条数据,默认10条 |
返回示例
{
"code": 200,
"data": [{
"_id": "63e272c2ef48a999cd32af4a",
"pid": "63e12f8ebd7398910857e60f",
"title": "aa",
"created": "2023-02-06T16:49:18.287Z"
}],
"msg":"查询用户收藏的帖子成功",
"total": 1
}
返回参数说明
| 参数名 | 类型 | 说明 |
|---|---|---|
| code | int | 200-成功 |
| data | Array | 数据 |
| msg | string | 系统数据 |
| total | Number | 总数 |
返回参数中要有total,让前端分页器能确定要展示几个页码。
详情可以参考(接口文档地址)www.showdoc.com.cn/weshare/978…
懒加载
页面快滚动到底部时,自动向后端发请求拿数据,然后渲染到页面上。
优点
用户体验好 只要不断下滑就能继续看,不像分页那样总要被打断一下。更多用在C端产品。
缺点
不方便快速跳转,不方便回溯定位 假设你已经浏览了十多多条数据,想回到某一数据的位置,你还需要手动往回滑动去寻找某一数据,速度非常慢。
实现
如果是手动实现的话有两种方案。
方案1(手动实现)
首先将页面上的图片的 src 属性设为空字符串,而图片的真实路径则设置在data-url属性中, 当页面滚动的时候需要去监听scroll事件,在scroll事件的回调中,判断我们的懒加载的图片是否进入可视区域,如果图片在可视区内将图片的 src 属性设置为data-url 的值。
但这种方案有很明显的缺点,scroll 事件可能会被高频度的触发,并且在scroll事件里又去操作dom,页面便会重排和重绘,页面会卡顿。
方案2(手动实现)
利用 IntersectionObserver 接口,它可以异步监听目标元素与其祖先或视窗的交叉状态,它不随着目标元素的滚动同步触发,所以它并不会影响页面的滚动性能。
方案3(利用成熟的轮子)
这里用antd和react-infinite-scroll-component来实现滚动自动加载列表。
react-infinite-scroll-component这个库底层也是用了方案1的scroll来实现,不过在此之上做了很多优化的点去提高性能,比如对scroll事件进行节流。
import React, { useEffect, useState } from 'react';
import { Avatar, Divider, List, Skeleton, Tag, Space } from 'antd';
import { MessageOutlined } from '@ant-design/icons';
import { BASE_URL } from '../../../../service/config';
import InfiniteScroll from 'react-infinite-scroll-component';
import axios from 'axios';
// 后端接口
const getLitsAction = async (params) => {
return await axios.get('/api/public/lists', {
params
})
}
// 分页,每次加载20个
const limit = 20;
// 点击加载更多,loading状态时页面显示骨架屏的个数
const skeletonNumber = 1;
export const tagConfig = {
index: {
name: "首页",
color: "green"
},
ask: {
name: "提问",
color: "magenta"
},
advise: {
name: "建议",
color: "gold"
},
discuss: {
name: "交流",
color: "lime"
},
share: {
name: "分享",
color: "geekblue"
},
logs: {
name: "动态",
color: "purple"
},
notice: {
name: "公告",
color: "cyan"
},
}
const MyList = ({ header, className, catalog, isTop, isEnd, sort, key }) => {
// 分页第几页
const [pageSize, setPageSize] = useState(0);
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [total, setTotal] = useState(0);
const loadMoreData = async () => {
if (loading) {
return;
}
setLoading(true);
const result = await getLitsAction({
isTop,
catalog,
limit,
page: pageSize,
isEnd,
sort
})
const { code, total } = result;
if (code === 200) {
setPageSize(pageSize + 1);
setData([...data, ...result.data]);
setTotal(total);
}
setLoading(false);
}
useEffect(() => {
loadMoreData();
}, [catalog, isEnd, sort]);
return (
!(total === 0 && isTop === '1')
&&
<InfiniteScroll
dataLength={data.length}
next={loadMoreData}
hasMore={data.length < total}
loader={
<Skeleton
avatar
paragraph={{
rows: skeletonNumber,
}}
active
/>
}
endMessage={isTop == '0' && <Divider plain>没有更多了🤐</Divider>}
>
<List
key={key}
header={header}
className={className}
itemLayout="horizontal"
dataSource={data}
locale={{ emptyText: "快去发表第一条帖子吧!" }}
style={{ marginBottom: "10px" }}
renderItem={(item) => {
const { uid: user } = item;
return (
<List.Item
className='list-item'
actions={[
...item?.tags?.map(({ name, class: color }) =>
<Tag color={color || "#87d068"}>{name}</Tag>
),
<div className='list-item-favs'><MessageOutlined />{item.answer}</div>,
]}
>
<Skeleton avatar title={false} loading={item.loading} active>
<List.Item.Meta
avatar={<Avatar src={`${BASE_URL}${user.pic}`} />}
title={
<>
<Tag color={tagConfig[item.catalog].color}>
{tagConfig[item.catalog].name}
</Tag>
<span className='list-item-title'>{item.title}</span>
</>
}
/>
</Skeleton>
</List.Item>
)
}
}
>
</List>
</InfiniteScroll>
);
}
export default MyList;
虚拟列表
假如ul下有5个li,当你往下滚的时候,虽然数据发生更新了,但是还是只有5个li,所以叫虚拟列表。
而使用懒加载的话,往下滚的时候,页面就不止5个li,可能有10几个li了。
优点
懒加载在一定程度上没有解决长列表的DOM操作带来的性能问题。而虚拟列表解决了这个问题。
举个例子,你打开知乎首页,一直往下拉(知乎用的就是懒加载),当你拉到1000个之后,你挑一个点击赞同,你会发现,页面很卡顿。超多DOM的造成回流重绘,性能问题无法避免。
而虚拟列表会较好的解决这个问题,因为DOM一直就这么点。
实现
todo
项目地址
WeShare是一款发帖分享论坛。
🚀 登录模块实现了用户注册、登录,忘记密码(通过邮箱验证找回)、修改密码功能
🚀 帖子模块实现了发帖、删帖、收藏帖子、评论回复、评论点赞功能
🚀 用户模块实现了修改用户资料、展示用户资料、每日签到获取积分功能
项目亮点
🚀 采用jwt+localStorage进行会话管理,实现了用户在期限内免登录的效果
🚀 利用axios请求拦截器解决异步请求竞态问题
🚀 对首页帖子展示实现滚动加载(懒加载)