在 JavaScript 中高效处理大量数据的 API
在处理大型数据集的API时,高效管理数据流并解决诸如分页、速率限制和内存使用等方面的问题至关重要。本文将介绍如何使用JavaScript的内置fetch
函数来消费API。我们将讨论以下几个重要主题:
- 处理大量数据:逐步检索大型数据集,以避免数据过多导致系统过载。
- 分页:我们将探讨如何管理分页,实现高效的数据检索。
- 速率限制:API通常会设置速率限制以防止滥用。我们将看到如何检测并处理这些限制。
- 重试等待机制:如果API响应时返回429状态码(请求过多),我们将实现“重试等待机制”,这会指示等待多久后再重试,以确保平稳的数据获取。
- 并发请求:并行获取多个页面可以加快进程。我们将使用JavaScript的
Promise.all()
来并发发送请求并提高性能。 - 避免内存泄漏:处理大量数据需要谨慎的内存管理。我们将分块处理数据,并确保操作的内存效率,利用生成器。
我们将通过Storyblok内容交付API来探索这些技术,并说明如何利用JavaScript中的fetch
来处理所有这些因素。让我们开始写代码吧。
在开始研究代码之前,这里有几个关于Storyblok API的重要点需要考虑,
- CV 参数 :
cv
(内容版本)参数用于获取缓存的内容。cv
的值在首次请求中返回,请在后续请求中再次使用,以确保获取相同缓存版本的内容。 - 使用
page
和per_page
进行分页:通过使用page
和per_page
参数来控制每个请求返回的项目数量,并逐页获取结果。 - 总数头部 :第一个响应的
total
头部表示总项目数量。这对于计算需要获取的页面数量非常重要。 - 处理 429(速率限制) :Storyblok 限制速率;当你达到速率限制时,API 会返回 429 状态码。通过
Retry-After
头部(或默认等待时间)来确定重试请求前的等待时间。
fetch()
处理大数据集的 JavaScript 示例
这里是如何使用JavaScript的原生fetch函数来实现这些概念的。注意:
- 此代码片段将创建一个名为
stories.json
的新文件作为示例。如果文件已存在,请在代码片段中修改文件名。 - 由于请求是并行执行的,故事的顺序无法保证。例如,如果第三页的响应比第二页的响应更快,生成器将先提供第三页的故事。
- 我用 Bun 测试了这段代码 :)
import { writeFile, appendFile } from "fs/promises";
// 从环境变量中获取访问令牌
const STORYBLOK_ACCESS_TOKEN = process.env.STORYBLOK_ACCESS_TOKEN;
// 从环境变量中获取访问令牌
const STORYBLOK_VERSION = process.env.STORYBLOK_VERSION;
/**
* 从API获取单页数据,并针对速率限制(HTTP 429)实现重试机制。
*/
async function fetchPage(url, page, perPage, cv) {
let retryCount = 0;
// 最大重试次数
const maxRetries = 5;
while (retryCount <= maxRetries) {
try {
const response = await fetch(
`${url}&page=${page}&per_page=${perPage}&cv=${cv}`,
);
// 处理429 Too Many Requests(速率限制)
if (response.status === 429) {
// some APIs 提供 Retry-After 信息在头部中
// Retry-After 表示重试前需要等待的时间间隔
// Storyblok 使用固定的窗口计数器(1秒窗口)
const retryAfter = response.headers.get("Retry-After") || 1;
console.log(response.headers,
`速率限制在第 ${page} 页。重试前等待 ${retryAfter} 秒...`,
);
retryCount++;
// 在遭遇速率限制时,等待1秒即可。否则从第二次尝试开始,每次等待时间逐渐增加
// setTimeout 接受毫秒,所以我们需要将乘数设置为1000
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * retryCount));
continue;
}
if (!response.ok) {
throw new Error(
`获取第 ${page} 页失败: HTTP ${response.status}`,
);
}
const data = await response.json();
// 返回当前页的故事信息
return data.stories || [];
} catch (error) {
console.error(`获取第 ${page} 页失败: ${error.message}`);
return []; // 如果请求失败,返回空数组以不中断流程
}
}
console.error(`未能在 ${maxRetries} 次尝试后获取第 ${page} 页`);
return []; // 如果达到了最大重试次数,返回空数组
}
/**
* 并行获取所有数据,使用生成器(这是为什么我们使用 `*`)处理页面批次
*/
async function* fetchAllDataInParallel(
url,
perPage = 25,
numOfParallelRequests = 5,
) {
let currentPage = 1;
let totalPages = null;
// 获取第一页以获取:
// - 总条目数(HTTP头部的 `total`)
// - 用于缓存的 CV(JSON响应负载中的 `cv` 属性)
const firstResponse = await fetch(
`${url}&page=${currentPage}&per_page=${perPage}`,
);
if (!firstResponse.ok) {
console.log(`${url}&page=${currentPage}&per_page=${perPage}`);
console.log(firstResponse);
throw new Error(`获取数据失败: HTTP ${firstResponse.status}`);
}
console.timeLog("API", "第一响应之后");
const firstData = await firstResponse.json();
const total = parseInt(firstResponse.headers.get("total"), 10) || 0;
totalPages = Math.ceil(total / perPage);
// 生成第一页的故事
for (const story of firstData.stories) {
yield story;
}
const cv = firstData.cv;
console.log(`总页数: ${totalPages}`);
console.log(`用于缓存的 CV 参数: ${cv}`);
currentPage++; // 从第二页开始
while (currentPage <= totalPages) {
// 获取当前批次需要获取的页面列表
const pagesToFetch = [];
for (
let i = 0;
i < numOfParallelRequests && currentPage <= totalPages;
i++
) {
pagesToFetch.push(currentPage);
currentPage++;
}
// 并行获取页面
const batchRequests = pagesToFetch.map((page) =>
fetchPage(url, page, perPage, firstData, cv),
);
// 等待批次中的所有请求完成
const batchResults = await Promise.all(batchRequests);
console.timeLog("API", `获取到 ${batchResults.length} 个响应`);
// 生成从每个批次请求获取的故事
for (let result of batchResults) {
for (const story of result) {
yield story;
}
}
console.log(`获取的页面: ${pagesToFetch.join(", ")}`);
}
}
console.time("API");
const apiUrl = `https://api.storyblok.com/v2/cdn/stories?token=${STORYBLOK_ACCESS_TOKEN}&version=${STORYBLOK_VERSION}`;
// const apiUrl = `http://localhost:3000?token=${STORYBLOK_ACCESS_TOKEN}&version=${STORYBLOK_VERSION}`;
const stories = fetchAllDataInParallel(apiUrl, 25,7);
// 在追加之前,先创建一个空文件(如果文件已存在则覆盖)
await writeFile('stories.json', '[', 'utf8'); // 开始JSON数组
let i = 0;
for await (const story of stories) {
i++;
console.log(story.name);
// 如果不是第一个故事,则添加逗号以分隔JSON对象
if (i > 1) {
await appendFile('stories.json', ',', 'utf8');
}
// 将当前故事追加到文件中
await appendFile('stories.json', JSON.stringify(story, null, 2), 'utf8');
}
// 在文件中关闭JSON数组
await appendFile('stories.json', ']', 'utf8'); // 结束JSON数组
console.log(`总故事数: ${i}`);
点击全屏可以进入,点击全屏可以退出
看来看看关键步骤以下是对代码中确保使用Storyblok内容交付API进行高效可靠的数据获取的重要步骤的分解如下:
带有重试功能的页面获取,称为 fetchPage
此功能用于从API获取单页数据。它包括在API响应为429(请求过多)状态时重试的逻辑,这表明速率限制已被超出,需要等待一定时间才能再次请求。
retryAfter
值指定了重试前等待的时间。我使用setTimeout
来暂停一段时间,然后再进行后续请求,重试次数限制在5次以内。
2) 初始页面请求过程和CV参数(简历参数)
第一个API请求特别重要,因为它获取比如total
头部(表示故事总数)和cv
参数(这个参数用于缓存)。
你可以使用total
头部来算出总页数,而这个cv
参数确保缓存内容被使用。
3): 分页处理
分页是通过page
和per_page
这两个查询字符串参数来控制的。代码请求每页25个故事(您可以调整此数字),并且total
头部帮助我们计算需要获取的页面数量。代码一次最多批量发起7个(您可以调整此数字)并行请求来获取故事,以提高性能并避免压垮API。
- 例如,使用
Promise.all()
进行并发请求
为了加快进程,通过JavaScript的Promise.all()
并行获取多个页面。此方法同时发送多个请求,并等待所有响应,以确保所有请求完成。
每次并行请求完成后,处理结果以生成故事内容,这样可以避免一次性加载所有数据到内存中,从而节省内存。
5) 使用异步遍历 (for await...of
),例如,进行内存管理:
而不是将所有数据都收集到一个数组中,我们使用JavaScript生成器(function*
和for await...of
)来逐个处理获取到的故事。这样在处理大数据集时可以防止内存超载。
通过逐个生成故事,代码保持高效并避免内存泄漏的问题。
6),限速处理:
如果 API 返回状态码 429
(表示速率限制),脚本会使用 retryAfter
提供的时间值。脚本会暂停指定的时间,然后重新发送请求。这确保脚本遵守 API 的速率限制,避免了请求发送过于频繁。
在这篇文章里,我们讨论了使用原生 fetch
函数来消费 API 时的关键考虑因素。我尝试解决以下问题:
- 大型数据集 : 通过分页获取大型数据集。
- 分页 : 使用
page
和per_page
参数管理分页。 - 速率限制和重试机制 : 处理速率限制并按照延迟时间重试请求。
- 并发请求 : 使用 JavaScript 的
Promise.all()
并行获取页面,从而加快数据检索速度。 - 内存管理 : 使用 JavaScript 生成器 (
function*
和for await...of
) 来处理数据,以避免占用过多内存。
通过应用这些技术,你可以高效、可扩展且内存安全地消费API。
欢迎留言评论或反馈。
参考资料共同学习,写下你的评论
评论加载中...
作者其他优质文章