首頁 >web前端 >js教程 >JavaScript 中海量資料的高效能 API 消耗

JavaScript 中海量資料的高效能 API 消耗

Susan Sarandon
Susan Sarandon原創
2024-10-20 20:42:02417瀏覽

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