资讯动态

从XHR到Axios:前端Ajax请求与CORS跨域实战指南

发布时间:2026/10/22 14:51:50 来源:尧图企业网站定制
1. 从一次接口联调失败说起为什么前端绕不开Ajax刚入行那会儿我接手了一个后台管理系统的前端页面需求很简单点击按钮从服务端拉取一张用户列表渲染到表格里。我心想这有什么难的三下五除二写了个fetch请求本地打开 HTML 文件一测控制台直接飘红——CORS policy: No Access-Control-Allow-Origin header is present。那一刻我才意识到Ajax 这件事远不是发个请求拿数据这么简单。后来带过几个新人发现大家踩的坑惊人地相似有人分不清XMLHttpRequest和fetch的区别有人以为 jQuery 的$.ajax和 Axios 是同一层的东西更多人对跨域的理解停留在加个Access-Control-Allow-Origin: *就完事结果一遇到带 Cookie 的请求就彻底懵掉。所以这篇内容我想把 Ajax 这条链路从头到尾捋一遍——从最原始的原生XMLHttpRequest到 jQuery 封装再到如今主流的 Axios最后重点拆解 CORS 跨域这个最容易翻车的环节。这篇适合谁看如果你是刚接触前后端交互的新手它能帮你建立完整的请求链路认知如果你已经用过 Axios 但说不清它底层干了什么它能补上你知识体系里的断层如果你正在被跨域问题折磨第 4 章和第 5 章的排查思路可以直接拿去用。我不会只给你 API 列表而是把每个选择背后的为什么讲透让你下次遇到问题时能自己判断而不是到处搜跨域怎么解决。2. 原生XMLHttpRequest所有Ajax的起点2.1 一个完整的XHR请求到底经历了什么很多人用XMLHttpRequest都是复制粘贴一段模板代码但如果你不知道readyState的五个状态分别代表什么调试时就会像盲人摸象。我先把一个标准请求拆开讲。const xhr new XMLHttpRequest(); xhr.open(POST, /api/user/list, true); xhr.setRequestHeader(Content-Type, application/json); xhr.onreadystatechange function () { if (xhr.readyState 4) { if (xhr.status 200 xhr.status 300) { console.log(JSON.parse(xhr.responseText)); } else { console.error(请求失败, xhr.status); } } }; xhr.send(JSON.stringify({ page: 1, size: 20 }));这段代码里readyState会依次经历0未初始化、1已调用 open、2已发送 send收到响应头、3正在接收响应体、4完成。我见过有新人把判断写在readyState 3里结果拿到的是半截数据解析 JSON 直接报错。记住只有readyState 4才代表整个响应接收完毕。open的第三个参数true表示异步这个几乎永远是true。如果写成false浏览器会警告主线程被阻塞页面直接卡死现代浏览器甚至已经在部分场景下禁止同步 XHR。所以别图省事写同步请求。2.2 为什么原生XHR现在还在用你可能会问都 2024 年了为什么还要学XMLHttpRequest直接用fetch或 Axios 不香吗原因有几个都是实战中绕不开的。第一上传进度。fetch至今没有原生的上传进度事件而 XHR 有xhr.upload.onprogress。如果你要做文件上传的进度条XHR 是最直接的选择。xhr.upload.onprogress function (e) { if (e.lengthComputable) { const percent (e.loaded / e.total * 100).toFixed(2); console.log(上传进度${percent}%); } };第二兼容性兜底。虽然现在主流浏览器都支持fetch但一些老旧的嵌入式 WebView 或者特定环境里XHR 仍然是唯一可靠的选择。我做过一个工业设备的管理界面运行在某个定制浏览器内核上fetch直接不可用最后全靠 XHR 撑起来。第三理解底层。Axios 在浏览器端的底层实现就是 XHR在 Node 端是http模块。你理解了 XHR 的readyState、status、timeout、abort这些机制再看 Axios 的源码和配置项会有一种原来如此的通透感。2.3 原生XHR的几个实战坑点坑点一超时设置必须在open之后、send之前。顺序错了不生效而且超时触发的是ontimeout事件不是onerror很多人只监听onerror结果超时了没有任何提示。xhr.open(GET, /api/data, true); xhr.timeout 5000; xhr.ontimeout function () { console.error(请求超时); }; xhr.onerror function () { console.error(网络错误); }; xhr.send();坑点二setRequestHeader必须在open之后调用。在open之前调用会直接抛异常。这个顺序问题我至少见过三个人踩过。坑点三跨域时withCredentials的坑。如果你需要携带 Cookie必须设置xhr.withCredentials true同时服务端的Access-Control-Allow-Origin不能是*必须是具体域名且要带上Access-Control-Allow-Credentials: true。这个组合缺一不可后面第 5 章会详细展开。3. jQuery的$.ajax一个时代的封装范式3.1 $.ajax的配置项设计为什么影响深远虽然现在新项目基本不用 jQuery 了但$.ajax的配置项设计思路几乎被后来的所有请求库继承。你去看 Axios 的配置url、method、headers、data、timeout、beforeSend对应 Axios 的拦截器这些命名和分层逻辑都能在$.ajax里找到影子。所以理解$.ajax其实是在理解现代请求库的设计哲学。$.ajax({ url: /api/user/list, type: POST, contentType: application/json, data: JSON.stringify({ page: 1, size: 20 }), dataType: json, timeout: 5000, success: function (res) { console.log(res); }, error: function (xhr, status, err) { console.error(status, err); }, complete: function () { console.log(请求结束无论成功失败); } });这里有个容易混淆的点contentType是发送给服务端的数据格式dataType是期望服务端返回的数据格式。我见过有人把这两个搞反结果服务端收到的 Content-Type 不对直接 415 错误。contentType默认是application/x-www-form-urlencoded如果你传 JSON 字符串必须显式改成application/json。3.2 $.ajax的全局事件与请求生命周期jQuery 提供了一套全局的 Ajax 事件机制这在做统一 loading 和错误处理时非常有用。$(document).ajaxStart(function () { $(#loading).show(); }); $(document).ajaxStop(function () { $(#loading).hide(); }); $(document).ajaxError(function (event, xhr, settings, error) { console.error(全局捕获, settings.url, error); });这套机制的好处是你不需要在每个请求里重复写 loading 逻辑。但要注意ajaxStart和ajaxStop是全局的如果页面上有多个并发请求ajaxStop会在最后一个请求结束时才触发。这个特性在做全局遮罩时是优点但如果你只想针对某个请求做 loading就得用beforeSend和complete局部配置。3.3 jQuery时代的跨域处理JSONP的局限在 CORS 还没普及的年代jQuery 的$.ajax支持dataType: jsonp来绕过同源策略。它的原理是动态插入一个script标签利用 script 标签不受同源限制的特性让服务端返回一段调用回调函数的 JS 代码。$.ajax({ url: http://other-domain.com/api/data, dataType: jsonp, jsonp: callback, jsonpCallback: handleResponse, success: function (res) { console.log(res); } });但 JSONP 有几个致命局限这也是它被 CORS 取代的原因只支持 GET 请求无法发送 POST无法获取 HTTP 状态码服务端返回 500 你也只能拿到一个解析失败无法设置请求头像Authorization这种自定义头完全用不了安全性差本质上是执行第三方返回的脚本存在注入风险。所以现在除非对接一些非常古老的服务端接口否则不要再用 JSONP 了。4. Axios现代前端请求的事实标准4.1 Axios相比fetch解决了哪些痛点fetch是浏览器原生 API按理说应该更轻量但它在实际项目中有几个让人抓狂的设计。Axios 之所以能成为主流正是因为它把这些坑都填了。痛点一fetch 只在网络错误时 reject。服务端返回 404 或 500fetch的 Promise 仍然是 resolved 状态你必须手动检查response.ok。而 Axios 默认在非 2xx 状态码时直接 reject走catch分支符合大多数人的直觉。痛点二fetch 不会自动带 Cookie。需要手动设置credentials: include。Axios 在浏览器端默认会带上同源 Cookie跨域时通过withCredentials: true控制。痛点三fetch 没有超时控制。只能靠AbortController手动实现代码啰嗦。Axios 一个timeout配置搞定。痛点四fetch 没有请求/响应拦截器。你没法在请求发出前统一加 token也没法在响应返回后统一处理错误码。Axios 的拦截器机制让这些横切关注点变得极其优雅。const service axios.create({ baseURL: /api, timeout: 8000, withCredentials: true }); service.interceptors.request.use( config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, error Promise.reject(error) ); service.interceptors.response.use( response { const res response.data; if (res.code ! 0) { console.error(业务错误, res.message); return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response error.response.status 401) { console.warn(登录过期跳转登录页); } return Promise.reject(error); } );这段拦截器代码几乎是每个中后台项目的标配。请求拦截器统一注入 token响应拦截器统一处理业务错误码和 401 跳转。注意响应拦截器里返回的是response.data还是response决定了你后续then里拿到的是什么。我建议统一返回response.data让业务代码更干净但要在团队里约定好避免有人以为拿到的是完整 response。4.2 Axios的取消请求与并发控制Axios 早期用CancelToken取消请求现在推荐用AbortController因为后者是标准 API。const controller new AbortController(); axios.get(/api/search, { params: { keyword: test }, signal: controller.signal }).then(res console.log(res)) .catch(err { if (axios.isCancel(err)) { console.log(请求已取消); } }); // 取消请求 controller.abort();这个场景在搜索框实时联想里特别常见用户每输入一个字符就发一次请求如果不取消上一次可能出现后发的请求先返回导致结果错乱。用AbortController在每次新请求前取消旧请求就能保证只渲染最新一次的结果。并发控制方面Axios 提供了axios.all和axios.spread不过现在更推荐直接用Promise.all。const [userRes, orderRes] await Promise.all([ service.get(/user/info), service.get(/order/list) ]);但要注意Promise.all是一败俱败只要有一个请求失败整个 Promise 就 reject。如果你希望某个请求失败不影响其他结果可以用Promise.allSettled它会等所有请求结束返回每个请求的状态和结果。4.3 Axios在Node端的表现差异Axios 在 Node 环境下底层用的是http/https模块而不是 XHR。这意味着一些浏览器端的配置在 Node 端行为不同。比如withCredentials在 Node 端无效Cookie 需要通过headers.Cookie手动传递。再比如timeout在 Node 端是连接和响应的总超时行为比浏览器端更严格。我在做服务端渲染SSR时踩过一个坑在 Node 端用 Axios 请求内部接口因为没走浏览器同源策略不生效但服务端接口做了来源校验导致请求被拒。解决办法是在请求头里手动加上Referer或Origin或者让服务端对内部调用放行。这个细节在纯前端开发时根本遇不到一旦上 SSR 就会暴露出来。5. CORS跨域从报错到彻底搞懂5.1 同源策略到底在保护什么很多人把跨域当成一个要绕过的障碍但其实同源策略是浏览器最核心的安全机制之一。它的核心逻辑是A 页面的脚本默认不能读取 B 源协议域名端口任一不同的响应内容。注意是读取受限请求本身可能已经发出去了只是响应被浏览器拦截了。这就是为什么你在 Network 面板里能看到请求返回了 200但控制台却报 CORS 错误。请求确实到达了服务端服务端也返回了数据但浏览器发现响应头里没有允许当前源的声明就把响应内容丢弃了不交给 JS。理解这一点很关键因为它决定了排查方向跨域问题要么在服务端加响应头要么在前端走代理而不是去改请求本身。5.2 简单请求与预检请求的分界线CORS 把请求分成两类简单请求和需要预检的请求。这个分类直接决定了你的请求会发一次还是两次。满足以下所有条件的是简单请求条件具体要求方法只能是 GET、HEAD、POST请求头只能包含 Accept、Accept-Language、Content-Language、Content-Type 等安全头Content-Type只能是text/plain、multipart/form-data、application/x-www-form-urlencoded无自定义头不能有Authorization、X-Token等自定义头只要有一条不满足浏览器就会先发一个OPTIONS预检请求问服务端我能不能用这个方法和这些头发请求。服务端必须正确响应这个 OPTIONS 请求返回允许的方法和头浏览器才会发真正的请求。我见过最常见的翻车场景前端发了一个Content-Type: application/json的 POST 请求这不满足简单请求条件因为application/json不在允许的 Content-Type 列表里于是浏览器先发 OPTIONS。服务端没处理 OPTIONS直接返回 405预检失败真正的 POST 根本没发出去。前端看到的现象就是请求发不出去其实是卡在预检。5.3 服务端CORS响应头的正确配置服务端要正确支持 CORS需要根据场景配置以下响应头。我用一个表格把关键头和含义列清楚。响应头作用注意事项Access-Control-Allow-Origin允许的源带 Cookie 时不能为*必须是具体域名Access-Control-Allow-Methods允许的方法预检请求会检查需包含实际使用的方法Access-Control-Allow-Headers允许的请求头自定义头必须列在这里否则预检失败Access-Control-Allow-Credentials是否允许携带凭证为true时 Origin 不能为*Access-Control-Max-Age预检结果缓存时间单位秒减少 OPTIONS 请求次数Access-Control-Expose-Headers允许前端读取的响应头默认只能读简单响应头自定义头需暴露一个典型的 Node/Express 配置长这样app.use((req, res, next) { const origin req.headers.origin; const allowedOrigins [https://a.example.com, https://b.example.com]; if (allowedOrigins.includes(origin)) { res.setHeader(Access-Control-Allow-Origin, origin); res.setHeader(Access-Control-Allow-Credentials, true); } res.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type,Authorization,X-Token); res.setHeader(Access-Control-Max-Age, 86400); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });这里有个细节Access-Control-Allow-Origin如果动态回显req.headers.origin一定要做白名单校验不能无脑回显。否则任何网站都能带着用户 Cookie 请求你的接口等于把同源策略的保护完全拆掉了。这是安全红线不是性能优化。5.4 前端代理开发环境最省心的跨域方案开发阶段最省事的跨域方案不是让后端配 CORS而是用构建工具的代理功能。原理是前端请求发给同源的开发服务器开发服务器再把请求转发给真实后端因为服务器之间的通信不受同源策略限制。Vite 的配置// vite.config.js export default { server: { proxy: { /api: { target: http://backend-server:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } };Webpack DevServer 的配置devServer: { proxy: { /api: { target: http://backend-server:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }changeOrigin: true的作用是把请求头里的 Host 改成目标服务器的域名很多后端会校验 Host不加这个可能被拒。这个配置在开发环境几乎万能但上线后代理就失效了生产环境要么同源部署要么后端配 CORS要么用 Nginx 做反向代理。我见过有人开发时用代理跑得好好的一上线就跨域报错就是因为没意识到代理只在开发服务器生效。6. 跨域排查的完整链路与实战心得6.1 从控制台报错反推问题根源跨域报错的信息其实很有信息量但很多人只看最后一句has been blocked by CORS policy忽略了前面的细节。我总结了一个排查顺序。第一步看请求有没有发出去。在 Network 面板里找那条请求如果状态是(failed)或者CORS error但服务端日志里没有记录说明请求被浏览器拦截在预检阶段或者根本没发出去。第二步看有没有 OPTIONS 请求。如果有 OPTIONS 且返回了非 2xx问题在服务端没处理预检。如果 OPTIONS 返回 204 但真正的请求还是失败看 OPTIONS 响应头里的Access-Control-Allow-Headers是否包含了你实际发送的自定义头。第三步看Access-Control-Allow-Origin的值。如果报错说The value of the Access-Control-Allow-Origin header must not be the wildcard * when the requests credentials mode is include那就是带 Cookie 时用了*必须改成具体域名。第四步看Access-Control-Allow-Credentials。如果前端设了withCredentials: true服务端必须返回Access-Control-Allow-Credentials: true缺一不可。6.2 几个反直觉的跨域现象现象一Postman 能通浏览器不通。这是最经典的困惑。因为 Postman 不是浏览器不受同源策略约束它直接发请求拿响应根本不检查 CORS 头。所以Postman 能通不能证明接口没问题只能证明服务端逻辑是通的。现象二本地开发跨域部署后不跨域。因为部署后前端和后端同源了同一个域名和端口同源策略不触发。这也是为什么很多团队生产环境用 Nginx 把前后端放在同一个域名下从根上避免跨域。现象三改了服务端配置但没生效。检查是不是被 Nginx 或网关层覆盖了响应头。我遇到过一次Node 服务明明返回了正确的 CORS 头但经过一层网关后头被删掉了排查了半天才发现是网关配置问题。现象四预检请求被缓存了。如果Access-Control-Max-Age设得很大改了服务端配置后浏览器还在用缓存的预检结果。这时候需要清空浏览器缓存或者在 Network 面板勾选 Disable cache。6.3 我个人的跨域处理原则经过这么多项目我形成了一个固定的处理顺序开发环境一律用代理生产环境优先同源部署实在不行才配 CORS。原因很简单代理和同源部署不涉及安全配置不会因为头配错导致漏洞而 CORS 配置一旦出错要么功能不通要么安全敞开维护成本最高。如果必须配 CORS我的原则是Origin 白名单动态校验Credentials 按需开启Max-Age 设一个合理值比如 86400 秒Expose-Headers 只暴露必要的头。不要图省事写*尤其是在带 Cookie 的场景下那等于把用户凭证暴露给任何网站。还有一个容易被忽略的点CORS 只保护浏览器不保护服务端。如果你的接口涉及敏感操作不能只靠 CORS 来防必须在服务端做身份校验和权限控制。CORS 是浏览器的行为攻击者完全可以用脚本直接请求你的接口绕过浏览器。所以 CORS 是防君子不防小人真正的安全要靠服务端的鉴权体系。最后分享一个调试小技巧在 Chrome DevTools 的 Network 面板里右键请求选择 Copy as cURL然后把命令粘贴到终端执行。如果 cURL 能通而浏览器不通那百分之百是 CORS 问题如果 cURL 也不通那就是服务端本身的问题跟跨域无关。这个判断方法能帮你快速缩小排查范围省下大量瞎猜的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑