news 2026/10/13 15:00:56

前端网络请求全指南:从fetch到axios,搞定跨域与拦截器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端网络请求全指南:从fetch到axios,搞定跨域与拦截器

前端开发做到第三篇,终于轮到“网络请求”这个绕不开的核心主题了。很多同学写静态页面很溜,一到对接真实接口就发怵:数据从哪来?怎么发出去?报错了怎么看?这些问题我在带人时天天遇到。不管你是写原生JavaScript、Vue还是React,只要页面里要展示动态数据——用户列表、商品详情、订单状态——网络请求就是你的必经之路。

这篇内容围绕前端最主流的几种请求方案展开:XMLHttpRequest、fetch、axios。我会结合实际开发中的踩坑经验,把请求参数、响应处理、跨域、拦截器、错误处理这些高频细节拆开讲一遍。适合刚学完JavaScript基础、准备写真实项目的同学,也适合用了很久请求库但没搞懂底层原理的开发者。

我对网络请求有个很直观的比喻:它就像餐厅点菜。你报菜名(请求方法),告诉服务员口味偏好(请求头),后厨做菜(服务端处理),最后菜端上桌(响应)。中间任何一环出错,你都要知道问题出在哪个环节。

1. 网络请求的前世今生:从表单提交到fetch

1.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、body

fetch的基本用法很简单,新手照着写就能通:

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请求通常是null,POST请求放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没有超时机制,取消需要配合AbortController:

const 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、403;5xx是服务器问题,比如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,页面就是没数据”,这是我被问烂了的一句话。实际上原因通常就那么几种,按顺序排查很快就能定位。

第一种:接口返回200,res.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、跨域这些错误全部触发一遍,打开控制台认真看报错长什么样,再决定要不要引入重型封装。踩过一轮坑的人,和只看文档的人,区别一眼就能看出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 14:59:14

二手车价格预测EDA实战:从数据探索到特征工程全流程

1. 赛题背景与EDA的核心价值定位二手车交易价格预测,本质是一个典型的回归类结构化数据挖掘任务。给定一辆车的品牌、行驶里程、注册时间、变速箱类型、燃油类型、车身类型、功率、地区编码等字段,要求预测其交易价格。这类赛题在数据挖掘学习路径中非常…

作者头像 李华
网站建设 2026/10/13 14:55:52

SpringBoot构建高并发火车票系统的核心技术解析

1. 项目概述火车票订票系统是典型的在线票务服务平台,采用SpringBoot框架构建后端服务,Java作为主要开发语言。这个系统需要处理高并发访问、保证数据一致性,同时提供友好的用户交互体验。作为交通出行领域的核心应用,这类系统需要…

作者头像 李华
网站建设 2026/10/13 14:54:45

Flink Checkpoint原理与生产调参实战:从Barrier对齐到健康度评估

1. 为什么说Checkpoint不是“定时快照”,而是Flink容错的神经中枢?在某次线上数据处理任务突发OOM导致整个作业重启后,我盯着监控面板上那条断崖式下跌的吞吐量曲线,第一次真正意识到:Checkpoint机制根本不是教科书里轻…

作者头像 李华
网站建设 2026/10/13 14:53:31

挡风被生产线设备全解析:从复合绗缝到裁切包边

挡风被,这个在寒冷季节里为我们提供温暖的小发明,背后其实藏着一条比多数人想象中复杂得多的产业链。市面上几十块钱到一百多块钱的挡风被,在电动车把上一搭、绳子一系,看起来简单;但如果进过车间,看到从卷…

作者头像 李华