ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScript での膨大なデータの API の効率的な使用

JavaScript での膨大なデータの API の効率的な使用

Susan Sarandon
Susan Sarandonオリジナル
2024-10-20 20:42:02411ブラウズ

Efficient API consumption for huge data in JavaScript

大規模なデータセットを処理する API を操作する場合、データ フローを効率的に管理し、ページネーション、レート制限、メモリ使用量などの課題に対処することが重要です。この記事では、JavaScript のネイティブ フェッチ関数を使用して API を使用する方法について説明します。次のような重要なトピックが表示されます:

  • 大量のデータの処理: システムに負荷がかかることを避けるために、大規模なデータセットを段階的に取得します。
  • ページネーション: Storyblok Content Delivery API を含むほとんどの API はページ内のデータを返します。効率的なデータ取得のためにページネーションを管理する方法を検討します。
  • レート制限: API は、悪用を防ぐためにレート制限を課すことがよくあります。これらの制限を検出して処理する方法を見ていきます。
  • 再試行後のメカニズム: API が 429 ステータス コード (リクエストが多すぎる) で応答した場合、スムーズなデータを確保するために再試行するまでの待ち時間を示す「再試行後」メカニズムを実装します。取得しています。
  • 同時リクエスト: 複数のページを並行して取得すると、プロセスが高速化されます。 JavaScript の Promise.all() を使用して同時リクエストを送信し、パフォーマンスを向上させます。
  • メモリ リークの回避: 大規模なデータセットを処理するには、慎重なメモリ管理が必要です。 ジェネレーターのおかげで、データをチャンク単位で処理し、メモリ効率の高い操作を保証します。

Storyblok Content Delivery API を使用してこれらのテクニックを検討し、フェッチを使用して JavaScript でこれらすべての要素を処理する方法を説明します。コードを詳しく見てみましょう。

Storyblok Content Delivery API を使用する際の注意事項

コードの説明に入る前に、考慮すべき Storyblok API の重要な機能をいくつか説明します。

  • CV パラメータ: cv (コンテンツ バージョン) パラメータは、キャッシュされたコンテンツを取得します。 cv 値は最初のリクエストで返され、同じキャッシュされたバージョンのコンテンツが確実にフェッチされるように、後続のリクエストで渡す必要があります。
  • ページおよびページごとのページネーション: page パラメーターと per_page パラメーターを使用して、各リクエストで返される項目の数を制御し、結果ページを反復処理します。
  • 合計ヘッダー: 最初の応答の合計ヘッダーは、利用可能な項目の合計数を示します。これは、フェッチする必要があるデータ ページの数を計算するために不可欠です。
  • 処理 429 (レート制限): Storyblok はレート制限を強制します。これらをヒットすると、API は 429 ステータスを返します。 Retry-After ヘッダー (またはデフォルト値) を使用して、リクエストを再試行するまでの待ち時間を確認します。

fetch() を使用して大規模なデータセットを処理する JavaScript サンプル コード

JavaScript のネイティブフェッチ関数を使用してこれらの概念を実装する方法を次に示します。
次のことを考慮してください:

  • このスニペットは、例として stories.json という名前の新しいファイルを作成します。ファイルがすでに存在する場合は上書きされます。したがって、同じ名前のファイルが作業ディレクトリにすでにある場合は、コード スニペット内の名前を変更します。
  • リクエストは並行して実行されるため、ストーリーの順序は保証されません。たとえば、3 ページ目の応答が 2 番目のリクエストの応答よりも速い場合、ジェネレーターは 2 ページ目のストーリーよりも前に 3 ページ目のストーリーを配信します。
  • 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 (Too Many Requests) ステータスで応答したときに再試行するロジックが含まれています。
retryAfter 値は、再試行するまでの待機時間を指定します。 setTimeout を使用して次のリクエストを行う前に一時停止し、再試行は最大 5 回に制限されています。

2) 初期ページリクエストと CV パラメータ

最初の API リクエストは、合計ヘッダー (ストーリーの合計数を示す) と cv パラメーター (キャッシュに使用される) を取得するため、非常に重要です。
合計ヘッダーを使用して必要なページの合計数を計算でき、cv パラメーターによりキャッシュされたコンテンツが確実に使用されます。

3) ページネーションの処理

ページネーションは、page および per_page クエリ文字列パラメーターを使用して管理されます。このコードは 1 ページあたり 25 のストーリーを要求し (これは調整できます)、合計ヘッダーは取得する必要があるページ数を計算するのに役立ちます。
このコードは、一度に最大 7 つの並列リクエスト (これは調整可能) のバッチでストーリーをフェッチし、API に負担をかけずにパフォーマンスを向上させます。

4) Promise.all() による同時リクエスト:

プロセスを高速化するために、JavaScript の Promise.all() を使用して複数のページが並行して取得されます。このメソッドは、複数のリクエストを同時に送信し、すべてのリクエストが完了するまで待機します。
並列リクエストの各バッチが完了すると、結果が処理されてストーリーが生成されます。これにより、すべてのデータを一度にメモリにロードすることがなくなり、メモリの消費量が削減されます。

5) 非同期反復によるメモリ管理 (await...of 用):

すべてのデータを配列に収集する代わりに、JavaScript ジェネレーター (関数* および for await...of) を使用して、取得された各ストーリーを処理します。これにより、大規模なデータセットを処理する際のメモリの過負荷が防止されます。
ストーリーを 1 つずつ生成することで、コードの効率性が維持され、メモリ リークが回避されます。

6) レート制限の処理:

API が 429 ステータス コード (レート制限) で応答した場合、スクリプトは retryAfter 値を使用します。その後、指定された時間だけ一時停止してから、リクエストを再試行します。これにより、API レート制限への準拠が保証され、あまりにも多くのリクエストが急速に送信されることが回避されます。

結論

この記事では、ネイティブのフェッチ関数を使用して JavaScript で API を使用する際の重要な考慮事項について説明しました。私は次のことを処理しようとします:

  • 大規模なデータセット: ページネーションを使用して大規模なデータセットを取得します。
  • ページネーション: page パラメーターと per_page パラメーターを使用してページネーションを管理します。
  • レート制限と再試行メカニズム: レート制限を処理し、適切な遅延の後にリクエストを再試行します。
  • 同時リクエスト: JavaScript の Promise.all() を使用してページを並行して取得し、データの取得を高速化します。
  • メモリ管理: JavaScript ジェネレーター (関数* および for await...of) を使用して、過剰なメモリを消費せずにデータを処理します。

これらの手法を適用すると、スケーラブルで効率的かつメモリ安全な方法で API の使用を処理できます。

お気軽にコメント/フィードバックをお寄せください。

参考文献

  • JavaScript ジェネレーター
  • JavaScript ランタイムを作成する
  • Storyblok コンテンツ配信 API

以上がJavaScript での膨大なデータの API の効率的な使用の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。