
本文旨在解决在javascript中进行大量网络请求时,因网络不稳定导致进程中断的问题。通过引入一个自定义的`fetchwithretry`函数,文章详细阐述了如何构建一个具备自动重试功能的请求机制。该机制能在请求失败时自动进行多次尝试,显著提升了web抓取或api调用的健壮性和成功率,确保即使面对瞬时网络故障也能顺利完成任务。
引言:网络请求的挑战与健壮性需求
在进行Web数据抓取、API调用或任何涉及大量HTTP请求的JavaScript应用中,网络的不稳定性是一个常见且难以避免的挑战。当应用程序需要在一个循环中发送成百上千个请求时,即使是短暂的网络波动、服务器响应延迟或瞬时连接中断,都可能导致整个处理流程中断,影响数据的完整性和程序的稳定性。
考虑以下常见的代码模式,它在一个循环中顺序执行网络请求和DOM解析:
for (const el of NodeList) {
const url = el.getAttribute('href');
// 如果此处fetch请求失败或没有响应,后续代码将不会执行
const res = await fetch(url);
const html = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
alert('parsed successfully'); // 仅在浏览器环境中有效
}在这种模式下,一旦fetch(url)操作因为网络问题抛出错误或长时间无响应,后续的代码(如res.text()、DOMParser解析)将无法执行,导致当前循环迭代中断,甚至可能影响到整个循环的继续执行,从而破坏整个数据获取过程。为了提升应用的鲁棒性,我们需要一种机制来优雅地处理这些瞬时故障。
解决方案:实现请求重试机制
解决上述问题的核心在于引入一个重试机制。当fetch请求失败时,程序不应立即放弃,而是应该在限定的次数内进行多次尝试,直到请求成功或达到最大重试次数。这可以通过封装一个自定义的异步函数来实现。
立即学习“Java免费学习笔记(深入)”;
fetchWithRetry 函数详解
我们将创建一个名为fetchWithRetry的异步函数,它负责处理单个URL的请求,并在失败时自动重试。
/**
* 异步函数,用于带重试机制地获取网页内容并解析。
* @param {string} url - 要抓取的URL。
* @param {number} numberOfRetries - 最大重试次数。
* @returns {Promise} 解析后的DOM Document对象。
* @throws {Error} 当达到最大重试次数后仍然失败时抛出错误。
*/
async function fetchWithRetry(url, numberOfRetries) {
try {
// 尝试执行标准的fetch请求
const response = await fetch(url);
// 检查HTTP响应状态码,例如,非2xx状态码也可能需要重试
if (!response.ok) {
// 对于服务器错误(5xx)或特定客户端错误(如429 Too Many Requests)可以考虑重试
// 对于其他如404 Not Found,通常不适合重试
if (numberOfRetries > 0 && (response.status >= 500 || response.status === 429)) {
console.warn(`URL: ${url} 收到非成功状态码 ${response.status}。正在重试... 剩余重试次数: ${numberOfRetries}`);
// 可以添加一个延迟,避免立即重试导致服务器压力过大或再次失败
await new Promise(resolve => setTimeout(resolve, 1000 * (3 - numberOfRetries + 1))); // 递增延迟
return fetchWithRetry(url, numberOfRetries - 1);
} else {
// 对于不适合重试的非成功状态码,直接抛出错误
throw new Error(`HTTP Error: ${response.status} for URL: ${url}`);
}
}
const html = await response.text();
const parser = new DOMParser(); // 注意:DOMParser是浏览器API,在Node.js中需要polyfill
const doc = parser.parseFromString(html, 'text/html');
console.log(`URL: ${url} 解析成功。`);
return doc;
} catch (error) {
// 捕获网络错误或其他运行时错误
if (numberOfRetries > 0) {
console.error(`URL: ${url} 请求失败。正在重试... 剩余重试次数: ${numberOfRetries}`, error);
// 在重试前添加一个延迟,给网络或服务器恢复时间
await new Promise(resolve => setTimeout(resolve, 1000 * (3 - numberOfRetries + 1))); // 递增延迟
return fetchWithRetry(url, numberOfRetries - 1); // 递归调用自身进行重试
} else {
console.error(`URL: ${url} 请求失败。已达到最大重试次数。`, error);
throw error; // 达到最大重试次数后,抛出最终错误
}
}
} 函数说明:
-
参数:
- url: 目标网页的URL。
- numberOfRetries: 允许的最大重试次数。
-
try...catch 块:
- try 块: 包含正常的fetch请求、响应处理和DOMParser解析逻辑。如果请求成功且响应状态码为2xx,则返回解析后的Document对象。
- 响应状态码检查: 除了网络错误,我们还增加了对HTTP响应状态码的检查。对于服务器错误(5xx)或特定的客户端错误(如429 Too Many Requests,表示请求过于频繁),也视为需要重试的情况。对于其他如404(未找到)等,通常不应重试,而是直接抛出错误。
-
catch 块: 捕获任何在try块中发生的错误,包括网络连接问题、DNS解析失败或上述非成功的HTTP响应。
- 重试判断: 检查numberOfRetries是否大于0。如果还有剩余重试次数,则输出错误信息,并递归调用fetchWithRetry函数,同时将numberOfRetries减1。
- 延迟重试: 在每次重试前,我们引入了一个setTimeout来创建一个延迟。这非常重要,可以避免在短时间内对服务器造成过大压力,并给网络或服务器一个恢复的时间。这里使用了一个简单的递增延迟策略,例如第一次重试等待1秒,第二次等待2秒,以此类推。
- 最大重试次数: 如果numberOfRetries为0,表示已达到最大重试次数,此时不再重试,而是抛出原始错误,让上层调用者处理。
在循环中集成重试函数
现在,我们可以将原始循环中的fetch调用替换为我们新创建的fetchWithRetry函数:
const NodeList = [
{ getAttribute: (attr) => attr === 'href' ? 'https://example.com/page1' : '' },
{ getAttribute: (attr) => attr === 'href' ? 'https://example.com/page2' : '' },
{ getAttribute: (attr) => attr === 'href' ? 'https://example.com/page3' : '' },
// 更多节点...
];
async function processNodes() {
for (const el of NodeList) {
const url = el.getAttribute('href');
try {
// 调用带重试机制的函数,例如,最多重试3次
const doc = await fetchWithRetry(url, 3);
// 在此处处理解析后的文档对象
console.log(`成功处理URL: ${url},文档标题: ${doc.title}`);
// alert('parsed successfully'); // 在实际应用中,避免在循环中频繁使用alert
} catch (error) {
console.error(`处理URL: ${url} 最终失败:`, error.message);
// 可以选择记录失败的URL,或进行其他错误恢复操作
}
}
console.log('所有节点处理完毕。');
}
// 调用主处理函数
processNodes();通过这种方式,即使在处理过程中遇到临时的网络问题,程序也能自动尝试恢复,大大提高了整个数据获取过程的健壮性。
注意事项与最佳实践
- 最大重试次数的设定: 合理设置numberOfRetries至关重要。过多的重试可能导致不必要的资源消耗和时间延长,而过少的重试则可能无法应对常见的瞬时故障。通常,3到5次是一个比较合理的范围。
-
重试延迟策略:
- 固定延迟: 每次重试都等待相同的时间。
- 指数退避(Exponential Backoff): 每次重试的等待时间逐渐增加(例如,1秒、2秒、4秒、8秒...)。这种策略能有效避免在服务器负载过高时进一步加剧问题,并给服务器更长的恢复时间。上述示例中采用了简化的递增延迟。
- 抖动(Jitter): 在指数退避的基础上,随机化一部分延迟时间,避免所有客户端同时重试造成“惊群效应”。
-
错误类型区分: 并非所有错误都适合重试。例如:
- 网络错误(Network Error)、超时(Timeout): 适合重试。
- HTTP 5xx 状态码(服务器错误): 通常适合重试。
- HTTP 429 Too Many Requests: 适合重试,但需要结合指数退避和等待Retry-After头部字段指示的时间。
- HTTP 4xx 状态码(客户端错误,如404 Not Found、400 Bad Request): 通常不适合重试,因为这意味着请求本身有问题,重试也无济于事。
- 超时配置: fetch API本身支持AbortController来实现请求超时。将其与重试机制结合,可以在规定时间内未收到响应时触发重试。
- 错误日志与监控: 详细的错误日志对于调试和理解系统行为至关重要。记录每次失败的URL、错误类型和重试次数,有助于发现潜在的长期问题。
- 并发控制: 如果在循环中并行发送大量请求,还需要考虑并发控制,避免同时打开过多的网络连接,这可以通过使用Promise.allSettled结合限制并发数的工具库(如p-limit)来实现。
- DOMParser的适用性: DOMParser是浏览器环境下的API。如果在Node.js环境中进行服务器端抓取,需要使用类似jsdom或cheerio等库来解析HTML。本教程的示例代码假设在浏览器或类似浏览器环境(如Electron)中运行。
总结
通过实现一个健壮的fetchWithRetry函数,我们可以显著提升JavaScript应用程序处理网络请求的可靠性。这种重试机制能够有效地应对瞬时网络故障和服务器不稳定,确保即使在复杂的网络环境下,也能最大限度地完成数据获取任务。结合合理的重试策略、延迟机制和错误处理,我们可以构建出更加稳定和用户友好的Web应用。










