前端网络请求全指南:从fetch到axios,搞定跨域与拦截器
发布时间:2026/10/13 15:00:58 锦皓数字建站

前端开发做到第三篇终于轮到“网络请求”这个绕不开的核心主题了。很多同学写静态页面很溜一到对接真实接口就发怵数据从哪来怎么发出去报错了怎么看这些问题我在带人时天天遇到。不管你是写原生JavaScript、Vue还是React只要页面里要展示动态数据——用户列表、商品详情、订单状态——网络请求就是你的必经之路。这篇内容围绕前端最主流的几种请求方案展开XMLHttpRequest、fetch、axios。我会结合实际开发中的踩坑经验把请求参数、响应处理、跨域、拦截器、错误处理这些高频细节拆开讲一遍。适合刚学完JavaScript基础、准备写真实项目的同学也适合用了很久请求库但没搞懂底层原理的开发者。我对网络请求有个很直观的比喻它就像餐厅点菜。你报菜名请求方法告诉服务员口味偏好请求头后厨做菜服务端处理最后菜端上桌响应。中间任何一环出错你都要知道问题出在哪个环节。1. 网络请求的前世今生从表单提交到fetch1.1 为什么页面必须依赖网络请求网页从“静态展示”到“动态交互”的关键转折点就是网络请求。早期网页是纯HTML所有内容都在服务器端写死用户每次点击都要刷新整个页面。后来Ajax出现页面终于可以不刷新地从服务器拿数据“前端网络请求”这个概念才算真正立住了。对于刚入门的朋友可以这样理解你写了一个用户列表页面但页面上的用户数据不可能凭空变出来它存在服务器的数据库里。前端要做的事就是通过HTTP协议向服务器要数据拿到之后渲染到页面上。这个“要数据”的动作就是网络请求。实际开发中除了拿数据网络请求还遍布在各种场景里用户登录把账号密码发给服务端换回一个token提交表单把用户填写的内容POST到一个接口保存到数据库文件上传把图片、文档以multipart/form-data格式传到服务器轮询接口定时请求某个接口获取最新状态比如在线人数、任务进度前端网络请求的本质就是一次完整的HTTP事务。它包含请求行方法加路径加协议版本、请求头headers、请求体body以及服务端返回的状态行、响应头和响应体。把这些概念搞清楚后面所有问题都能迎刃而解。1.2 三代请求方式对比XHR、fetch、axios前端发请求的方式我习惯分成三代来看。第一代是XMLHttpRequest也就是常说的XHR。2000年左右出来的老技术用起来相当啰嗦要写回调函数要自己处理readyState状态变化代码一多就很容易嵌套成“回调地狱”。现在直接手写XHR的团队不多了但很多老项目、部分第三方库底层还在用它所以面试和排查问题时还是得认识它。第二代是fetch。现代浏览器原生提供基于Promise设计语法比XHR简洁太多。最大的优势是“浏览器内置、不用引库”一个简单的GET请求三行代码就搞定。但它也有明显的坑默认不带cookie超时和取消请求支持得很原始服务器返回404或500时它不会把请求判定为失败。第三代是axios。它不是官方标准而是一个第三方HTTP客户端库浏览器底层封装的还是XHR同时支持Node环境。它流行起来的原因很现实拦截器、取消请求、超时设置、统一错误处理这些能力开箱即用团队协作时能快速统一请求风格。三种方式放在一起对比优缺点就非常清晰了方案底层Promise拦截器超时/取消适用场景XMLHttpRequest浏览器原生不支持需要手动封装原生支持但写法繁琐老项目兼容、底层库fetch浏览器原生原生支持需要手动封装需配合AbortController轻量请求、新项目axios浏览器XHR/Node支持内置内置中大型项目、团队协作这里想提醒一句不要为了“流行”而用axios也不要为了“原生”而拒绝fetch。方案选型的核心是看项目需求后面我会专门展开聊这个话题。2. 手写第一个fetch请求参数、响应与跨域2.1 fetch完整参数拆解method、headers、bodyfetch的基本用法很简单新手照着写就能通const res await fetch(https://api.example.com/users); const data await res.json(); console.log(data);这段代码做了两件事向指定地址发一个GET请求然后把响应体解析成JSON。对于初学者来说这是最直观的入门写法。但fetch的完整签名远不止两个参数完整的调用形态是这样fetch(url, { method: GET, headers: { Content-Type: application/json }, body: null, credentials: same-origin });参数里最常用的五个我逐个说method请求方法最常用GET、POST、PUT、DELETE。GET用来拿数据POST用来提交数据PUT用来整体更新DELETE用来删除。headers请求头对象用来设置Content-Type、Authorization等元信息。body请求体。GET请求通常是nullPOST请求放JSON字符串或FormData对象。credentials控制请求是否携带cookie取值有omit、same-origin、include。signal用来和AbortController配合实现取消请求。有个非常容易踩的坑POST请求如果传的是JavaScript对象必须先通过JSON.stringify序列化成字符串同时header里声明Content-Type为application/json。很多人直接把对象塞进body结果后端收到的是[object Object]还反过来怪接口有bug。这个问题我在代码评审里见过太多次。2.2 响应解析的四种方法与关键坑fetch返回的res并不是最终的响应数据而是一个Response对象。你必须调用对应的解析方法才能拿到真正的内容const json await res.json(); // 解析JSON const text await res.text(); // 解析纯文本 const blob await res.blob(); // 解析二进制数据图片、文件 const formData await res.formData(); // 解析表单数据这几种解析方法有一个共同特性都返回Promise而且同一个Response对象只能被解析一次。如果你先调了res.json()再调res.text()后面会直接报错提示body stream already read。实际开发中有的同学想在catch分支里再读一次响应体拿错误详情就很容易碰到这个错误。另一个关键细节很多人不知道fetch只有在网络层真正出错时才会reject。比如断网、DNS解析失败、连接被重置。如果服务器返回404、500这种HTTP错误状态码fetch不会走进catch分支而是正常返回一个Response对象只是res.ok为false。也就是说你必须手动检查if (!res.ok) { throw new Error(HTTP error! status: ${res.status}); }否则你会在页面上收获一个合法却不合理的“成功请求”数据却没有如期渲染。2.3 请求头、CORS跨域与credentials配置请求头是前后端沟通的“元信息”最常见的几个包括Content-Type告诉服务器你发送的body是什么格式application/json、application/x-www-form-urlencoded、multipart/form-data是三大主力。Authorization存放身份凭证最常用格式是Bearer加一个token字符串。Accept告诉服务器你希望返回什么格式。X-Requested-With有一部分后端用它判断当前请求是不是Ajax请求。跨域方面浏览器出于安全考虑默认拦截“不同源”的请求。所谓同源是指协议、域名、端口三者完全一致只要有一个不一样就算跨域。跨域报错长这样看着吓人但信息量很大Access to fetch at https://api.example.com/data from origin http://localhost:3000 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.解决CORS前端能做的其实有限。开发环境下最常用的是配置代理比如Vite的server.proxy或者webpack的devServer.proxy生产环境则由后端在响应头里加Access-Control-Allow-Origin。前端代码层面没有“绕过”浏览器同源策略的正规方法谁要是跟你说他能绕过去多半是用了危险的做法千万别信。credentials这个参数也要重点记住。fetch默认不带cookie如果你依赖cookie做登录态必须显式设置credentials: include同时后端响应头还得配上Access-Control-Allow-Credentials: true。很多同学登录成功后下一次请求依然是“未登录”状态多半就是这里漏掉了。3. 前端请求层封装拦截器、超时与错误处理3.1 自己封装一个request基础函数项目里如果每个页面都直接写fetch代码会散得没法看。日志、token注入、错误处理到处重复后端接口一旦调整前端要改的地方能有几十处。我的习惯是封装一个request函数所有请求都走同一个出入口。一个最小可用的封装长这样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.ok) { throw new Error(HTTP error ${response.status}); } return response.json(); }封装的价值在几方面token统一注入不用每个请求手动带Content-Type默认设置好避免遗漏HTTP错误统一抛出调用方可以用try/catch兜底后续加日志、埋点、Loading控制都只需要改这一处我见过有人觉得这样多了一层、不够“透明”宁愿在各个页面里手写fetch。短期看是省了几行代码长期看每加一个功能就要动几十个文件的感觉谁写谁知道。3.2 拦截器机制与AbortController取消请求拦截器的概念很多人是从axios认识的。它的本质是“在请求发出去之前、响应回来之后插入一段统一逻辑”。比如请求前统一加token响应后统一处理401跳转登录页。axios的拦截器写法非常经典axios.interceptors.request.use(config { config.headers.Authorization Bearer ${token}; return config; }); axios.interceptors.response.use( response response, error { if (error.response error.response.status 401) { window.location.href /login; } return Promise.reject(error); } );如果用的是原生fetch你需要自己模拟拦截器思路就是在request内部做类似操作。虽然没有axios那么优雅但维护性好照样能统一逻辑。超时和取消请求是fetch最让人头疼的部分。原生fetch没有超时机制取消需要配合AbortControllerconst controller new AbortController(); setTimeout(() controller.abort(), 5000); try { const res await fetch(https://api.example.com/slow, { signal: controller.signal }); } catch (err) { if (err.name AbortError) { console.log(请求超时或已取消); } }AbortController不仅fetch能用axios的新版本也支持传signal参数。我建议团队统一约定所有请求都设超时时间。没有超时的请求一旦接口卡住用户体验就是loading转半小时这种问题在线上特别常见。3.3 错误分类网络错误、HTTP错误与业务错误网络请求的错误我习惯分成四类每一类的处理方式都不一样网络层错误断网、连接被重置。用户不需要知道技术细节提示“网络连接异常”就行。HTTP错误4xx是客户端问题比如404、4035xx是服务器问题比如500、502。需要根据状态码给不同提示。业务错误接口返回200但响应体里的code字段不是0比如“密码错误”“库存不足”。这种错误只能在业务层判断。解析错误服务端返回了200但响应体不是预期的JSON格式导致res.json()直接抛异常。处理方式上我的建议是“错误信息向上抛用户提示向下沉”。底层request只负责把错误规范化UI层再决定提示什么文案。别把“请求失败”四个字写死在底层提交订单失败和加载列表失败是两种完全不同的用户场景提示文案和后续动作都不一样。封装的错误对象可以统一成这种结构{ code: NETWORK_ERROR, message: 网络连接异常请稍后重试, status: 0, raw: Error }上层拿到之后按code精确处理后端接口的报错信息也能在UI层展示出来而不是只留下一个干巴巴的“Error”。4. 网络请求常见问题排查与避坑记录4.1 CORS跨域报错怎么看、怎么解跨域是我见过新手提问最多的问题没有之一。报错信息里通常藏着三个关键信息请求是从哪个origin发起的、被什么策略拦截、响应头里缺少什么字段。第一步先分清是谁的问题打开浏览器的Network面板如果请求已经发出去了并且响应头里确实没有Access-Control-Allow-Origin那大概率是后端没配置CORS。前端再折腾也没用直接去和后端同事对需求。如果响应头里有这个字段但仍然报错可能是允许的域名范围不对或者credentials配置有冲突。开发环境还经常遇到一种情况明明后端配置了CORS但还是被拦截。这时候大概率是请求带了自定义header比如Authorization触发了浏览器的预检请求OPTIONS。后端如果没处理OPTIONS后续请求就会失败。你可以在Network面板里看有没有一条OPTIONS记录如果有就让后端对OPTIONS请求做合法响应。跨域不是前端代码能“绕过”的东西它存在的基础是浏览器安全模型。正确的态度是开发环境用代理生产环境找后端配响应头。谁要是试图用奇怪的方式忽略安全策略项目上线时爆发的问题绝对够喝一壶。4.2 请求成功但页面没数据的排查步骤“接口明明返回200页面就是没数据”这是我被问烂了的一句话。实际上原因通常就那么几种按顺序排查很快就能定位。第一种接口返回200res.ok也是true但res.json()抛错。这种情况往往是后端返回了空字符串或者返回的是HTML错误页。先别急着解析JSON在Network面板里看响应体的原始内容如果是空串前端代码要做好兼容。第二种请求发出去了但Network面板里看到的URL和你代码里写的不一样。这可能是发生了重定向也可能是BASE_URL拼接出了双斜杠或者前缀丢失。重点检查相对路径和绝对路径的拼接逻辑尤其是环境变量里BASE_URL和管理后台接口地址的组合。第三种拿到的数据是空数组或null。这不一定是请求的问题很可能是过滤条件没传对或者分页参数出了问题。先看Network面板里的请求参数具体是什么再和后端联调确认。排查这类问题的标准动作我总结成三个字看Network。不要凭直觉猜不要靠console.log一个大对象瞎翻。打开开发者工具选中对应请求依次看请求URL、请求头、请求参数、响应体、响应状态码。九成的问题在这一步就能定位。4.3 竞态、重复提交与并发控制前端网络请求里有一种隐蔽问题叫“竞态条件”就是多个请求发出后先发的不一定先返回。典型场景是搜索框用户快速输入关键词第一次请求的结果可能在第二次请求之后才返回导致页面显示了旧数据。解决竞态的核心思路是“只认最后一次”。可以用请求序号来做let requestId 0; async function search(keyword) { const currentId requestId; const res await fetch(/api/search?q${keyword}); if (currentId requestId) { render(res); } }这样旧请求即使返回了因为编号已经不是最新的也不会被渲染到页面上。重复提交也很常见。用户快速双击“提交订单”按钮结果创建了两笔订单这种事故我见过不止一次。处理办法是提交后立刻禁用按钮或者用一个isSubmitting标记拦截。更稳妥的方式是在request层做“相同请求去重”短时间内相同URL和相同参数的请求只发第一个后面的直接复用第一个的Promise。这些细节看着不起眼但线上事故往往就是它们引起的。竞态问题、重复下单、并发超载都藏在几段看起来没问题的代码里。5. 请求方案选型与我的实操体会5.1 fetch优先还是axios优先两种选型逻辑我自己的选型标准很简单跟团队规模、项目阶段强相关新项目、团队愿意写封装逻辑、没有老代码包袱优先用fetch。浏览器原生、零依赖、维护成本低而且它的底层机制能逼着你真正理解HTTP请求。中大型项目、后端接口不规范、需要大量拦截器和统一错误处理优先用axios。它生态成熟团队上手成本低不用自己从头造轮子。老项目维护保持原有方案不要为了技术兴奋随意重写请求层。这个教训我见得太多网络请求方案一换涉及面极广特别容易引发线上回归。如果你问我个人偏好我其实更偏向fetch加自封装。因为fetch给开发者留下的控制空间更大封装得越深你对项目请求流的理解就越扎实。但我也完全理解团队选择axios的理由工具终究是为人服务的别被“技术洁癖”绑架。5.2 容易被忽略的五个请求细节最后分享几个我这些年实打实踩过的坑每一个都不起眼每一个都坑过人第一POST请求的Content-Type必须和body格式匹配。JSON用application/json普通表单用application/x-www-form-urlencoded文件上传用multipart/form-data。不要所有请求都统一用application/json文件传不了后端也解析不了。第二请求日志一定要留。哪怕只是console.info也能让你在排查问题时少走很多弯路。正规项目建议把请求URL、参数、状态码、耗时都记录下来。线上环境出了问题如果没有日志等于闭着眼睛查案。第三所有对外请求都要设置合理超时。后端再稳定也扛不住网络抖动和异常进程。一个没有超时的请求很可能让整个页面loading状态持续很久用户只能干等。第四不要在前端代码里硬编码接口地址。统一维护一份api配置开发、测试、生产环境切换时只改一处。这一点在多人协作的项目里尤其重要不然每个人手里一份地址联调时鸡飞狗跳。第五请求返回的数据结构最好和后端约定统一格式。我比较推荐code/message/data结构前端封装时统一解包业务代码里就不用到处写res.data.data这种取数逻辑出错的概率也会小很多。我个人的实操体会是网络请求这块理解HTTP本身比背库函数重要得多。你把请求头、响应头、状态码、跨域逻辑搞懂了用fetch还是axios只是换写法的问题。我建议你本地随便起一个接口亲手发几个请求把断网、404、500、跨域这些错误全部触发一遍打开控制台认真看报错长什么样再决定要不要引入重型封装。踩过一轮坑的人和只看文档的人区别一眼就能看出来。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。