前端网络请求生存指南:从XHR、Fetch到Axios的原理与选型

前端网络请求生存指南:从XHR、Fetch到Axios的原理与选型 1. 这不是技术演进史而是一份前端网络请求的“生存指南”你写过多少次axios.get(/api/user)又在控制台里见过多少次Failed to fetch或CORS error这些报错背后从来不是某一行代码写错了而是你对浏览器和服务器之间那条看不见的“数据通道”理解得不够深。我做前端开发十二年从 jQuery 时代手写$.ajax({ type: POST, url: /login, data: { u: a, p: b } })开始经历过 XHR 手动管理状态、Fetch API 初期兼容性踩坑、Axios 拦截器被滥用导致内存泄漏再到如今在 Next.js App Router 中用原生fetch做服务端数据获取——每一次技术切换表面是 API 变了本质是开发者对“如何可靠、可控、可维护地发起一次 HTTP 请求”的认知在升级。标题里说的“前世今生”不是怀旧而是复盘Ajax 不是某个库而是一种异步通信思想XHR 不是 Fetch 的前身而是浏览器最早暴露给 JS 的底层网络能力封装Fetch 不是 Axios 的替代品它更接近curl的语义而 Axios 是为业务场景定制的“HTTP 工具箱”。热搜词里反复出现的cors policy、failed to fetch、axios devserver 转发、nextjs 使用原生 fetch 封装拦截器好还是 axios 拦截器全指向一个现实问题当请求失败时你第一反应是查文档还是查自己对协议、浏览器机制、框架约束的理解盲区这篇文章不讲“谁更好”只讲“在什么场景下为什么必须用这个、不能用那个”。我会带你从最原始的XMLHttpRequest.open()开始一层层剥开为什么GET请求参数要手动拼接 URL 而POST要设置Content-Type为什么fetch默认不带 cookie 而axios默认带为什么 Vue 项目里devServer.proxy能解决跨域而生产环境必须后端配合为什么 Next.js App Router 强制要求fetch的cache和next配置直接影响 SSR 渲染结果。所有内容都来自真实项目现场——比如上周刚上线的 SaaS 后台因axios拦截器里没处理AbortController导致页面跳转后请求仍在后台执行拖慢了新页面加载再比如客户投诉 PDF 预览白屏排查发现是pdf.jsv2.16.105 内部调用fetch时未处理signal导致大文件下载超时后无法取消最终阻塞整个 Worker 线程。如果你正被Access to fetch at xxx from origin yyy has been blocked by CORS policy折磨或纠结于“该不该在 Next.js 里弃用 Axios”或面试官问你“fetch和XHR的核心区别到底是什么”那么这篇内容就是为你写的。它不教你怎么复制粘贴代码而是帮你建立一套判断逻辑看到报错能立刻定位到是协议层HTTP、运行时层浏览器/Node、框架层Vue/Next还是应用层业务逻辑的问题。接下来的内容每一节都对应一个真实战场。2. 核心设计思路为什么前端网络请求工具会不断迭代2.1 本质没变变的是“人”与“机器”的协作方式很多人误以为 Ajax → XHR → Fetch → Axios 是一条线性进化链仿佛旧技术被淘汰了。事实恰恰相反XHR 至今仍是浏览器唯一原生支持的底层网络接口Fetch 是基于 Promise 对 XHR 的语法糖封装Axios 是基于 XHR 或 Fetch 的上层业务适配器。它们共存是因为解决的问题维度不同XHR 解决“能不能发”提供open()、send()、onreadystatechange等原始方法让你能控制连接建立、发送、接收全过程但需手动处理状态码、超时、重试、错误分类。Fetch 解决“好不好写”用 Promise 替代回调用Response对象统一处理响应体用Headers对象管理头信息但默认不带 cookie、不自动解析 JSON、不支持请求取消需AbortController。Axios 解决“方不方便用”内置请求/响应拦截器、自动 JSON 序列化、超时重试、CSRF token 自动注入、取消重复请求等业务高频需求但引入额外包体积、隐藏了底层细节调试时容易迷失在拦截器链条中。这种分层不是偶然。我参与过三个大型政企系统迁移第一个用 jQuery.ajax第二个用原生 Fetch第三个用 Axios 封装。迁移动力从来不是“新技术更先进”而是团队能力与项目复杂度的匹配度变化。jQuery 时代前端工程师常兼做后端接口联调需要细粒度控制每个请求头Fetch 初期团队开始重视代码可读性async/await让异步逻辑更线性Axios 普及后业务模块增多需要统一处理登录态、错误弹窗、loading 状态拦截器成了刚需。所以选型逻辑很朴素当你的团队开始为“减少重复代码”付出比“理解原理”更高的成本时就该上 Axios当你发现拦截器里堆了 20 行逻辑却查不出 401 错误原因时就该回溯到 Fetch 甚至 XHR 查问题。2.2 浏览器能力演进倒逼 API 设计重构Fetch 的诞生直接源于浏览器厂商对 Web 平台标准化的推动。2015 年前XHR 存在严重缺陷状态管理混乱readyState有 0~4 五个值但4不代表成功需检查status200也不代表业务成功可能返回{ code: 500, msg: 系统繁忙 }二进制处理笨重读取图片需responseType arraybuffer再手动转Blob而 Fetch 直接支持response.blob()、response.arrayBuffer()流式处理缺失XHR 无法分块读取大文件Fetch 提供response.body.getReader()实现流式解析这对 PDF.js、音视频播放器至关重要。而 Axios 的流行则源于 Node.js 生态的成熟。早期前端发请求只能靠浏览器Node.js 出现后axios同时支持浏览器和 Node 环境让同构渲染如 Next.js成为可能。它的create()方法生成实例配合defaults设置 baseURL、timeout完美适配微前端架构——主应用统一配置子应用继承避免每个模块重复写https://api.xxx.com/v1。这背后是工程化思维的胜利不是 API 更好而是它让“配置即代码”落地了。2.3 框架约束重塑请求范式Vue 和 Next.js 对网络请求的影响常被低估。Vue 2 的vue-resource已淘汰Vue 3 官方推荐组合式 API fetch因为ref响应式系统天然适配async/await而 Next.js App Router 更激进它要求fetch必须在 Server Component 中调用且强制cache: no-store或revalidate配置否则会触发静态生成SSG而非服务端渲染SSR。这意味着你在useEffect里用axios获取用户数据在 Next.js 中是反模式——数据应在服务端获取并传入组件避免客户端水合hydration时的闪烁。我曾帮一家电商客户优化首页加载他们用 Vue 3 Pinia在setup()中await axios.get(/api/home)首屏白屏 2s。改成async setup()await fetch后Next.js 自动将请求提升到服务端首屏时间降至 300ms。这不是 Fetch 比 Axios 快而是框架利用 Fetch 的声明式特性cache、next做了编译时优化。所以选型必须看框架文档Vue CLI 项目用 Axios 无压力Vite Vue 3 可用 FetchNext.js App Router 必须用 Fetch而 Taro 多端框架则推荐taro.request——它底层根据平台自动选择 XHR 或 Fetch开发者无需关心。3. 核心细节解析从 XHR 到 Fetch每一步都在解决什么痛点3.1 XHR手把手教你“造轮子”的必经之路尽管现在很少直接写 XHR但理解它能让你一眼看穿所有高级封装的底层逻辑。下面这段代码是我当年在银行系统里处理文件上传的真实片段const xhr new XMLHttpRequest(); xhr.open(POST, /upload, true); xhr.setRequestHeader(X-Requested-With, XMLHttpRequest); xhr.upload.addEventListener(progress, (e) { const percent (e.loaded / e.total) * 100; console.log(上传进度: ${percent.toFixed(2)}%); }); xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200 xhr.status 300) { const res JSON.parse(xhr.responseText); console.log(上传成功:, res); } else { console.error(上传失败:, xhr.status, xhr.statusText); } } }; xhr.send(formData);关键点解析open(method, url, async)的async参数必须为true否则阻塞主线程已废弃但老系统仍存在setRequestHeader手动设置头Content-Type由send()的参数类型自动决定send(string)为text/plainsend(FormData)为multipart/form-datasend(JSON.stringify(obj))需手动设application/jsonupload对象监听上传进度download对象监听下载进度Fetch 无此能力需用ReadableStream手动实现onreadystatechange回调中readyState 4仅表示请求完成必须检查status才能确认 HTTP 成功这是新手最常踩的坑——status为 0 通常意味着跨域被拦截或网络断开。提示XHR 的responseType支持text、json、arraybuffer、blob、document。设为json时response直接是解析后的对象但 IE10 不支持需降级为JSON.parse(xhr.responseText)。3.2 FetchPromise 化带来的范式革命Fetch 的核心价值是用函数式编程思想重构请求流程。对比 XHR它把“发起请求”和“处理响应”彻底解耦// XHR 风格请求与响应处理混杂 xhr.send(data); xhr.onreadystatechange () { /* 处理逻辑 */ }; // Fetch 风格请求即返回 Promise链式处理 fetch(/api/user, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ id: 1 }) }) .then(res { if (!res.ok) throw new Error(HTTP error! status: ${res.status}); return res.json(); // 注意res.json() 本身也返回 Promise }) .then(data console.log(data)) .catch(err console.error(请求失败:, err));这里藏着三个关键设计res.ok判断代替status 200 status 300ok为true当且仅当status在 200-299语义更清晰res.json()、res.text()、res.blob()等方法返回 Promise避免手动JSON.parse()且自动处理编码如 UTF-8 BOMheaders用Headers对象管理支持append()、set()、get()比字符串拼接更安全。但 Fetch 的“简洁”是有代价的默认不带 cookie需显式设置credentials: include否则跨域请求无法携带 session没有超时控制需配合AbortController实现fetch(url, { signal: AbortSignal.timeout(5000) })是 Chrome 107 新增的便捷写法错误处理陷阱fetch只在网络错误如 DNS 失败、断网时 rejectHTTP 状态码 404、500 仍 resolve必须手动if (!res.ok) throw。注意AbortController是现代前端必备技能。我在一个实时监控大屏项目中用户切换仪表盘时需取消上一个图表的请求。用AbortController的abort()方法比在 Axios 拦截器里维护 cancelToken 数组更直观“创建 controller → 传入 signal → 切换时 controller.abort()”。3.3 Axios为业务场景而生的“瑞士军刀”Axios 的优势在于它把开发者从协议细节中解放出来专注业务逻辑。以下是一个典型的企业级配置const api axios.create({ baseURL: https://api.example.com/v1, timeout: 10000, headers: { X-App-Version: 2.3.1 } }); // 请求拦截器添加 token、日志 api.interceptors.request.use( config { const token localStorage.getItem(token); if (token) config.headers.Authorization Bearer ${token}; console.log([REQ] ${config.method} ${config.url}); return config; }, error Promise.reject(error) ); // 响应拦截器统一错误处理、自动刷新 token api.interceptors.response.use( response response.data, // 直接返回 data省去 .data async error { const originalRequest error.config; if (error.response?.status 401 !originalRequest._retry) { originalRequest._retry true; const newToken await refreshToken(); localStorage.setItem(token, newToken); originalRequest.headers.Authorization Bearer ${newToken}; return api(originalRequest); // 重试原请求 } return Promise.reject(error); } );这个配置解决了四大痛点请求地址统一管理baseURL避免硬编码超时与重试timeout防止请求挂起拦截器实现 401 刷新 token响应体标准化response.data直接返回业务数据无需res.data.data嵌套错误分类处理网络错误、HTTP 错误、业务错误如{ code: 1001, msg: 余额不足 }可分层捕获。但 Axios 的“便利”也带来风险内存泄漏拦截器中若引用了组件实例如this.$message.error()组件卸载后请求仍在执行导致报错过度封装axios.get(url, { params: obj })会自动序列化参数但obj { a: [1,2], b: null }时paramsSerializer默认行为是a[]1a[]2b而某些后端要求a1a2b需自定义序列化函数Tree-shaking 无效Axios 是类库而非模块Webpack 无法摇掉未用方法gzip 后约 14KB对性能敏感项目需权衡。4. 实操过程从零搭建一个可落地的请求方案4.1 场景还原一个需要兼顾 Vue 3 和 Next.js 的中后台系统假设你正在开发一个 SaaS 管理后台前端用 Vue 3SPA 模式同时需为 SEO 优化提供 Next.js 版本。API 由 Spring Boot 提供要求所有请求带Authorizationtoken登录态失效时自动跳转登录页文件上传需显示进度Next.js 页面需支持服务端数据获取错误统一展示 toast 提示。方案设计原则不追求“一套代码跑两端”而是“一套逻辑适配两端”。核心是抽象出request接口Vue 端用 Axios 实现Next.js 端用 Fetch 实现业务代码只调用request.get()。Vue 3 端实现基于 Axios// utils/request.js import axios from axios; const service axios.create({ baseURL: import.meta.env.VUE_APP_API_BASE_URL || /api, timeout: 10000 }); // 请求拦截器 service.interceptors.request.use( config { const token localStorage.getItem(token); if (token) config.headers.Authorization Bearer ${token}; // 添加 loading 状态配合全局 loading 组件 if (config.showLoading ! false) { window.dispatchEvent(new Event(request-start)); } return config; }, error Promise.reject(error) ); // 响应拦截器 service.interceptors.response.use( response { window.dispatchEvent(new Event(request-end)); // 假设后端统一返回 { code: 0, data: {}, msg: } if (response.data.code ! 0) { ElMessage.error(response.data.msg || 请求失败); if (response.data.code 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(new Error(response.data.msg)); } return response.data.data; }, error { window.dispatchEvent(new Event(request-end)); if (error.code ECONNABORTED) { ElMessage.error(请求超时请重试); } else if (error.response?.status 0) { ElMessage.error(网络连接失败请检查网络); } else { ElMessage.error(error.response?.data?.msg || 系统异常); } return Promise.reject(error); } ); export default service;关键实操心得window.dispatchEvent触发自定义事件由全局loading组件监听避免在每个 API 调用处写loading.value trueshowLoading: false用于导出 Excel 等无需 loading 的场景通过配置项开关response.data.code ! 0判断业务错误比status更贴近实际需求router.push(/login)跳转前需localStorage.removeItem(token)否则下次进入仍尝试带无效 token。Next.js App Router 端实现基于 Fetch// app/lib/request.ts use server; // Next.js Server Action 中的 fetch 需指定 cache 和 next export async function requestT( url: string, options: RequestInit {} ): PromiseT { const token cookies().get(token)?.value; const headers new Headers(options.headers || {}); if (token) headers.set(Authorization, Bearer ${token}); try { const res await fetch(${process.env.NEXT_PUBLIC_API_BASE_URL}${url}, { ...options, headers, cache: no-store, // 禁用缓存确保实时数据 next: { revalidate: 0 } // 同上强制不缓存 }); if (!res.ok) { const errorData await res.json(); throw new Error(errorData.msg || HTTP ${res.status}); } const data await res.json(); // 同样假设后端返回 { code: 0, data: {}, msg: } if (data.code ! 0) { throw new Error(data.msg || 业务错误); } return data.data as T; } catch (error) { // 服务端错误需转换为客户端可识别格式 if (error instanceof Error) { throw new Error([Server] ${error.message}); } throw new Error(网络请求失败); } } // 客户端组件中使用需 use client use client; import { request } from /app/lib/request; export default function UserList() { const [users, setUsers] useState([]); useEffect(() { request(/users).then(setUsers).catch(console.error); }, []); return div{/* 渲染列表 */}/div; }关键实操心得use server标识 Server Actioncookies()只能在服务端调用cache: no-store和next: { revalidate: 0 }必须同时设置否则 Next.js 可能缓存响应request函数返回PromiseTTypeScript 类型推导让const users await requestUser[](/users)有完整类型提示客户端组件中调用request时需注意useEffect的依赖数组避免无限请求。4.2 跨域问题实战从开发到生产的全链路解决方案Access to fetch at xxx from origin yyy has been blocked by CORS policy是前端最常见报错但根源不在前端。我总结出三类场景及解法场景原因Vue 开发环境解法Next.js 生产环境解法后端必须配置开发时 API 与前端域名不同如localhost:3000调localhost:8080浏览器同源策略拦截Vue CLIvue.config.js中devServer.proxyVitevite.config.ts中server.proxyNext.jsnext.config.js中rewrites代理到本地 API后端Access-Control-Allow-Origin设为*或具体域名生产环境前后端分离部署如app.example.com调api.example.com同源策略生效且credentials: include时Allow-Origin不能为*无解必须后端配置同上Access-Control-Allow-Origin设为https://app.example.comAccess-Control-Allow-Credentials: true第三方 API如微信支付回调后端无法修改 CORS 头前端无法直连需走代理Next.js Server Action 中调用绕过浏览器 CORS无解需后端中转Vue 开发代理配置实操Vite 项目中vite.config.tsexport default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, // 后端地址 changeOrigin: true, // 修改 Origin 头 rewrite: (path) path.replace(/^\/api/, ) // 去掉 /api 前缀 } } } });这样fetch(/api/users)实际请求http://localhost:8080/users浏览器认为是同源。Next.js 生产代理配置实操next.config.jsmodule.exports { async rewrites() { return [ { source: /api/:path*, destination: https://api.example.com/:path* } ]; } };访问/api/users时Next.js Server 自动转发到https://api.example.com/usersCORS 由 Next.js 服务端发出请求不受浏览器限制。提示changeOrigin: true在 Vite 中是必需的否则后端收到的Origin头仍是localhost:3000导致Allow-Origin匹配失败。很多团队卡在这里一周只因漏了这一行。4.3 文件上传与进度监控XHR 仍是不可替代的方案尽管 Fetch 支持body: FormData但上传进度监控必须用 XHR。原因在于 Fetch 的ReadableStream无法获取上传字节数而 XHR 的upload.onprogress提供e.loaded和e.total。以下是 Vue 3 中的实操封装script setup import { ref } from vue; const fileInput ref(null); const uploadProgress ref(0); const handleUpload async () { const file fileInput.value.files[0]; if (!file) return; const xhr new XMLHttpRequest(); xhr.open(POST, /api/upload); xhr.upload.onprogress (e) { if (e.lengthComputable) { uploadProgress.value Math.round((e.loaded / e.total) * 100); } }; xhr.onload () { if (xhr.status 200) { const res JSON.parse(xhr.responseText); console.log(上传成功:, res); uploadProgress.value 0; } }; xhr.onerror () { console.error(上传失败); uploadProgress.value 0; }; const formData new FormData(); formData.append(file, file); xhr.send(formData); }; /script template input typefile reffileInput changehandleUpload / div v-ifuploadProgress 0 上传进度: {{ uploadProgress }}% progress :valueuploadProgress max100/progress /div /template关键点xhr.upload.onprogress是唯一能实时获取上传进度的 APIe.lengthComputable为false时如 chunked 编码e.total为 0此时无法计算百分比xhr.onload在请求完成时触发xhr.status为 HTTP 状态码xhr.responseText为响应体。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “Failed to fetch” 的 7 种真实原因及定位方法Failed to fetch是 Fetch 的通用错误但背后原因千差万别。我整理了线上项目中最常遇到的 7 种情况附带快速定位步骤错误现象根本原因定位方法解决方案Failed to fetch且控制台无其他日志网络完全中断如 WiFi 关闭检查浏览器地址栏是否能打开其他网站navigator.onLine返回false提示用户检查网络禁用请求按钮Failed to fetch伴随TypeError: Failed to fetch跨域请求被拦截CORS查看 Network 面板请求状态为(blocked)Preview 为空后端配置Access-Control-Allow-OriginFailed to fetch且请求未出现在 Network 面板AbortController.abort()被调用在代码中搜索abort()检查路由切换、组件卸载逻辑在useEffect清理函数中调用abort()Failed to fetch且fetch调用后立即报错URL 协议错误如http://调用https://API检查fetch的 URL 字符串确认协议一致统一使用https或配置反向代理Failed to fetch且response.body为空后端返回空响应体如 204 No ContentNetwork 面板查看 Response Headers确认Content-Length: 0fetch后检查res.status ! 204再调用res.json()Failed to fetch且signal超时AbortSignal.timeout()时间过短检查timeout参数对比后端平均响应时间将 timeout 设为后端 P95 响应时间的 1.5 倍Failed to fetch伴随net::ERR_CONNECTION_REFUSED后端服务未启动或端口错误ping后端域名telnet host port测试端口连通性检查后端进程、防火墙、Docker 网络配置实操案例某次上线后大量用户反馈“保存失败”错误日志全是Failed to fetch。我首先在 Network 面板过滤fetch请求发现所有失败请求的 URL 都是http://api.xxx.com/save而生产环境应为https。追查代码发现环境变量VUE_APP_API_BASE_URL在.env.production中误写为http://修复后问题消失。教训环境变量必须用console.log打印验证不能只信文档。5.2 Axios 拦截器的三大致命陷阱Axios 拦截器是双刃剑用得好提升开发效率用不好引发线上事故。我踩过的坑陷阱一拦截器中修改config导致后续请求失效// ❌ 错误直接修改 config.headers service.interceptors.request.use(config { config.headers[X-Trace-ID] uuid(); // 每次请求覆盖 headers 对象 return config; }); // 后续请求中config.headers 是同一个引用可能被其他拦截器污染✅ 正确做法service.interceptors.request.use(config { const headers { ...config.headers }; // 创建新对象 headers[X-Trace-ID] uuid(); return { ...config, headers }; });陷阱二响应拦截器中未处理undefined响应// ❌ 错误假设所有响应都有 data service.interceptors.response.use(res res.data); // 当后端返回 204 No Content 时res.data 为 undefined导致后续 .then() 报错✅ 正确做法service.interceptors.response.use( res { if (res.status 204) return {}; // 显式返回空对象 return res.data; } );陷阱三拦截器中引用组件实例造成内存泄漏// ❌ 错误在拦截器中使用 this.$message service.interceptors.response.use( res res, error { this.$message.error(请求失败); // this 指向 Vue 实例组件卸载后 this 仍存在 } );✅ 正确做法// 在入口文件中初始化 message 实例 import { ElMessage } from element-plus; const message ElMessage; service.interceptors.response.use( res res, error { message.error(请求失败); // 使用独立实例不依赖组件 } );5.3 Next.js 中 Fetch 的cache和revalidate配置详解Next.js App Router 对fetch的缓存控制极其严格配置错误会导致数据陈旧或 SSR 失败。以下是真实项目中的配置策略场景cache值next.revalidate说明用户个人信息需实时no-store无禁用所有缓存每次请求都发往后端商品列表可容忍 30s 陈旧default30默认缓存30 秒后重新验证静态页面如关于我们force-cache无强制使用缓存即使页面重新加载管理后台仪表盘需实时但允许降级no-cache无不缓存但允许 CDN 缓存需后端配合实操验证在 Next.js 中cache: no-store会禁用 App Router 的所有缓存包括 ISR增量静态再生。我曾为一个实时股票行情页配置cache: default结果用户看到的是 1 分钟前的数据。改为cache: no-store后首屏加载变慢但数据实时性达标。平衡点在于对实时性要求高的页面宁可牺牲性能也要保证正确性对新闻列表等场景用revalidate: 60可显著降低后端压力。5.4 编码问题终极解决方案ajax请求设置编码格式的真相热搜词ajax请求设置编码格式暴露了一个长期被误解的问题。实际上GET 请求的编码由浏览器自动处理URL 中的中文会自动 encodeURIComponent后端用URLDecoder.decode()解码POST 请求的编码取决于Content-Typeapplication/x-www-form-urlencoded浏览器自动 encode后端用request.getParameter()application/jsonJSON 字符串本身是 UTF-8无需额外设置multipart/form-data文件名等字段需在FormData.append()时手动 encode。真实案例某政府项目中用户上传含中文名的文件后端收到乱码。排查发现前端用FormData.append(file, file, 测试文件.xlsx)而 Chrome 对文件名自动 encode 为 UTF-8但 IE11 用 GBK。解决方案是// 兼容 IE11手动 encode 文件名 const fileName encodeURIComponent(file.name); formData.append(file, file, fileName);后端用new String(fileName.getBytes(ISO-8859-1), UTF-8)解码。最后分享一个小技巧在 Vue 3 中input typefile的files[0].name在不同浏览器中编码不一致建议统一用encodeURIComponent处理再由后端按 UTF-8 解码。这比纠结“怎么设置 Ajax 编码”更治本。