1. 前言

在移动应用开发里,列表数据的分页加载是一个高频需求。用户在浏览列表时,为了提升体验,通常希望滚动到底部就能自动加载更多数据。 RuiPaging 组件正是基于此需求开发,它封装了分页加载的通用逻辑,借助 Taro 框架,可适配多端应用,帮助开发者快速实现列表的分页加载功能。

2. 实现分析

  1. 组件定义与类型声明:借助泛型 T 提升组件的灵活性,使组件能处理不同类型的列表数据。利用 forwardRef 让组件可以接收 ref ,方便父组件调用组件内部的方法。同时定义了 RuiPagingProps 和 RuiPagingRef 接口,明确组件的属性和可调用方法的类型。
  2. 状态管理,使用 useState 管理四个状态: 2.1 page :记录当前页码,初始值为 1。 2.2 isEnd :标记是否已加载完所有数据,初始值为 false 。 2.3 isLoading :标记是否正在加载数据,避免重复请求,初始值为 false 。 2.4 list :存储列表数据,初始值为 [] 。
  3. 自动加载逻辑:在 useEffect 钩子中,若 auto 属性为 true 且当前没有在加载数据,就触发数据加载操作。
  4. 滚动加载逻辑:通过 ScrollView 组件的 onScrollToLower 事件监听滚动到底部的操作,若未加载完所有数据且当前没有在加载数据,就触发新数据的加载。
  5. 数据处理逻辑 5.1 complete 方法:处理数据加载完成后的逻辑,更新列表数据和页码,判断是否加载完所有数据。 5.2 reload 方法:重置状态并重新加载第一页数据。
  6. 方法暴露:使用 useImperativeHandle 暴露 complete 和 reload 方法,方便父组件调用。

3. 类型定义

  1. 定义一个空的ListItem接口,作为列表项的基本类型;
  2. 导出RuiPagingProps接口,用于定义分页组件的属性,使用泛型T继承ListItem;
  3. 可选的value属性,类型为T数组,用于传递初始数据;
  4. 可选的auto属性,类型为boolean,用于控制是否自动加载第一页数据;
  5. 必需的onChange回调函数,当列表数据变化时调用,接收T数组作为参数;
  6. 可选的size属性,类型为number,用于设置每页数据条数;
  7. 必需的onQuery回调函数,用于触发数据查询,接收页码和页面大小作为参数;
  8. 必需的children属性,类型为React.ReactNode,用于接收子组件。
  9. 导出RuiPagingRef接口,用于定义分页组件的引用方法,使用泛型T继承ListItem;
  10. complete方法定义,用于完成数据加载,接收T数组或布尔值作为参数;
  11. reload方法定义,用于重新加载数据,无参数。

interface ListItem {} export interface RuiPagingProps<T extends ListItem> { value?: T[]; auto?: boolean; onChange: (e: T[]) => void; size?: number; onQuery: (page: number, size: number) => void; children: React.ReactNode; } export interface RuiPagingRef<T extends ListItem> { complete: (value: T[] | boolean) => void; reload: () => void; }

4. 传入变量和状态

  1. 使用解构赋值语法从props中提取组件需要的属性;
  2. 设置auto属性默认值为true,表示组件挂载后自动加载第一页数据;
  3. 设置onChange回调函数默认为空函数,当数据发生变化时触发;
  4. 设置size属性默认值为10,表示每次查询的数据条数;
  5. 设置onQuery回调函数默认为空函数,用于执行实际的数据查询逻辑;
  6. 提取children属性,用于渲染子组件。
  7. 初始化page状态为1,表示当前页码;
  8. 初始化isEnd状态为false,表示是否已加载完所有数据;
  9. 初始化isLoading状态为false,表示是否正在加载数据;
  10. 初始化list状态为空数组,用于存储加载到的数据列表。

const { auto = true, onChange = () => {}, size = 10, onQuery = () => {}, children, } = props; const [page, setPage] = useState(1); const [isEnd, setIsEnd] = useState(false); const [isLoading, setIsLoading] = useState(false); const [list, setList] = useState<T[]>([]);

5. 初始化是否自动加载

  1. 使用useEffect钩子,用于在组件挂载时执行副作用操作;
  2. 检查auto属性是否为true且当前未在加载数据;
  3. 如果条件满足,设置加载状态为true,表示开始加载数据;
  4. 调用onQuery函数,传入当前页码和每页大小来获取数据。

useEffect(() => { if (auto && !isLoading) { setIsLoading(true); onQuery(page, size); } }, []);

6. 滚动加载实现

  1. 定义一个名为onScrollToLower的函数,当用户滚动到列表底部时触发;
  2. 检查是否未达到数据末尾且当前未在加载数据;
  3. 如果条件满足,设置加载状态为true,表示开始加载数据;
  4. 调用onQuery函数,传入当前页码和每页大小来获取更多数据。

const onScrollToLower = () => { if (!isEnd && !isLoading) { setIsLoading(true); onQuery(page, size); } };

7. 更新数据和重新加载实现

  1. 定义一个名为complete的函数,接收一个泛型数组或布尔值作为参数,用于处理分页加载完成后的逻辑;
  2. 设置加载状态为false,表示数据加载已完成;
  3. 判断传入的参数是否为数组类型;
  4. 如果参数是数组,则执行以下操作;
  5. 使用concat方法将新数据连接到现有列表末尾,创建一个新的数组;
  6. 调用onChange回调函数,将更新后的完整列表传递给父组件;
  7. 更新组件内部的状态,保存新的列表数据;
  8. 判断新获取的数据长度是否小于请求的页面大小;
  9. 如果数据长度小于页面大小,说明已加载完所有数据,设置结束状态为true;
  10. 如果数据长度等于页面大小,说明可能还有更多数据,将页码增加1;
  11. 如果传入的参数不是数组(通常表示加载失败),直接设置结束状态为true。
  12. 定义reload函数,用于重新加载数据;
  13. 将页码重置为1,从第一页开始加载;
  14. 清空当前列表数据;
  15. 将结束状态设置为false,允许重新加载数据;
  16. 调用onQuery函数,使用初始页码和页面大小开始加载数据。

const complete = (value: T[] | boolean) => { setIsLoading(false); if (Array.isArray(value)) { const newList = list.concat(value); onChange(newList); setList(newList); if (value.length < size) { setIsEnd(true); } else { setPage(page + 1); } } else { setIsEnd(true); } }; const reload = () => { setPage(1); setList([]); setIsEnd(false); onQuery(1, size); };

8. DOM 结构


<ScrollView className="rui-paging-content" scrollY onScrollToLower={onScrollToLower} > {children} {list.length === 0 && isEnd && ( <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]"> 暂无数据 </View> )} {isEnd && list.length > 0 && ( <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]"> 没有更多数据了 </View> )} {isLoading && ( <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]"> 加载中... </View> )} </ScrollView>

9. 总结

到这一步基本功能就实现完成,当然如果需要添加下拉刷新、数据量大的虚拟列表等功能,就需要单独开发补充,到这一步已经满足基本的开发需求。

10. 完整代码


import { useEffect, useState, forwardRef, useImperativeHandle } from "react"; import { ScrollView, View } from "@tarojs/components"; import "./paging.scss"; function RuiPaging<T extends ListItem>( props: RuiPagingProps<T>, ref: React.ForwardedRef<RuiPagingRef<T>> ) { const { auto = true, onChange = () => {}, size = 10, onQuery = () => {}, children, } = props; const [page, setPage] = useState(1); const [isEnd, setIsEnd] = useState(false); const [isLoading, setIsLoading] = useState(false); const [list, setList] = useState<T[]>([]); useEffect(() => { if (auto && !isLoading) { setIsLoading(true); onQuery(page, size); } }, []); const onScrollToLower = () => { if (!isEnd && !isLoading) { setIsLoading(true); onQuery(page, size); } }; const complete = (value: T[] | boolean) => { setIsLoading(false); if (Array.isArray(value)) { const newList = list.concat(value); onChange(newList); setList(newList); if (value.length < size) { setIsEnd(true); } else { setPage(page + 1); } } else { setIsEnd(true); } }; const reload = () => { setPage(1); setList([]); setIsEnd(false); onQuery(1, size); }; useImperativeHandle(ref, () => ({ complete, reload, })); return ( <ScrollView className="rui-paging-content" scrollY onScrollToLower={onScrollToLower} > {children} {list.length === 0 && isEnd && ( <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]"> 暂无数据 </View> )} {isEnd && list.length > 0 && ( <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]"> 没有更多数据了 </View> )} {isLoading && ( <View className="rui-flex-cc h-[60px] text-[24px] text-[#999]"> 加载中... </View> )} </ScrollView> ); } export default forwardRef(RuiPaging); interface ListItem {} export interface RuiPagingProps<T extends ListItem> { value?: T[]; auto?: boolean; onChange: (e: T[]) => void; size?: number; onQuery: (page: number, size: number) => void; children: React.ReactNode; } export interface RuiPagingRef<T extends ListItem> { complete: (value: T[] | boolean) => void; reload: () => void; }

11. 组件使用

注意:可以配合【taro react】 ---- useModel 数据双向绑定 hook 实现进行开发,更加方便快捷,同时减少代码量。


const paging = useRef<RuiPagingRef<Course>>(null); const [keyword, keywordModel] = useModel(""); const [videos, model] = useModel<Course[]>([]); // 教程列表 const getCourseList = (pageNum, pageSize) => { https .getCourseList({ keyword, classify_id: "", pageNum, pageSize }) .then((res) => { const data = res?.data?.data; paging?.current?.complete(data?.records); }) .catch((_) => { paging?.current?.complete(false); }); }; return ( <View className="bg-[#eee]"> <RuiPaging {...model()} ref={paging} onQuery={getCourseList}> <View> {/* 搜索框 */} <TutorialSearch {...keywordModel()} onSearch={init} /> {/* 视频列表 */} <View className="mt-[30px] ml-[30px] mr-[30px] pb-[30px]"> <TutorialList videos={videos} /> </View> </View> </RuiPaging> </View> );

12. 总结

  1. 代码复用性高 :将通用的分页逻辑封装在组件中,不同页面的列表只需引入该组件就能实现分页加载,减少了重复代码。
  2. 维护成本低 :分页逻辑集中管理,若需要修改或优化,只需在组件内部进行修改,无需在多个地方改动代码。
  3. 交互体验统一 :组件内部统一处理加载中和没有更多数据的提示,保证了不同列表的交互体验一致。
  4. 轻量级 :不依赖其他第三方库,减少了项目的依赖体积。
  5. 可定制性强 :可以根据项目的具体需求对组件进行修改和扩展。
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐