Node.js中fetch API的完整指南:从原生支持到生产环境实践

发布时间:2026/8/3 15:34:59
Node.js中fetch API的完整指南:从原生支持到生产环境实践 1. 项目概述为什么Node.js开发者需要关注fetch如果你和我一样是从前端开发转向或者同时涉足Node.js后端那么对fetch这个API一定不会陌生。在浏览器里它是我们与服务器通信的“瑞士军刀”简洁的语法和基于Promise的设计让它迅速取代了古老的XMLHttpRequest。然而当我们在Node.js环境中也想顺手拈来地使用fetch时却常常会碰壁——你会发现原生的Node.js核心模块里根本没有fetch这个东西。这引出了一个核心问题在Node.js里发起HTTP请求我们到底有多少种选择从内置的http/https模块到社区里经久不衰的axios、request已弃用但影响深远再到node-fetch、got等后起之秀选择众多。但fetchAPI以其标准化的接口和与前端开发体验的一致性对全栈开发者有着独特的吸引力。近年来随着Node.js自身的发展情况正在发生变化。从v17.5.0开始Node.js实验性地引入了对fetch的原生支持并在v18.0.0中将其提升为稳定功能但仍需通过--experimental-fetch标志开启直到最近的LTS版本如v18在特定子版本后它终于成为了一个全局可用的、稳定的API。这意味着我们正处在一个过渡期。很多教程、老项目还在使用node-fetch这个第三方polyfill库而新项目则可以直接拥抱原生fetch。这种新旧交替正是各种“坑”滋生的温床。本文将基于我最近在多个生产级Node.js服务中整合fetch的经验为你彻底梳理在Node.js中使用fetch的完整路径并重点剖析那些官方文档不会明说但实际开发中一定会遇到的典型问题及其解决方案。无论你是想在新项目中直接使用原生fetch还是需要维护一个使用了node-fetch的老项目这篇文章都能给你提供直接的参考。2. 核心思路与方案选型原生、Polyfill还是其他在动手写第一行代码之前我们必须先厘清一个根本问题在当前的项目环境和Node.js版本下我应该使用哪种fetch这绝不是一个拍脑袋的决定它直接关系到项目的依赖复杂度、运行稳定性以及未来的可维护性。2.1 三种核心实现路径的深度对比1. Node.js原生fetch (v18及以上LTS版本推荐)这是最理想、最简洁的方案。如果你的项目可以运行在Node.js v18.0.0或更高版本强烈建议使用最新的LTS版本如v20.x那么fetch已经作为一个全局API存在无需任何额外安装。优势零依赖减少node_modules体积避免依赖冲突提升安装速度和安全性。官方支持与Node.js运行时深度集成通常能获得更好的性能和兼容性保证更新随Node.js版本同步。标准一致行为最接近Web标准前端经验无缝迁移。劣势版本锁定要求生产环境Node.js版本必须足够新在一些受限制的旧服务器或容器环境中可能无法满足。功能相对基础相较于一些成熟的第三方库原生fetch在早期版本可能缺少如请求超时自动取消、请求重试、进度监控等高级特性不过这些正在逐步完善。2. node-fetch库 (兼容旧版本Node.js或需要特定功能的项目)这是一个将浏览器fetchAPI移植到Node.js环境的第三方库在原生fetch成熟之前它是事实上的标准。优势广泛的兼容性支持低至Node.js v12的版本是旧项目或环境无法升级时的救星。社区生态成熟拥有大量的使用者常见问题通常都能找到社区解决方案。其API与Web标准高度一致。可预测性在原生fetch尚未稳定的时期它提供了一个行为稳定的替代品。劣势额外依赖需要安装和维护这个第三方包。潜在的API差异虽然极力模仿但与原生实现或浏览器实现之间可能存在细微差别尤其是在处理流Stream、重定向或某些错误边界时。未来迁移成本如果项目后期升级Node.js并想转向原生fetch可能需要修改代码尽管大部分兼容。3. 其他HTTP客户端 (如axios, got)这些是功能更全面的HTTP客户端它们有自己的API设计哲学但通常也提供类似fetch的体验或兼容层。优势功能强大内置请求/响应拦截器、自动转换JSON数据、取消请求、文件上传、HTTP/2支持等高级功能。开发者体验通常提供更便捷的API例如axios直接区分axios.get()、axios.post()。劣势非标准API如果你追求与浏览器fetchAPI的一致性那么这些库的API是不同的需要单独学习。体积更大功能丰富意味着包体积通常比单纯的fetchpolyfill要大。注意在Node.js v16及更早版本中尝试直接使用fetch会导致ReferenceError: fetch is not defined错误。这是你判断环境是否支持原生fetch的最直接信号。2.2 如何做出你的技术选型我的决策逻辑通常遵循以下流程你可以参考检查并锁定Node.js版本这是第一步也是最重要的一步。运行node -v确认生产环境和开发环境的Node.js版本。如果18.x LTS优先考虑原生fetch。评估项目需求如果是全新的微服务、API Server或脚本工具且对运行环境有控制权如使用Docker容器可自由选择Node.js版本强烈推荐使用Node.js v18 LTS并直接采用原生fetch。这是最干净、最面向未来的选择。如果是需要兼容旧企业环境的遗留项目或者是一个需要被广泛安装的命令行工具CLI用户Node.js版本不可控那么使用node-fetch是更安全的选择。如果你的应用需要非常复杂的HTTP交互逻辑如请求重试、缓存、高级拦截等可以考虑axios或got它们开箱即用。你也可以在原生fetch基础上封装这些功能。制定降级/回退策略对于库Library或框架的开发者你需要考虑用户的多样性。一种常见的模式是优先尝试使用原生fetch如果不存在则动态加载require或importnode-fetch作为备选。这需要一些条件判断代码。// 一个简单的降级策略示例 let fetchImplementation; if (globalThis.fetch) { // 使用原生fetch fetchImplementation globalThis.fetch; } else { // 动态引入node-fetch。注意在ES模块中需使用异步import() const { default: nodeFetch } await import(node-fetch); fetchImplementation nodeFetch; } // 后续使用 fetchImplementation 进行请求3. 从零开始在不同环境中安装与配置确定了方案接下来就是搭建环境。这里我们分场景详细说明。3.1 场景一使用Node.js原生fetch (v18)如果你的Node.js版本符合要求那么你不需要安装任何东西。fetch、Request、Response、Headers这些Web API已经是全局可用的了。你可以创建一个最简单的test.js文件来验证// test.js try { const response await fetch(https://api.github.com); console.log(原生fetch可用状态码:, response.status); } catch (error) { console.error(原生fetch不可用或请求失败:, error.message); }在终端运行node test.js如果看到状态码输出如200恭喜你环境已经就绪。重要配置点对于Node.js v18.0.0你可能还需要在启动时加上--experimental-fetch标志但在v18.17.0之后的LTS版本中它已是默认启用且稳定的。你可以通过node -p process.versions查看Node.js详细版本并查阅对应版本的官方文档确认。3.2 场景二使用node-fetch库这是目前更普遍的场景因为很多项目还未升级到v18。1. 初始化项目与安装首先确保你有一个Node.js项目如果没有使用npm init -y初始化。然后在项目根目录下安装node-fetch。# 使用 npm npm install node-fetch # 或使用 yarn yarn add node-fetch # 或使用 pnpm pnpm add node-fetch2. 不同模块系统的引入方式这是第一个容易踩坑的地方。node-fetch从v3版本开始只提供ES模块ESM支持。这意味着你的项目文件必须是.mjs扩展名或者package.json中设置了type: module。而v2版本则同时支持CommonJS和ESM。如果你的项目是ES模块.mjs文件或设置了type: module// 正确使用ESM的import语法 import fetch from node-fetch;如果你的项目是CommonJS.js文件且未设置type: module方案A安装v2版本不推荐v2已停止维护。npm install node-fetch2方案B使用动态import()推荐异步方式。// 在CommonJS中使用node-fetch v3 const fetch (...args) import(node-fetch).then(({default: fetch}) fetch(...args)); // 然后在一个async函数中使用 async function makeRequest() { const response await fetch(https://api.github.com); const data await response.text(); console.log(data); } makeRequest();方案C将你的项目迁移到ESM长期来看是最好的选择。实操心得我强烈建议新项目直接采用ESM规范。这不仅是为了兼容node-fetchv3更是因为ESM是JavaScript官方的模块标准越来越多的生态库正在转向ESM。迁移虽然初期有阵痛但能避免未来更多的兼容性问题。你可以在package.json中加入type: module来开启。3. 处理TypeScript项目在TypeScript项目中使用node-fetch你还需要安装对应的类型定义文件。npm install --save-dev types/node-fetch然后在你的TS文件中引入import fetch from node-fetch; // TypeScript现在能正确识别fetch的类型3.3 环境变量与代理配置在企业网络或某些特定环境下你可能需要通过代理服务器访问外部网络。fetch无论是原生还是node-fetch默认会尊重系统的HTTP_PROXY、HTTPS_PROXY和NO_PROXY环境变量。在Linux/macOS的终端中临时设置export HTTPS_PROXYhttp://your-proxy:port node your-script.js在Windows的CMD中临时设置set HTTPS_PROXYhttp://your-proxy:port node your-script.js在代码中通过Agent显式配置以node-fetch为例原生fetch目前不支持直接传入自定义agent需通过undici配置这是另一个复杂话题import fetch from node-fetch; import { HttpsProxyAgent } from https-proxy-agent; const proxyAgent new HttpsProxyAgent(http://your-proxy:port); const response await fetch(https://api.example.com, { agent: proxyAgent });如果你遇到fetch请求在本地成功但在服务器失败或者出现奇怪的网络超时首先检查代理配置和环境变量。4. 基础到进阶fetch API的实战应用详解无论底层实现是原生还是node-fetch其API都与Web标准基本一致。让我们通过实例从最简单的GET请求深入到复杂的场景。4.1 发起一个简单的GET请求这是最常见的操作。注意fetch()返回一个Promise它解析为一个Response对象。这个Response代表了HTTP响应的整个状态但响应体body本身可能还没有被完全读取。import fetch from node-fetch; // 或直接使用全局fetch const url https://jsonplaceholder.typicode.com/posts/1; async function getPost() { try { // 1. 发起请求获取Response对象 const response await fetch(url); // 2. 关键检查响应状态是否成功状态码在200-299之间 if (!response.ok) { // 如果响应不成功抛出错误包含状态码和状态文本 throw new Error(HTTP错误! 状态码: ${response.status} ${response.statusText}); } // 3. 解析响应体。根据内容类型选择方法。 // 对于JSON响应 const data await response.json(); console.log(获取到的数据:, data); // 其他常见的解析方法 // const text await response.text(); // 解析为纯文本 // const blob await response.blob(); // 解析为Blob对象Node.js中有限支持 // const arrayBuffer await response.arrayBuffer(); // 解析为ArrayBuffer } catch (error) { // 捕获网络错误、解析错误或我们抛出的HTTP错误 console.error(请求失败:, error.message); } } getPost();关键点解析response.ok这是一个非常方便的布尔属性当response.status在200-299范围内时为true。永远不要只检查status 200因为像201Created、204No Content也是成功的状态。response.json()这个方法也是异步的返回Promise因为它需要从网络流中读取完整的响应体并解析为JSON。忘记await它是一个常见错误。错误处理fetch只在网络故障如DNS解析失败、连接被拒绝时才会拒绝rejectPromise。对于HTTP错误状态404 500等Promise仍然是兑现fulfilled的你需要通过response.ok或response.status来手动检查。这是fetch与axios等库的一个重要区别axios默认会将HTTP错误状态也视为reject。4.2 构造复杂的POST/PUT请求发送数据到服务器需要配置method、headers和body选项。async function createPost() { const url https://jsonplaceholder.typicode.com/posts; const postData { title: 我的新文章, body: 这是一篇关于fetch API的精彩内容。, userId: 1, }; const response await fetch(url, { method: POST, // 或 PUT, PATCH, DELETE headers: { // 必须根据你发送的body类型设置正确的Content-Type Content-Type: application/json, // 可以添加其他头如认证令牌 Authorization: Bearer your-token-here, }, // body可以是字符串、FormData、Buffer、URLSearchParams等 body: JSON.stringify(postData), // 将JavaScript对象序列化为JSON字符串 }); if (!response.ok) { const errorText await response.text(); // 尝试获取服务器返回的错误信息 throw new Error(创建失败: ${response.status} - ${errorText}); } const newPost await response.json(); console.log(创建成功新文章ID:, newPost.id); return newPost; }Content-Type的学问application/json当你发送JSON字符串时使用。application/x-www-form-urlencoded发送表单格式数据body应为new URLSearchParams({key: value})生成的字符串。multipart/form-data用于文件上传。在浏览器中可以用FormData对象在Node.js中特别是node-fetch处理起来稍复杂通常需要借助form-data这个npm包来构建请求体。4.3 处理超时与取消请求原生fetch和node-fetch早期版本一个被诟病的点是没有内置的超时机制。这意味着一个请求可能会永远挂起。我们必须自己实现。1. 使用AbortController实现超时现代推荐方式AbortController是Web标准的一部分用于中止一个或多个Web请求。Node.js原生fetch和node-fetchv3都支持它。async function fetchWithTimeout(resource, options {}, timeout 8000) { // 1. 创建一个AbortController实例 const controller new AbortController(); // 获取它的signal信号 const { signal } controller; // 2. 设置一个定时器在超时后触发abort const timeoutId setTimeout(() { controller.abort(); // 中止请求 console.log(请求超时: ${resource}); }, timeout); // 3. 发起fetch请求传入signal try { const response await fetch(resource, { ...options, signal, // 将signal关联到这次fetch请求 }); // 请求成功完成清除超时定时器 clearTimeout(timeoutId); return response; } catch (error) { // 请求失败清除定时器 clearTimeout(timeoutId); // 判断错误是否是由abort引起的 if (error.name AbortError) { console.error(请求被主动中止超时); // 这里可以抛出一个自定义的超时错误 throw new Error(请求超时${timeout}ms); } else { // 其他类型的错误网络错误等 console.error(请求发生其他错误:, error.message); throw error; } } } // 使用示例 try { const response await fetchWithTimeout(https://httpbin.org/delay/10, {}, 5000); // 5秒超时 const data await response.json(); } catch (error) { console.error(捕获到的错误:, error.message); // 将输出“请求超时5000ms” }2. 使用Promise.race的旧方案备选在AbortController不被支持的环境极老的Node.js或浏览器可以使用Promise.race。function fetchWithTimeoutOld(resource, options, timeout 8000) { // 创建一个会在超时后reject的Promise const timeoutPromise new Promise((_, reject) { setTimeout(() reject(new Error(请求超时${timeout}ms)), timeout); }); // 比赛是fetch先完成还是超时先发生 return Promise.race([ fetch(resource, options), timeoutPromise ]); } // 注意此方案有一个严重缺陷即使请求超时被reject底层的fetch请求可能仍在后台进行无法真正中止会浪费资源。实操心得务必为生产环境的每一个外部HTTP请求设置合理的超时。超时时间应根据接口的SLA服务等级协议来定通常快速API可以设为5-10秒文件上传等操作可以更长。AbortController是当前最优雅和正确的解决方案。4.4 处理流式响应与大文件下载对于大体积的响应如文件下载一次性将数据读入内存response.json()或response.text()可能导致内存溢出。fetch的响应体response.body是一个Node.js可读流Readable Stream我们可以流式处理。import fs from fs; import { pipeline } from stream/promises; async function downloadFile(url, filePath) { const response await fetch(url); if (!response.ok) { throw new Error(下载失败: ${response.status}); } // 1. 获取响应体作为可读流 const readableStream response.body; // 2. 创建一个写入到本地文件的流 const writableStream fs.createWriteStream(filePath); // 3. 使用pipeline管理流它会自动处理错误、关闭和管道连接 await pipeline(readableStream, writableStream); console.log(文件已下载至: ${filePath}); } // 使用示例 await downloadFile(https://example.com/large-video.mp4, ./video.mp4);关键点response.body在Node.js环境中是一个Readable流。使用stream/promises中的pipeline函数是处理流的最佳实践它比手动监听data、end事件或使用.pipe()更安全能妥善处理错误和清理工作。这种方式内存占用非常小无论文件多大都只使用一个固定大小的缓冲区。4.5 处理Cookie与会话默认情况下fetch不会像浏览器那样自动发送或存储Cookie。如果你需要处理基于Cookie的会话例如模拟登录需要手动处理Cookie请求头并可能使用像tough-cookie这样的库来管理Cookie jar。一个简单的示例手动传递Cookieconst someCookie sessionIdabc123; userId456; const response await fetch(https://api.example.com/protected, { headers: { Cookie: someCookie } }); // 从响应中读取Set-Cookie头 const setCookieHeader response.headers.get(set-cookie); if (setCookieHeader) { console.log(服务器设置了新的Cookie:, setCookieHeader); // 你需要解析这个字符串并在后续请求中携带它 }对于复杂的会话管理建议使用专门的HTTP客户端库如axios它内置了withCredentials选项和Cookie jar支持或者在fetch基础上封装自己的会话逻辑。5. 避坑指南那些官方文档没明说的问题在实际项目中我踩过不少坑。下面这些问题是搜索引擎的高频词也是开发者的血泪史。5.1 问题一fetch返回的Promise只在网络错误时reject这是fetch设计上最需要适应的一点。一个返回404或500的请求在fetch看来依然是“成功的请求”Promise fulfilled。你必须手动检查response.ok或response.status。错误示范// 这样写404错误会被吞掉 fetch(/api/not-found) .then(response response.json()) .then(data console.log(data)) // 如果404这里会报解析错误因为响应体不是JSON .catch(error console.error(捕获到错误, error)); // 可能捕获的是JSON解析错误而非HTTP错误正确做法fetch(/api/not-found) .then(async (response) { if (!response.ok) { // 尝试读取错误信息可能是文本也可能是JSON const errorText await response.text(); throw new Error(请求失败 ${response.status}: ${errorText}); } return response.json(); }) .then(data console.log(data)) .catch(error console.error(请求全过程错误:, error));5.2 问题二response.json()解析失败当服务器返回的不是有效的JSON比如HTML错误页面、空响应或纯文本调用response.json()会抛出SyntaxError。防御性代码async function safeJsonParse(response) { const contentType response.headers.get(content-type); if (!contentType || !contentType.includes(application/json)) { // 如果不是JSON返回文本 const text await response.text(); return { _raw: text }; // 或者根据业务逻辑处理 } try { return await response.json(); } catch (e) { // 即使Content-Type是JSON也可能解析失败 console.warn(JSON解析失败返回原始文本, e); const text await response.text(); return { _raw: text, parseError: e.message }; } } // 使用 const response await fetch(someUrl); const data await safeJsonParse(response);5.3 问题三并发限制与连接池Node.js底层的HTTP客户端undici 自v18起fetch基于它有默认的连接池限制。如果你同时发起海量例如成千上万的fetch请求可能会遇到性能瓶颈甚至 socket 耗尽错误。现象大量请求排队延迟增加可能出现ECONNRESET或socket hang up错误。解决方案控制并发量使用如p-limit、async库的queue或Promise.allSettled配合分片将大量请求分批进行。import pLimit from p-limit; const limit pLimit(10); // 最多同时10个请求 const urls [...]; // 1000个URL const promises urls.map(url limit(() fetchAndProcess(url))); const results await Promise.allSettled(promises);调整Agent配置针对node-fetch或底层http对于node-fetch你可以传递自定义的agent来自http或https模块并设置maxSockets等参数。原生fetch的配置更底层涉及undici的选项。5.4 问题四SSL/TLS证书问题在开发环境中特别是使用自签名证书测试HTTPS服务时fetch会抛出unable to verify the first certificate或self signed certificate错误。警告以下方案仅用于开发测试生产环境必须使用有效证书。方案A设置 rejectUnauthorized (不推荐用于生产)通过自定义Agent来禁用证书验证node-fetchimport https from https; import fetch from node-fetch; const agent new https.Agent({ rejectUnauthorized: false // 危险跳过证书验证 }); const response await fetch(https://self-signed.badssl.com, { agent });原生fetch目前没有直接提供此选项需要通过更复杂的方式配置底层undici的connect选项。方案B设置环境变量NODE_TLS_REJECT_UNAUTHORIZED (更不推荐)在启动Node.js进程前设置NODE_TLS_REJECT_UNAUTHORIZED0 node your-script.js这会禁用整个进程的所有TLS证书验证极度危险仅用于临时测试。正确做法在开发/测试环境将自签名证书添加到系统的受信任根证书库或使用工具如mkcert生成本地可信证书。5.5 问题五内存泄漏与资源释放fetch请求完成后如果响应体没有被完全读取或消费可能会导致内存或连接资源未被及时释放。错误示范// 只读取了部分数据就停止了 const response await fetch(url); const reader response.body.getReader(); const { value, done } await reader.read(); // 如果没有继续读到donetrue流就没有关闭正确做法如果使用response.json()、response.text()等便捷方法它们会帮你消费完整个流。如果使用流式接口response.body确保流被完全读取或手动取消。// 使用pipeline或readableStream.destroy()确保清理 const stream response.body; // 方案1: 使用pipeline消费到底 // 方案2: 如果中途需要停止 // stream.destroy(); // 销毁流释放资源5.6 常见错误信息速查表错误信息可能原因解决方案fetch is not definedNode.js版本低于v17.5且未安装node-fetch升级Node.js至v18 LTS或安装node-fetch库。Cannot find package node-fetch未安装node-fetch或安装失败运行npm install node-fetch。检查网络和npm源。Error [ERR_REQUIRE_ESM]: require() not support在CommonJS文件中用require()引入了node-fetchv3改用ESM的import语法或使用动态import()或降级到node-fetch2。Unexpected token in JSON at position 0response.json()解析失败服务器返回了HTML如404页面先检查response.ok和Content-Type再决定是否调用.json()。socket hang up/ECONNRESET服务器过早关闭连接或客户端并发过高增加超时时间控制请求并发量检查服务器稳定性。net::ERR_CERT_AUTHORITY_INVALID(浏览器) 或unable to verify the first certificate(Node.js)SSL证书无效或自签名开发环境可临时配置rejectUnauthorized: false仅测试生产环境必须使用有效证书。请求无限挂起无响应未设置超时服务器无响应或网络问题务必使用AbortController设置请求超时。TypeError: Failed to parse URL提供的URL字符串格式不正确检查URL是否包含非法字符是否完整如缺少协议http://。使用new URL()构造函数验证。6. 性能优化与高级实践掌握了基础用法和避坑技巧后我们可以关注如何让fetch用得更高效、更健壮。6.1 连接复用与Keep-AliveHTTP/1.1默认启用了Keep-AliveNode.js的HTTP Agent也会复用TCP连接。对于原生fetch基于undici其连接池默认就是开启且优化的。对于node-fetch你可以通过传递一个复用的agent来提升性能import https from https; import fetch from node-fetch; // 创建一个可复用的Agent实例 const keepAliveAgent new https.Agent({ keepAlive: true, // 启用连接保持 maxSockets: 50, // 每个主机最大socket数 maxFreeSockets: 10, // 空闲时保持的最大socket数 }); async function makeMultipleRequests() { const options { agent: keepAliveAgent }; // 一系列到同一主机的请求将复用连接 const req1 fetch(https://api.example.com/endpoint1, options); const req2 fetch(https://api.example.com/endpoint2, options); // ... }6.2 实现请求重试机制网络不稳定偶尔的失败是正常的。一个健壮的客户端应该具备重试能力。async function fetchWithRetry(url, options {}, maxRetries 3, baseDelay 1000) { let lastError; for (let attempt 1; attempt maxRetries; attempt) { try { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), options.timeout || 10000); const response await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { return response; // 成功直接返回 } // 如果是服务器错误5xx可以考虑重试 if (response.status 500 response.status 600) { lastError new Error(服务器错误 ${response.status}); } else { // 客户端错误4xx通常重试无意义 throw new Error(客户端错误 ${response.status}); } } catch (error) { lastError error; // 如果是中止错误超时或网络错误进行重试 if (error.name AbortError || error.code ECONNRESET || error.code ETIMEDOUT) { console.warn(请求失败开始第${attempt}次重试..., error.message); } else { // 其他错误如语法错误、非重试的HTTP错误直接抛出 throw error; } } // 指数退避延迟第一次等1秒第二次等2秒第三次等4秒... if (attempt maxRetries) { const delay baseDelay * Math.pow(2, attempt - 1); await new Promise(resolve setTimeout(resolve, delay)); } } // 所有重试都失败 throw lastError; }6.3 封装一个健壮的通用fetch工具函数结合超时、重试、错误处理、日志我们可以封装一个用于生产环境的fetch工具。// utils/fetchClient.js import fetch from node-fetch; // 或使用全局fetch class FetchClient { constructor(baseURL , defaultOptions {}) { this.baseURL baseURL; this.defaultOptions { timeout: 10000, retries: 2, ...defaultOptions, }; } async request(endpoint, options {}) { const url ${this.baseURL}${endpoint}; const mergedOptions { ...this.defaultOptions, ...options }; const { timeout, retries, ...fetchOptions } mergedOptions; let lastError; for (let i 0; i retries; i) { try { const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), timeout); const response await fetch(url, { ...fetchOptions, signal: controller.signal, }); clearTimeout(timeoutId); // 处理非2xx的HTTP状态码 if (!response.ok) { const errorText await response.text().catch(() 无法读取错误信息); const error new Error(HTTP ${response.status}: ${errorText}); error.status response.status; // 5xx错误且还有重试次数则继续循环 if (response.status 500 i retries) { lastError error; console.warn([FetchClient] 服务器错误准备重试 (${i1}/${retries}), url); await this._waitForRetry(i); continue; } throw error; // 客户端错误或重试次数用尽直接抛出 } // 根据Content-Type决定如何解析响应 const contentType response.headers.get(content-type) || ; if (contentType.includes(application/json)) { return await response.json(); } else if (contentType.includes(text/)) { return await response.text(); } else { // 其他类型返回原始的Response对象让调用者处理 return response; } } catch (error) { lastError error; // 网络错误、超时错误且还有重试次数 if ((error.name AbortError || error.code ECONNRESET) i retries) { console.warn([FetchClient] 网络错误准备重试 (${i1}/${retries}), url, error.message); await this._waitForRetry(i); continue; } // 其他错误或重试次数用尽跳出循环 break; } } // 所有重试都失败 throw lastError; } _waitForRetry(attempt) { const delay 1000 * Math.pow(2, attempt); // 指数退避 return new Promise(resolve setTimeout(resolve, delay)); } // 便捷方法 get(endpoint, options) { return this.request(endpoint, { method: GET, ...options }); } post(endpoint, body, options) { return this.request(endpoint, { method: POST, headers: { Content-Type: application/json, ...options?.headers }, body: JSON.stringify(body), ...options, }); } // 可以继续添加put, delete, patch等方法... } // 导出单例或类 export const apiClient new FetchClient(https://api.example.com/v1, { headers: { User-Agent: MyApp/1.0 }, }); // 使用示例 // try { // const user await apiClient.get(/users/123); // const newPost await apiClient.post(/posts, { title: Hello }); // } catch (error) { // console.error(API请求失败:, error.status, error.message); // }这个封装提供了基础URL、默认配置、自动重试、超时、错误分类和响应内容类型判断是一个不错的起点你可以根据项目需求进一步扩展。