ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端老兵实战解析:Ajax异步请求、同源策略与跨域传值全链路

前端老兵实战解析:Ajax异步请求、同源策略与跨域传值全链路 1. 从一个前端老兵的视角重新理解Ajax干了十来年前端如果让我挑一个最能体现“前端工程思维”的技术点Ajax绝对排得进前三。很多人刚入行的时候觉得Ajax不就是$.ajax一把梭吗但真正在项目里踩过坑的人都知道异步请求、同源策略、跨域传值这三件事任何一个没搞明白都够你加班到凌晨。这篇文章我想聊的不是教科书上的定义而是从一个实际项目的角度出发把Ajax异步请求的完整链路拆开揉碎把同源策略这个“隐形守门员”讲清楚再把跨域传值的几种主流方案做一个实战级别的对比。不管你是刚接触前端的新人还是已经写了几年业务代码但一直对跨域似懂非懂的老手我相信都能从里面找到一些可以直接抄作业的东西。核心关键词我会贯穿全文Ajax异步请求是手段同源策略是约束条件跨域传值是在约束下找到的解法。这三者是一条完整的因果链——不理解同源策略你就不知道为什么会有跨域问题不理解异步请求的底层机制你就不知道跨域方案为什么有的可行有的不可行。适合阅读的人群正在做前后端分离项目的开发者、需要对接第三方接口的工程师、以及任何被CORS报错折磨过的同学。我会尽量用生活化的类比来解释原理同时给出可以直接复制的代码和配置。2. Ajax异步请求的核心机制与设计思路2.1 为什么是“异步”从页面刷新到局部更新在Ajax出现之前网页的交互模式非常“笨重”。用户填完一个表单点击提交整个页面白屏等待服务器返回一个全新的HTML页面重新渲染。这个过程里哪怕你只是想把用户名输入框校验一下也得走一次完整的页面跳转。Ajax的核心价值就在于异步两个字。所谓异步就是浏览器在后台悄悄发一个请求出去不阻塞当前页面的任何操作等数据回来了再通过回调或者Promise通知你。用户感知到的就是“页面没动但内容变了”。我经常用一个类比来解释传统同步请求像是你去餐厅点菜点完必须站在柜台前等厨师做完才能走Ajax异步请求像是你点完菜拿了个号牌回座位厨师做好了会叫你。这个“号牌”就是回调函数或者Promise对象。从技术实现上看现代浏览器提供了两种发起异步请求的方式XMLHttpRequestXHR老牌方案兼容性极好但写法繁琐回调嵌套深。Fetch API新一代标准基于Promise写法简洁但需要注意它默认不带cookie且对HTTP错误状态码不会自动reject。我个人的建议是新项目一律用Fetch或者基于Fetch封装的库比如axios老项目维护时如果已经用了jQuery的$.ajax没必要强行迁移但要清楚它底层的XHR机制。2.2 一次完整的Ajax请求经历了什么很多人写Ajax就是调个方法传个URL但如果你不清楚请求从发出到返回中间经历了哪些环节遇到问题时就无从下手。我把一次完整的Ajax异步请求拆成以下几个阶段创建请求对象XHR模式下是new XMLHttpRequest()Fetch模式下是调用fetch()返回一个Promise。配置请求参数设置请求方法GET/POST/PUT/DELETE、请求URL、请求头Content-Type、自定义头等、请求体。发送请求浏览器将请求交给网络线程处理此时主线程继续执行后续代码这就是异步的体现。服务器处理并返回服务器收到请求处理后返回响应数据。浏览器接收响应浏览器检查响应是否通过同源策略和CORS校验通过则将数据交给回调不通过则抛出跨域错误。回调处理开发者在回调函数中解析数据、更新DOM或触发下一步操作。这里面第5步是很多人容易忽略的。同源策略的校验发生在浏览器接收到响应之后而不是在发送请求之前。这意味着即使跨域请求被拦截了服务器那边其实已经收到了请求并可能已经处理了。这一点在做调试和排查时非常关键。2.3 请求参数与编码格式那些年我们踩过的坑热词里有一个“ajax请求设置编码格式”这确实是个高频痛点。Ajax请求中涉及编码的地方主要有三处第一处是请求头的Content-Type。它告诉服务器我发过来的数据是什么格式。常见的取值有Content-Type数据格式典型场景application/x-www-form-urlencodedkeyvaluekey2value2传统表单提交application/jsonJSON字符串前后端分离项目multipart/form-data二进制分段文件上传text/plain纯文本简单数据传输我见过太多人用application/json发请求但后端按表单格式解析结果一直拿不到参数。反过来也有用表单格式发后端用JSON解析器去读同样拿不到。前后端一定要在接口文档里明确约定Content-Type这是最基本的协作规范。第二处是URL中的中文参数编码。GET请求的参数拼接在URL上如果包含中文或特殊字符必须用encodeURIComponent()处理。我遇到过有人直接拼中文参数在Chrome里测试没问题到了某些环境就乱码了。原因是不同浏览器和服务器对URL编码的默认处理不一致。第三处是请求体和响应体的字符集。虽然现在UTF-8已经是事实标准但在一些老系统对接时仍然会遇到GBK编码的情况。这时候需要在Content-Type中明确指定charsetutf-8并且确保前后端一致。注意使用Fetch API时如果你传的是一个对象作为body它不会自动序列化也不会自动设置Content-Type。你需要手动JSON.stringify()并设置请求头。这是新手最容易犯的错误之一。3. 同源策略浏览器的安全底线3.1 什么是“同源”三个维度缺一不可同源策略是浏览器最核心的安全机制之一。所谓“同源”要求协议、域名、端口三者完全相同。任何一个不同就是跨域。举个例子当前页面地址是https://www.example.com:443/pagehttps://www.example.com:443/api→ 同源协议、域名、端口都相同http://www.example.com:443/api→ 跨域协议不同https://api.example.com:443/data→ 跨域域名不同https://www.example.com:8080/api→ 跨域端口不同注意一个细节https的默认端口是443http的默认端口是80。如果URL中没有显式写端口浏览器会使用默认端口。所以https://www.example.com和https://www.example.com:443是同源的。还有一个容易混淆的点域名和IP地址之间即使指向同一台服务器也算跨域。比如页面在http://localhost:3000请求发往http://127.0.0.1:3000虽然本质上是同一台机器但浏览器认为域名不同判定为跨域。3.2 同源策略到底在保护什么很多人觉得同源策略是个“麻烦制造者”但实际上它是保护用户数据安全的关键防线。设想一下没有同源策略的世界你登录了网上银行页面里保存着你的登录凭证Cookie。这时候你手贱点开了一个恶意网站那个网站的JavaScript代码可以直接向银行网站发起请求因为浏览器会自动带上银行的Cookie恶意网站就能拿到你的账户信息。同源策略限制的主要是以下几类操作Ajax请求不允许向不同源的地址发起XHR/Fetch请求除非对方明确允许。DOM访问不允许不同源的页面之间互相操作DOM比如iframe里的页面不能读取父页面的内容。Cookie/LocalStorage访问不允许不同源的页面读取彼此的存储数据。但要注意同源策略并不是万能的。它主要限制的是“读取”操作对于“写入”操作比如表单提交、图片加载、脚本引入限制相对宽松。这也是为什么JSONP这种跨域方案能够存在——它利用的就是script标签不受同源策略限制的特性。3.3 浏览器的同源策略在Ajax中的具体表现当你用Ajax向一个不同源的地址发起请求时浏览器的行为分两种情况简单请求请求方法为GET、POST或HEAD且请求头只包含安全的首部字段如Accept、Accept-Language、Content-Language、Content-Type且值为application/x-www-form-urlencoded、multipart/form-data或text/plain浏览器会直接发出请求但会检查响应头中是否包含Access-Control-Allow-Origin。如果没有或值不匹配浏览器会拦截响应控制台报CORS错误。非简单请求不满足简单请求条件的比如用了PUT/DELETE方法或者Content-Type是application/json或者带了自定义请求头浏览器会先发一个预检请求OPTIONS方法询问服务器是否允许这次跨域请求。服务器需要正确响应预检请求返回允许的方法、头信息等浏览器才会发出真正的请求。这个预检机制是很多人调试跨域时的“噩梦”。因为预检请求在Network面板里可能一闪而过或者被过滤掉了导致你以为请求根本没发出去。实际上服务器收到了OPTIONS请求只是没有正确响应。实操心得调试CORS问题时一定要在Network面板中勾选“显示所有请求”确保能看到OPTIONS预检请求。如果预检失败真正的请求根本不会发出。4. 跨域传值的实战方案与选型对比4.1 CORS最正统的跨域解决方案CORSCross-Origin Resource Sharing是W3C标准也是目前最推荐的跨域方案。它的核心思路是由服务器明确声明允许哪些源来访问自己的资源。服务端需要设置的响应头主要有Access-Control-Allow-Origin允许的源可以是具体域名或*表示允许所有源。Access-Control-Allow-Methods允许的HTTP方法如GET, POST, PUT, DELETE。Access-Control-Allow-Headers允许的请求头如Content-Type, Authorization。Access-Control-Allow-Credentials是否允许携带Cookie值为true或false。Access-Control-Max-Age预检请求的缓存时间单位秒。这里有几个容易踩的坑坑一Access-Control-Allow-Origin设为*时不能携带Cookie。如果你需要跨域携带Cookie比如维持登录态Access-Control-Allow-Origin必须是具体的域名不能是通配符。同时前端请求需要设置withCredentials: trueXHR或credentials: includeFetch。坑二预检请求的响应也需要包含完整的CORS头。很多人只在真正的接口响应里加了CORS头忘了OPTIONS请求也要加导致预检失败。坑三Nginx反向代理时CORS头可能重复。如果后端框架已经设置了CORS头Nginx又加了一遍浏览器会报“multiple values”错误。需要确保只在一处设置。以Node.js Express为例一个完整的CORS配置大概长这样app.use((req, res, next) { res.header(Access-Control-Allow-Origin, https://www.example.com); res.header(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); res.header(Access-Control-Allow-Headers, Content-Type, Authorization); res.header(Access-Control-Allow-Credentials, true); res.header(Access-Control-Max-Age, 86400); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });4.2 反向代理开发环境下的首选方案在开发阶段我几乎不会去折腾后端的CORS配置而是直接用开发服务器的反向代理功能。原理很简单让前端开发服务器如webpack-dev-server、Vite把特定路径的请求转发到后端服务器这样浏览器看来请求还是发往同源的开发服务器不存在跨域问题。以Vite为例在vite.config.js中配置export default { server: { proxy: { /api: { target: http://backend-server:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } }这样前端代码里请求/api/user/list实际会被转发到http://backend-server:8080/user/list。浏览器全程以为自己在和同源的开发服务器通信完全绕开了同源策略。这种方案的好处是前端代码不需要做任何跨域适配后端也不需要改任何配置。缺点是只适用于开发环境生产环境还是需要CORS或Nginx代理。4.3 JSONP历史方案了解即可JSONP是利用script标签不受同源策略限制的特性来实现跨域的。原理是前端定义一个全局回调函数然后动态创建一个script标签src指向跨域接口并带上回调函数名作为参数。服务器返回一段JavaScript代码调用这个回调函数并传入数据。function handleData(data) { console.log(拿到数据了, data); } const script document.createElement(script); script.src http://api.example.com/data?callbackhandleData; document.body.appendChild(script);JSONP的局限性很明显只支持GET请求不支持POST没有错误处理机制存在安全风险服务器返回的脚本会被直接执行。现在除非对接非常老旧的系统否则不建议使用。4.4 三种方案对比与选型建议方案适用场景优点缺点CORS生产环境前后端分离标准方案支持所有HTTP方法需要后端配合配置反向代理开发环境、生产环境Nginx前端零改动后端零改动需要运维配置代理服务器JSONP对接老旧系统兼容性极好只支持GET安全性差我个人的选型逻辑是开发环境一律用反向代理省时省力生产环境优先用CORS由后端统一配置如果后端团队不方便改代码就在Nginx层做反向代理。JSONP只在对接第三方老接口且对方不支持CORS时才考虑。5. 常见问题排查与避坑指南5.1 CORS报错速查表报错信息原因解决方法No Access-Control-Allow-Origin header服务器未返回CORS头后端添加响应头The Access-Control-Allow-Origin header contains multiple values多处重复设置了CORS头检查Nginx和后端框架只保留一处Credentials flag is true, but Access-Control-Allow-Origin is *携带Cookie时用了通配符将Origin改为具体域名Request header field xxx is not allowed自定义请求头未在Allow-Headers中声明后端添加对应的头Method PUT is not allowed请求方法未在Allow-Methods中声明后端添加对应的方法5.2 那些文档里不会写的实操经验经验一预检请求的缓存时间要合理设置。Access-Control-Max-Age设得太短每次请求都要发预检性能差设得太长后端改了CORS配置后客户端长时间不生效。我一般设86400秒24小时兼顾性能和灵活性。经验二本地开发用localhost和127.0.0.1的区别。有些后端框架在判断Origin时对localhost和127.0.0.1的处理不一致。如果遇到莫名其妙的跨域问题试试把两者统一。经验三Postman能通不代表浏览器能通。Postman不受同源策略限制所以它请求成功不代表浏览器里也能成功。排查跨域问题时一定要以浏览器为准。经验四HTTPS页面请求HTTP接口会被浏览器直接拦截。这属于混合内容Mixed Content问题不是CORS问题但报错信息有时候容易混淆。解决方法是接口也升级到HTTPS。经验五Cookie的SameSite属性会影响跨域携带。现代浏览器默认SameSiteLax跨域请求不会带上Cookie。如果需要跨域携带Cookie后端设置Cookie时要加SameSiteNone; Secure。5.3 一个真实的排查案例之前有个项目前端在http://localhost:3000后端在http://localhost:8080。前端用axios发POST请求Content-Type设为application/json一直报CORS错误。排查过程首先看Network面板发现有一个OPTIONS请求返回了403。说明预检请求被后端拦截了。检查后端代码发现权限拦截器没有放行OPTIONS方法。在拦截器里加上对OPTIONS请求的直接放行后问题解决。这个案例的教训是预检请求不携带业务数据也不携带认证信息后端的权限拦截器需要特别放行OPTIONS请求。很多安全框架默认会拦截所有请求导致预检失败。6. 写给不同阶段开发者的建议如果你是刚入行的前端我的建议是先把XMLHttpRequest的原生写法手写一遍理解onreadystatechange和readyState的含义然后再用Fetch或axios。知其然也知其所以然遇到问题时才能快速定位。如果你已经工作了一两年天天写业务代码但一直对跨域似懂非懂我建议你亲手搭一个前后端分离的小项目前端用一个端口后端用另一个端口然后分别用CORS和反向代理两种方式跑通。跑通一次之后这个概念就彻底属于你了。如果你是在做全栈开发那更要理解同源策略的设计初衷。它不是来给你添堵的而是在保护你的用户。每次配置CORS时不要图省事直接上*想清楚哪些源真的需要访问把权限控制到最小粒度。最后分享一个我自己的习惯每次新建项目第一件事就是把开发环境的反向代理配好第二件事就是和后端确认生产环境的CORS策略。这两件事提前做好后面能省掉大量联调时间。跨域问题不可怕可怕的是不知道问题出在哪一层。把请求链路理清楚把浏览器开发者工具用熟大部分问题都能在十分钟内定位到根因。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进