首页  >  文章  >  web前端  >  JavaScript 中海量数据的高效 API 消耗

JavaScript 中海量数据的高效 API 消耗

Susan Sarandon
Susan Sarandon原创
2024-10-20 20:42:02314浏览

Efficient API consumption for huge data in JavaScript

使用处理大型数据集的 API 时,有效管理数据流并解决分页、速率限制和内存使用等挑战至关重要。在本文中,我们将介绍如何使用 JavaScript 的本机 fetch 函数来使用 API。我们将看到重要的主题,例如:

  • 处理大量数据:增量检索大型数据集以避免系统不堪重负。
  • 分页:大多数 API(包括 Storyblok Content Delivery API)以页面形式返回数据。我们将探索如何管理分页以实现高效的数据检索。
  • 速率限制:API 通常会施加速率限制以防止滥用。我们将了解如何检测和处理这些限制。
  • Retry-After机制:如果API响应429状态码(请求过多),我们将实现“Retry-After”机制,指示重试之前需要等待多长时间,以确保数据流畅正在获取。
  • 并发请求:并行获取多个页面可以加快进程。我们将使用 JavaScript 的 Promise.all() 发送并发请求并提高性能。
  • 避免内存泄漏:处理大型数据集需要仔细的内存管理。借助生成器
  • ,我们将分块处理数据并确保内存高效操作

我们将使用 Storyblok Content Delivery API 探索这些技术,并解释如何使用 fetch 在 JavaScript 中处理所有这些因素。让我们深入研究代码。

使用 Storyblok Content Delivery API 时要记住的事项

在深入研究代码之前,请考虑以下 Storyblok API 的一些关键功能:

  • CV 参数:cv(内容版本)参数检索缓存的内容。 cv 值在第一个请求中返回,并应在后续请求中传递,以确保获取内容的相同缓存版本。
  • 分页和每页分页:使用 page 和 per_page 参数来控制每个请求中返回的项目数并迭代结果页面。
  • 总标题:第一个响应的总标题指示可用项目的总数。这对于计算需要获取多少数据页至关重要。
  • 处理 429(速率限制):Storyblok 强制执行速率限制;当您点击它们时,API 会返回 429 状态。使用 Retry-After 标头(或默认值)了解重试请求之前需要等待多长时间。

使用 fetch() 处理大型数据集的 JavaScript 示例代码

以下是我如何使用 JavaScript 中的本机 fetch 函数实现这些概念。
考虑一下:

  • 此代码片段创建一个名为 Stories.json 的新文件作为示例。如果该文件已经存在,它将被覆盖。因此,如果工作目录中已有具有该名称的文件,请更改代码片段中的名称。
  • 由于请求是并行执行的,因此无法保证故事的顺序。例如,如果第三页的响应比第二个请求的响应快,则生成器将在第二页的故事之前传送第三页的故事。
  • 我用 Bun 测试了该片段:)
import { writeFile, appendFile } from "fs/promises";

// Read access token from Environment
const STORYBLOK_ACCESS_TOKEN = process.env.STORYBLOK_ACCESS_TOKEN;
// Read access token from Environment
const STORYBLOK_VERSION = process.env.STORYBLOK_VERSION;

/**
 * Fetch a single page of data from the API,
 * with retry logic for rate limits (HTTP 429).
 */
async function fetchPage(url, page, perPage, cv) {
  let retryCount = 0;
  // Max retry attempts
  const maxRetries = 5;
  while (retryCount <= maxRetries) {
    try {
      const response = await fetch(
        `${url}&page=${page}&per_page=${perPage}&cv=${cv}`,
      );
      // Handle 429 Too Many Requests (Rate Limit)
      if (response.status === 429) {
        // Some APIs provides you the Retry-After in the header
        // Retry After indicates how long to wait before retrying.
        // Storyblok uses a fixed window counter (1 second window)
        const retryAfter = response.headers.get("Retry-After") || 1;
        console.log(response.headers,
          `Rate limited on page ${page}. Retrying after ${retryAfter} seconds...`,
        );
        retryCount++;
        // In the case of rate limit, waiting 1 second is enough.
        // If not we will wait 2 second at the second tentative,
        // in order to progressively slow down the retry requests
        // setTimeout accept millisecond , so we have to use 1000 as multiplier
        await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * retryCount));
        continue;
      }

      if (!response.ok) {
        throw new Error(
          `Failed to fetch page ${page}: HTTP ${response.status}`,
        );
      }
      const data = await response.json();
      // Return the stories data of the current page
      return data.stories || [];
    } catch (error) {
      console.error(`Error fetching page ${page}: ${error.message}`);
      return []; // Return an empty array if the request fails to not break the flow
    }
  }
  console.error(`Failed to fetch page ${page} after ${maxRetries} attempts`);
  return []; // If we hit the max retry limit, return an empty array
}

/**
 * Fetch all data in parallel, processing pages in batches
 * as a generators (the reason why we use the `*`)
 */
async function* fetchAllDataInParallel(
  url,
  perPage = 25,
  numOfParallelRequests = 5,
) {

  let currentPage = 1;
  let totalPages = null;

  // Fetch the first page to get:
  // - the total entries (the `total` HTTP header)
  // - the CV for caching (the `cv` atribute in the JSON response payload)
  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(`Failed to fetch data: HTTP ${firstResponse.status}`);
  }
  console.timeLog("API", "After first response");

  const firstData = await firstResponse.json();
  const total = parseInt(firstResponse.headers.get("total"), 10) || 0;
  totalPages = Math.ceil(total / perPage);

  // Yield the stories from the first page
  for (const story of firstData.stories) {
    yield story;
  }

  const cv = firstData.cv;

  console.log(`Total pages: ${totalPages}`);
  console.log(`CV parameter for caching: ${cv}`);

  currentPage++; // Start from the second page now

  while (currentPage <= totalPages) {
    // Get the list of pages to fetch in the current batch
    const pagesToFetch = [];
    for (
      let i = 0;
      i < numOfParallelRequests && currentPage <= totalPages;
      i++
    ) {
      pagesToFetch.push(currentPage);
      currentPage++;
    }

    // Fetch the pages in parallel
    const batchRequests = pagesToFetch.map((page) =>
      fetchPage(url, page, perPage, firstData, cv),
    );

    // Wait for all requests in the batch to complete
    const batchResults = await Promise.all(batchRequests);
    console.timeLog("API", `Got ${batchResults.length} response`);
    // Yield the stories from each batch of requests
    for (let result of batchResults) {
      for (const story of result) {
        yield story;
      }
    }
    console.log(`Fetched pages: ${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);

// Create an empty file (or overwrite if it exists) before appending
await writeFile('stories.json', '[', 'utf8'); // Start the JSON array
let i = 0;
for await (const story of stories) {
  i++;
  console.log(story.name);
  // If it's not the first story, add a comma to separate JSON objects
  if (i > 1) {
    await appendFile('stories.json', ',', 'utf8');
  }
  // Append the current story to the file
  await appendFile('stories.json', JSON.stringify(story, null, 2), 'utf8');
}
// Close the JSON array in the file
await appendFile('stories.json', ']', 'utf8'); // End the JSON array
console.log(`Total Stories: ${i}`);

关键步骤说明

以下是代码中关键步骤的细分,可确保使用 Storyblok Content Delivery API 实现高效且可靠的 API 使用:

1) 使用重试机制获取页面(fetchPage)

此函数处理从 API 获取单页数据。它包括当 API 响应 429(请求过多)状态时重试的逻辑,这表示已超出速率限制。
retryAfter 值指定重试之前等待的时间。我在发出后续请求之前使用 setTimeout 暂停,并且重试次数最多限制为 5 次。

2) 初始页面请求和 CV 参数

第一个 API 请求至关重要,因为它会检索总标头(指示故事总数)和 cv 参数(用于缓存)。
您可以使用total header来计算所需的总页数,cv参数确保使用缓存的内容。

3) 处理分页

分页是使用 page 和 per_page 查询字符串参数进行管理的。该代码请求每页 25 个故事(您可以调整此值),总标题有助于计算需要获取的页面数。
该代码一次最多可批量获取 7 个(您可以调整此)并行请求的故事,以提高性能,而不会压垮 API。

4) 使用 Promise.all() 的并发请求:

为了加快该过程,使用 JavaScript 的 Promise.all() 并行获取多个页面。此方法同时发送多个请求并等待所有请求完成。
每批并行请求完成后,将处理结果以生成故事。这样可以避免一次将所有数据加载到内存中,从而减少内存消耗。

5) 异步迭代的内存管理(用于await...of):

我们没有将所有数据收集到数组中,而是使用 JavaScript 生成器(function* 和 wait...of)来处理获取的每个故事。这可以防止处理大型数据集时出现内存过载。
通过一一生成故事,代码保持高效并避免内存泄漏。

6) 速率限制处理:

如果 API 以 429 状态代码(速率受限)响应,则脚本使用 retryAfter 值。然后,它会暂停指定的时间,然后重试请求。这可确保符合 API 速率限制并避免过快发送过多请求。

结论

在本文中,我们介绍了使用本机 fetch 函数在 JavaScript 中使用 API 时的关键注意事项。我尝试处理:

  • 大型数据集:使用分页获取大型数据集。
  • 分页:使用 page 和 per_page 参数管理分页。
  • 速率限制和重试机制:处理速率限制并在适当的延迟后重试请求。
  • 并发请求:使用 JavaScript 的 Promise.all() 并行获取页面,以加快数据检索速度。
  • 内存管理:使用JavaScript生成器(function*和await...of)来处理数据而不消耗过多的内存。

通过应用这些技术,您可以以可扩展、高效且内存安全的方式处理 API 消耗。

请随时发表您的评论/反馈。

参考

  • JavaScript 生成器
  • 构建 JavaScript 运行时
  • Storyblok 内容交付 API

以上是JavaScript 中海量数据的高效 API 消耗的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn