ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

fetch和axios别只背区别:从设计哲学到实战踩坑的深度对比

fetch和axios别只背区别:从设计哲学到实战踩坑的深度对比 前端面试里有一道出现频率极高的题fetch 和 axios 有什么区别多数人背完几个关键词就觉得自己会了可真到项目里要定请求方案或者要把老项目里的请求层重构一遍时该纠结还是纠结。我接过一次大规模重构想把所有 axios 调用改成 fetch改到一半发现拦截器、超时、进度这些能力全部要手搓代码量没少多少反而多了一堆补丁函数。另一个项目里又反着来想把 fetch 彻底换回 axios结果发现短平快的小页面根本没有必要引入这个库。这两次经历让我意识到一件事fetch 和 axios 从来不是简单的谁替代谁而是你站在哪一层看问题。这篇文章我不想再给你背fetch 是原生、axios 是库这种标准答案而是从实际开发的角度把两者的设计逻辑、能力边界、踩坑经验掰开揉碎聊一遍。不管你是新手还是已经写了两三年前端这篇都值得花十分钟读完。1. 先厘清出身原生能力与第三方封装如何去选1.1 两个东西的血缘完全不一样fetch 是浏览器内置的 Web API标准归 WHATWG 管底层设计思路是基于 Promise 的 Request/Response 模型。它从 Chrome 42 左右开始进入主流浏览器此后一路补齐了 AbortController、ReadableStream 等配套能力Node.js 到 18 版本也把 fetch 做成了内置能力。也就是说fetch 不是什么库而是运行环境直接提供给你的原住功能。axios 是第三方库社区维护底层在浏览器端走的是 XMLHttpRequestXHR那套老路线在 Node 端则走 http/https 模块。后来 axios 也支持了 fetch adapter但它的核心心智模型还是配置对象驱动 XHR 能力补齐。这个出身差异决定了后面几乎所有的行为差异。fetch 要考虑的是浏览器标准规定我要怎么用axios 要考虑的是开发者日常用请求时到底需要什么便利。1.2 兼容性的坐标系完全不同回到实际项目兼容性是最容易被忽略的一环。fetch 在 IE 阵营全军覆没IE11 都不支持axios 因为底层是 XHR在 IE11 上配合 polyfill 还能跑这也是很多老项目到今天仍然不敢碰 fetch 的原因。现代浏览器里 fetch 已经是标配但要注意几个特殊环境小程序环境微信小程序、支付宝小程序的网络请求是独立 API既不等于 fetch 也不等于 XHRaxios 需要专门适配。React Native全局有 fetch但没有 XHR 的完整实现axios 在 RN 里走的是 fetch adapter。Node.jsNode 18 之后原生 fetch 可用但如果你用 Node 16还是老老实实装 axios 或者 undici 之类。所以兼容性这件事不能只看浏览器支持度要看你项目的运行环境矩阵。如果是一个纯 Web 项目目标用户都是现代浏览器那兼容性基本不构成选型阻力如果要做老浏览器适配axios 反而是更稳的选择。1.3 体积和依赖带来的隐性影响axios 压缩后大概在 14KB 左右加上依赖可能到 20KB 上下。这个体积对现代应用来说不算什么但少引一个依赖的意义不在带宽而在供应链安全。近几年前端依赖链上的安全事件不少每一次引入第三方库都是在增加审查成本。fetch 零依赖这对一些小工具、浏览器扩展、Edge Function、Serverless 函数来说很有吸引力。我写 Cloudflare Worker 或 Vercel Edge 函数时能用原生 fetch 就绝对不引 axios冷启动体积和部署速度都有优势。不过体积不能单独作为决策依据。axios 那 14KB 买来的是成熟稳定的封装能力如果你项目里已经在大量使用拦截器、上传进度、统一错误处理那这 14KB 的 ROI 其实很高。2. API 设计的分水岭请求体、响应体和错误处理2.1 发起请求的姿势折射出两种设计哲学fetch 的标准用法是 fetch(url, options)一个 URL 加一个配置对象。它对应的语义是你要发一个网络请求到这个地址Request 和 Response 都是独立的可操作对象。axios 的用法则是 axios(config) 或者 axios.get / axios.post 这类便捷方法。它更接近我要完成一次携带特定数据的 HTTP 请求底层帮你把配置展开成 XMLHttpRequest 的调用。两种姿势本身没有高下但写起来体感差别很大。// fetch 的经典写法 const response await fetch(/api/users, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ name: 张三 }) }); // axios 的经典写法 const response await axios.post(/api/users, { name: 张三 });细看就能发现axios 把序列化 JSON 设置 Content-Type 设置请求体这三件事合并成了一个参数。fetch 把每一步都摊开了灵活是灵活出错概率也高。2.2 响应体的读取逻辑一个容易被忽略的坑fetch 返回的 response 对象body 是一个 ReadableStream不能直接拿到数据。你必须先调用 response.json()、response.text()、response.formData() 等方法把这个流消费掉才能得到真实数据。而且一个响应流只能消费一次你不能先 json() 再 text()第二次会直接报错。axios 不一样axios 发请求后默认就会根据响应头帮你做序列化数据直接放在 response.data 里。也就是说 axios 把响应流消费这个动作前置做掉了。我见过很多新手写 fetch 代码时在 response 对象上找 data 字段找了半天找不到最后才发现要 res.json()。这不是蠢是 fetch 的设计意图本身就更底层——它不替你决定你想要的解析格式这在一些特殊场景比如你想直接拿到 ArrayBuffer 处理二进制流反而是优点。2.3 HTTP 状态码的处理哲学两边截然相反这是 fetch 和 axios 最核心的差异之一也是误解最深的一个点。fetch 的 Promise 只有在网络层面出错时才会 reject比如断网、DNS 解析失败、连接被拒绝、CORS 拦截。当你向服务器发出请求且服务器返回了 404、500 这类 HTTP 错误状态码时fetch 不会抛异常它照常 resolve只是 response.ok 为 false。// fetch 这样避开4xx/5xx 不报错的坑 const response await fetch(/api/not-found); if (!response.ok) { // 手动处理 404、500 throw new Error(HTTP error: ${response.status}); }axios 相反它默认只有 2xx 状态码才会走 resolve其他状态码都会进入 reject并且错误对象里带着 response 字段包含状态码、状态文本、响应体数据。// axios 的 404 直接进 catch try { const response await axios.get(/api/not-found); } catch (error) { console.log(error.response.status); // 404 console.log(error.response.data); // 服务端返回的详细错误信息 }从开发者体感来说axios 更自动fetch 更原始。但这不代表 axios 的哲学一定更好。有些项目对请求完成但业务失败的场景有自己的一套约定比如 HTTP 200 返回业务码 errorCode这种情况下 fetch 的不主动抛错反而更贴近你要的处理方式。2.4 请求头和请求体的类型处理差距fetch 里设置请求头要用 Headers 对象axios 里直接用普通对象就行。虽然 fetch 也允许传普通对象给 headers 字段但当你需要读取响应头时必须通过 response.headers 这个 Headers 对象去 get不能当普通对象用。请求体方面差距更明显。fetch 发送 JSON 时如果你直接写 body: JSON.stringify(data)不带任何 headers那 Content-Type 默认是 text/plain;charsetUTF-8。大多数后端框架不会自动解析这种格式接口直接 400。所以 fetch 发 JSON 必须手动设置 Content-Type: application/json。axios 只要你传的是普通对象它会自动把 data 做 JSON 序列化并设置好 Content-Type。如果你传的是 FormDataaxios 会自动带上 multipart/form-data 形式的头不会踩到坑。这能得出一个实操判断如果你团队里有不少刚入门的前端用 axios 可以少掉一批为什么后端收不到参数的弱智问题如果团队整体基础扎实那 fetch 的显式操作反而让你更清楚请求到底发成了什么样。3. 项目实战里最容易拉开差距的四个能力对比3.1 拦截器axios 自带fetch 要手搓拦截器是 axios 最让人上头的功能之一。请求发出之前你可以在拦截器里统一加 token、改 Header、做埋点、做节流响应回来之后可以统一解析业务码、弹错误提示、跳转登录页下游业务代码就不用每个接口都写一遍 try-catch。// axios 拦截器 axios.interceptors.request.use((config) { config.headers.Authorization Bearer ${localStorage.getItem(token)}; return config; }); axios.interceptors.response.use( (response) response.data, (error) { if (error.response?.status 401) { redirectToLogin(); } return Promise.reject(error); } );fetch 没有拦截器概念你需要自己包一层封装函数// fetch 的伪拦截器 async function request(url, options {}) { const token localStorage.getItem(token); const response await fetch(url, { ...options, headers: { Content-Type: application/json, ...(token ? { Authorization: Bearer ${token} } : {}), ...options.headers } }); if (response.status 401) { redirectToLogin(); } return response.json(); }说实话手搓拦截器不难难的是规范。项目一大如果每个人各写各的 request 封装最后会乱成一锅粥。所以选 axios 的团队通常等于默认接受了一套成熟的拦截器心智模型这对长期维护是有利的。3.2 取消请求两边现在都靠 AbortController很多老文章说 axios 有 CancelTokenfetch 没有取消机制这已经是过时信息了。axios 从 0.22.0 开始推荐使用 AbortController官方不再建议使用 CancelTokenfetch 原生支持 AbortController两边现在站在同一条起跑线上。// 用 AbortController 取消请求 const controller new AbortController(); const { signal } controller; fetch(/api/search, { signal }) .then((response) response.json()) .catch((error) { if (error.name AbortError) { console.log(请求被取消); } }); // 某些场景需要取消时 controller.abort();axios 用法几乎一样const controller new AbortController(); axios.get(/api/search, { signal: controller.signal }) .catch((error) { if (error.name AbortError) { console.log(请求被取消); } }); controller.abort();最典型的场景是搜索防抖和列表页竞态。用户输入关键词 A 发起请求又输入关键词 B 发起请求A 的响应后到就会覆盖 B 的结果。解决办法就是在发起新请求前 abort 掉上一次请求。现在两边都能做不用再纠结。3.3 超时控制axios 一个配置项fetch 要自己算axios 有 timeout 配置单位是毫秒到点直接中断请求并抛出错误。axios.get(/api/xxx, { timeout: 5000 });fetch 没有 timeout 参数需要你自己组合 AbortController 或者超时 signal 来实现。传统写法是 setTimeout 里调用 controller.abort()但这样代码会比较啰嗦。现代浏览器现在有 AbortSignal.timeout() 这个静态方法可以直接生成一个定时自动中止的 signal// fetch 超时5 秒 fetch(/api/xxx, { signal: AbortSignal.timeout(5000) });不过这有两个坑。一是 AbortSignal.timeout() 的兼容性没有 AbortController 那么覆盖全家桶老旧浏览器需要 polyfill二是超时中止后错误对象可能也是 AbortError代码里要区分用户主动取消和超时取消否则容易误判。如果你用的是旧式 Node 环境AbortSignal.timeout 也不一定可用。所以基线上看axios 的超时是开箱即用 错误语义清晰fetch 的超时是能实现但要自己弄干净。3.4 上传/下载进度axios 现成fetch 要自己读流上传进度是 axios 压倒性优势之一。XHR 天然支持 upload.onprogress 事件axios 把这个能力稳定暴露为 onUploadProgress 回调几行代码就能做出靠谱的上传进度条。axios.post(/api/upload, formData, { onUploadProgress: (progressEvent) { const percent Math.round((progressEvent.loaded / progressEvent.total) * 100); console.log(上传进度${percent}%); } });fetch 的上传进度没有原生方案你只能借助 XMLHttpRequest 或者第三方流处理库。下载进度方面 fetch 倒是能做因为 response.body 是 ReadableStream你可以用流读取的方式算出进度const response await fetch(/api/download); const contentLength Number(response.headers.get(Content-Length)); const reader response.body.getReader(); let received 0; while (true) { const { done, value } await reader.read(); if (done) break; received value.length; console.log(下载进度${Math.round((received / contentLength) * 100)}%); }但注意两个前提服务器要返回 Content-Length 响应头否则你拿不到总长度如果你用了一些压缩中间件Content-Encoding: gzipContent-Length 长度和实际读取字节数对不上进度条会超过 100% 或者卡在 99%。这个问题在 axios 的 onDownloadProgress 里也存在但 axios 因为做了内部处理体感会好一些。我的建议是但凡项目里有上传文件这种核心交互直接用 axios 或基于 XHR 的方案别拿 fetch 硬扛。4. failed to fetch 背后两组错误的解读方式完全不同4.1 同样是报错axios 的错和 fetch 的错信息量差太多用了 fetch 的人十有八九都在控制台见过那行大字TypeError: Failed to fetch。这段英文极其反人类它在不同浏览器里可能是完全不同的底层原因但统一收敛成一句NetworkError when attempting to fetch resource。axios 的错误对象则包含四样关键信息config 是发起请求时完整配置、request 是底层请求对象、response 是服务端返回的响应如果收到了的话、isAxiosError 是判断这个错误的专属标记。这就意味着你用 axios 时可以非常容易地在日志里还原当时发了什么请求、服务器到底有没有响应、响应体是什么。fetch 的错误对象基本只有一个 message。Windows 上还出现过 fetch 网络栈抛出的不同枚举错误Chrome 和 Firefox 的措辞也偶尔不一样。这就导致线上排查问题时你拿到一条Failed to fetch基本等于没有线索只能去翻网络面板和服务器日志。4.2 failed to fetch 最常见的五个来源第一真断网或者连接不可达。域名解析失败、TCP 连接被拒绝、服务器主动断开都会报 Failed to fetch。第二CORS 预检失败。浏览器判断你的请求是非简单请求先发一个 OPTIONS 预检如果后端没有正确返回 CORS 头浏览器直接把请求拦截掉前端拿到的就是 Failed to fetch。这个错误在前后端分离开发中太常见了尤其是你改了请求头的自定义字段、改了 Content-Type 为 application/json 时。第三SSL 证书校验失败。本地用了自签名证书或者线上证书过期、域名不匹配浏览器在 TLS 握手阶段认为连接不安全请求直接失败报的也是 Failed to fetch。第四请求被中断。用户切后台、浏览器扩展拦截、页面卸载、AbortController 被调用这些情况下 fetch 也会报类似错误。第五本地开发代理配置问题。用 Vite 或 Webpack 开发服务器做代理时后端地址写错、代理转发的 target 配错前端访问一个根本不存在的地址自然也会失败。有一条完整排查链我建议背下来先开浏览器 Network 面板看重发请求的原始请求是否真的发出去。如果请求是红色的且没有响应优先查 CORS 和地址如果请求根本没出现在面板里优先查扩展拦截和代码里的 URL 拼接如果请求发出去了但状态是 pending 直到超时优先查代理配置和网络本身。4.3 ABC Connection Reset 这类冷门错误怎么收敛除了 Failed to fetchfetch 在部分平台还会抛出一些底层网络错误比如 Connection Reset、Network is unreachable、ERR_SSL_PROTOCOL_ERROR 这类名目。你会发现这些错误在不同浏览器里不保证统一这给前端错误监控带来了不小麻烦。我的做法是在全局错误处理器里做一个归一化函数把所有网络异常统一转成自己项目的 ErrorCode。function normalizeFetchError(error) { if (error.name AbortError) { return { code: REQUEST_CANCELED, message: 请求已取消 }; } // fetch 抛出的 TypeError通常代表网络层不可达 if (error instanceof TypeError) { return { code: NETWORK_ERROR, message: 网络连接异常请检查网络后重试 }; } // 超时用 AbortSignal.timeout 触发时同样会落在 AbortError if (error.name TimeoutError) { return { code: TIMEOUT, message: 请求超时 }; } return { code: UNKNOWN, message: error.message || 未知错误 }; }用 fetch 做底层的时候这个归一化函数是必需品。因为跨浏览器环境下你对 fetch 抛出的错误几乎不能做任何字符串匹配只能利用 error.name、error instanceof TypeError 这类稳定的属性去判断层级。5. 我的选型思路不迷信任何一个库5.1 什么情况无脑用 axios如果你维护的是一个中大型后台管理系统、中后台 CRUD 应用有统一的 token 刷新、统一的错误提示、大量的文件上传下载、团队里前端水平参差不齐那直接选 axios。原因不是 axios 比 fetch 高级而是它自带的拦截器、超时、错误对象结构、进度回调能帮你凝聚一套团队级的请求规范。这种场景下把时间花在业务上比花在自研请求封装上划算得多。老项目里已经在用 axios 且封装了一层 request 的更不要轻易迁移到 fetch。我接过一个项目原来的 axios 封装里带着十几个拦截器逻辑一旦迁移得自己去复刻请求队列、重放、401 刷新令牌、错误码映射工程量远超你想象。5.2 什么情况可以裸用 fetch项目很小很轻比如一个活动页、一个营销落地页只调三五个接口不需要复杂错误处理那直接 fetch 就行。浏览器原生能力不引入库代码简洁性能也没有差别。后端脚本、云端函数这种用 Node 跑的服务端环境优先用原生 fetch。Serverless 冷启动、Edge Runtime 对第三方库兼容性要求高原生 fetch 几乎是最稳的选择。Next.js App Router 的 Server Actions、Route Handlers、Vercel Edge 函数这些场景社区默认直接用 fetch。如果你在做浏览器扩展或者 Electron 主进程相关开发fetch 对运行环境的依赖更小很多时候也比 axios 更合适。5.3 一个折中方案fetch 做底包一个轻量 request如果你既不想引 axios又想要 axios 的便利可以自己写一个轻量封装。我认为一个够用的 fetch request 需要满足四件事统一 baseURL 拼接和超时控制支持请求/响应拦截器至少要有在请求前加 token、响应后统一处理错误的能力统一错误归一化把 Failed to fetch、AbortError、超时错误都转成自己项目的错误对象保留取消能力通过接受外部传入的 signal 做到可取消class HttpClient { constructor(baseURL, defaultOptions {}) { this.baseURL baseURL; this.defaultOptions defaultOptions; this.requestInterceptors []; this.responseInterceptors []; } useRequestInterceptor(fn) { this.requestInterceptors.push(fn); return this; } useResponseInterceptor(fn) { this.responseInterceptors.push(fn); return this; } async request(path, options {}) { let merged { ...this.defaultOptions, ...options, headers: { Content-Type: application/json, ...(this.defaultOptions.headers || {}), ...(options.headers || {}) } }; for (const fn of this.requestInterceptors) { merged (await fn(merged)) || merged; } try { const response await fetch(${this.baseURL}${path}, merged); let data null; const contentType response.headers.get(Content-Type) || ; if (contentType.includes(application/json)) { data await response.json(); } else { data await response.text(); } for (const fn of this.responseInterceptors) { const result await fn(response, data); if (result ! undefined) data result; } if (!response.ok) { throw new HttpClientError(response.status, data, response); } return data; } catch (error) { if (error instanceof HttpClientError) throw error; throw normalizeFetchError(error); } } get(path, options) { return this.request(path, { ...options, method: GET }); } post(path, body, options) { return this.request(path, { ...options, method: POST, body: JSON.stringify(body) }); } }这个封装大概 100 行左右覆盖了绝大多数项目需要的功能也保留了 fetch 的可组合性。但它有一个前提团队里要有人能把这块代码维护好并且把规范沉淀下来。如果做不到还是老老实实用 axios 更省心。5.4 给新手的几条实用建议第一先理解 response.body 的流式设计再谈比较。fetch 的一切 API 都围绕 ReadableStream 展开你把它当普通 JSON 对象用当然觉得天生残疾。第二不要用基准测试谁更快来选型。两者在 99% 的场景下性能几乎没有差异瓶颈在服务端、网络和序列化不在请求库。第三选一个方向学透。用 axios 的人把拦截器、错误对象、取消机制搞清楚用 fetch 的人把 ReadableStream、AbortController、stream 消费搞清楚。学透了之后你自然会发现两者背后的同构性很多纠结都不存在了。第四团队项目里必须统一。最怕的是这个人用 axios、那个人用 fetch最后请求层逻辑散落一地谁接手谁崩溃。我在实际项目里最常见的真实场面是一个团队因为某次技术分享就决定把 axios 换掉结果两个星期的工时都耗在了请求层的边界处理上。写代码这件事很多时候稳定的、被团队熟悉的工具价值远超某个 API 在纸面上的优势。fetch 和 axios 的对比最终结论不是谁更好而是你在确定的时间点、确定的环境下选一个能让你和团队长期省心的方案。
返回列表