使用處理大型資料集的 API 時,有效管理資料流並解決分頁、速率限制和記憶體使用等挑戰至關重要。在本文中,我們將介紹如何使用 JavaScript 的本機 fetch 函數來使用 API。我們將看到重要的主題,例如:
我們將使用 Storyblok Content Delivery API 來探索這些技術,並解釋如何使用 fetch 在 JavaScript 中處理所有這些因素。讓我們深入研究程式碼。
在深入研究程式碼之前,請先考慮以下 Storyblok API 的一些關鍵功能:
以下是我如何使用 JavaScript 中的本機 fetch 函數來實現這些概念。
考慮一下:
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 時的關鍵注意事項。我嘗試處理:
透過應用這些技術,您可以以可擴展、高效且記憶體安全的方式處理 API 消耗。
請隨時發表您的評論/回饋。
以上是JavaScript 中海量資料的高效能 API 消耗的詳細內容。更多資訊請關注PHP中文網其他相關文章!