背景
在项目开发过程中,我们经常需要从服务器获取分页数据。有时候我们会遇到一些奇葩的需求,从服务器一次性获取某个查询条件下的所有数据,这时候如果数据量较大,一次性获取所有数据可能会导致性能问题。那么这时候我们就要考虑通过分页分批次获取数据,然后讲所有分页数据合并为最终想要的总数据,这时候我们就不得不要考虑通过控制并发请求的数量,以提高性能和用户体验。
目标
实现一个自定义 Hook useFetchAll,用于批量获取分页数据,并控制并发请求的数量。这个 Hook 应该具备以下功能:
- 获取所有分页数据:根据给定的 URL 和每页请求的数据量,获取所有分页数据。
- 控制并发请求数量:通过并发池限制同时进行的请求数量,避免一次性发出过多请求导致服务器压力过大或浏览器性能问题。
- 错误处理:在请求过程中处理可能出现的错误,并在组件卸载时取消未完成的请求。
代码实现
自定义 Hook:useFetchAll
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
const useFetchAll = (url, pageSize, concurrencyLimit) => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const abortController = useRef(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
abortController.current = new AbortController();
try {
// 获取第一页数据,来确定总数
const firstPageResponse = await axios.get(url, {
params: { page: 1, pageSize },
signal: abortController.current.signal,
});
const total = firstPageResponse.data.total;
const totalPages = Math.ceil(total / pageSize);
// 创建一个数组,包含所有页码
const pages = Array.from({ length: totalPages }, (_, i) => i + 1);
// 并发请求函数
const fetchPage = async (page) => {
try {
const response = await axios.get(url, {
params: { page, pageSize },
signal: abortController.current.signal,
});
return { page, items: response.data.items };
} catch (err) {
if (axios.isCancel(err)) {
console.log(`Request for page ${page} was cancelled`);
} else {
throw err;
}
}
};
// 控制并发请求
const fetchAllPages = async () => {
const results = [];
const pool = new Set();
for (const page of pages) {
const promise = fetchPage(page).then((result) => {
if (result) {
results.push(result);
}
pool.delete(promise);
});
pool.add(promise);
if (pool.size >= concurrencyLimit) {
await Promise.race(pool);
}
}
await Promise.all(pool);
return results;
};
const allData = await fetchAllPages();
// 按页码排序并合并数据
const sortedData = allData
.sort((a, b) => a.page - b.page)
.flatMap(result => result.items);
setData(sortedData);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
return () => {
if (abortController.current) {
abortController.current.abort();
}
};
}, [url, pageSize, concurrencyLimit]);
return { data, loading, error };
};
export default useFetchAll;
这段代码在干嘛?
-
初始化状态: 我们用
useState创建了data、loading和error三个状态变量,分别用于存储数据、加载状态和错误信息。 -
取消请求: 我们用
useRef创建了一个abortController,在组件卸载时取消未完成的请求,以避免内存泄漏。 -
获取第一页数据: 我们用
axios请求第一页的数据,来确定总数。这样我们就知道总共有多少页需要请求。 -
创建页码数组: 根据总数和每页请求数,我们创建一个包含所有页码的数组。
-
并发请求: 我们定义了一个
fetchPage函数,用于请求每一页的数据,并返回页码和数据。然后用一个并发池来控制同时进行的请求数量。 -
合并数据: 我们将所有页的数据按页码排序并合并成一个完整的数据集,并更新
data状态。 -
错误处理: 在请求过程中,如果发生错误,我们会捕获并设置
error状态。 -
取消请求: 在组件卸载时取消未完成的请求,以避免内存泄漏。
使用示例
下面是一个使用 useFetchAll Hook 的示例组件:
import React from 'react';
import useFetchAll from './useFetchAll';
const ExampleComponent = () => {
const url = 'https://api.example.com/data';
const pageSize = 10;
const concurrencyLimit = 3;
const { data, loading, error } = useFetchAll(url, pageSize, concurrencyLimit);
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了: {error.message}</p>;
return (
<div>
<h1>数据列表</h1>
<ul>
{data.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
</div>
);
};
export default ExampleComponent;
解释
-
定义 URL 和参数: 我们定义了数据请求的 URL、每页请求的数据量
pageSize和并发请求的限制concurrencyLimit。 -
调用
useFetchAll: 我们调用useFetchAllHook,并解构出data、loading和error三个状态。 -
处理加载和错误状态: 如果数据正在加载,显示加载提示;如果发生错误,显示错误信息。
-
渲染数据: 如果数据加载完成且没有错误,渲染数据列表。
最后
通过这个自定义 Hook useFetchAll,我们可以方便地批量获取分页数据,并控制并发请求的数量。这个 Hook 具有以下优点:
-
简化代码: 将复杂的分页请求逻辑封装在 Hook 中,使组件代码更加简洁。
-
提高性能: 通过控制并发请求数量,避免一次性发出过多请求导致的性能问题。
-
易于扩展: 可以根据需要对 Hook 进行扩展,例如添加更多的请求参数或处理不同的错误情况。
希望这个笔记对你有所帮助!Happy coding! 🎉