学完这篇,你能分清 8 种 JavaScript 跨域方案各自解决什么问题、什么时候该用哪一种,以及每种方案最容易出错的地方在哪。
浏览器发起跨域请求会报错,是因为同源策略在拦截跨域请求:协议、域名、端口三者任意一项不同,就算跨域。下面 8 种方式,前 3 种是主流选择,后 5 种各有特定场景。
方式一:CORS(服务端响应头,首选)
由后端添加响应头,这是最正规、能力最全的做法。
Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
前端正常用 fetch 或 axios 发请求即可,不需要额外写代码。
注意:带 cookie 时 Allow-Origin 不能写 *,必须写具体域名,同时前端要加 credentials: 'include',后端要加 Allow-Credentials: true;三者缺一不可。另外,非简单请求(比如 Content-Type: application/json、带自定义头)会先发 OPTIONS 预检,后端必须正确响应 OPTIONS 并返回 204,否则真实请求根本发不出去。
方式二:开发环境代理(本地调试用)
本地用 Vite、webpack 开发时的首选:让请求先发到本地 dev server,再由它转发,浏览器就感知不到跨域。
// vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
前端把 baseURL 改成 /api 就行,上线后交给 Nginx。
方式三:Nginx 反向代理(生产环境)
前后端同域部署,把接口路径转给后端服务:
location /api/ {
proxy_pass https://api.b.com/;
proxy_set_header Host api.b.com;
}
注意:proxy_pass 末尾有没有斜杠,会改变路径的拼接方式。https://api.b.com/ 会截掉 location 匹配的部分,https://api.b.com 则会保留。写错就会返回 404。
方式四:JSONP(老项目兼容)
利用 script 标签不受同源限制的特点。只能发 GET 请求,也不方便统一处理错误。
function jsonp(url, cb) {
const name = 'cb_' + Date.now();
const s = document.createElement('script');
window[name] = data => { cb(data); delete window[name]; s.remove(); };
s.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + name;
document.body.appendChild(s);
}
注意:JSONP 没有原生的超时和错误回调,必须自己加 setTimeout 做保底处理,否则接口挂掉时页面会一直等待。另外,它等于允许对方往你的页面注入脚本,存在 XSS 风险,对接的接口必须完全可信。
方式五:postMessage(跨窗口通信)
用于父页面与 iframe、window.open 打开的窗口之间传输数据,不限于 HTTP 请求。
// 发送方
iframe.contentWindow.postMessage({ type: 'ping' }, 'https://b.com');
// 接收方
window.addEventListener('message', e => {
if (e.origin !== 'https://b.com') return; // 必须校验
console.log(e.data);
});
注意:targetOrigin 千万不要写 '*',接收端也一定要校验 e.origin,否则任何页面都能往你的窗口发消息。
方式六:WebSocket
WebSocket 连接本身不受同源策略约束,可以跨域建立长连接,适合聊天、实时推送。
但服务端握手时必须校验 Origin 请求头,如果不校验,就等于允许任意站点连接你的服务。
方式七:document.domain(仅同主域,已不推荐)
a.example.com 和 b.example.com 双方都执行 document.domain = 'example.com',就能互通。
注意:现代浏览器已经开始弃用 document.domain 的 setter,Chrome 会给出 Origin-keyed agent cluster 警告,而且这种方式只对同主域有效。新项目不要用它。
方式八:服务端中转(BFF 或网关)
第三方接口完全不给 CORS 头时,让自家后端去请求,浏览器只访问自家域名,自然不跨域:
浏览器 → 你的后端(同域) → 第三方接口
没有浏览器限制,还能顺便做统一鉴权、日志和对返回结果做脱敏。缺点是多一次跳转,延迟略高。
八种方式快速对比
| 方式 | 改服务端 | 支持方法 | 带 Cookie | 典型场景 |
|---|
| CORS | 是 | 全部 | 支持 | 前后端分离接口 |
| 开发代理 | 否(本地配置) | 全部 | 支持 | 本地联调 |
| Nginx 代理 | 是(运维) | 全部 | 支持 | 生产同域部署 |
| JSONP | 是 | 仅 GET | 否 | 老系统兼容 |
| postMessage | 是 | — | — | iframe、弹窗通信 |
| WebSocket | 是 | — | — | 实时推送 |
| document.domain | 是 | — | — | 已过时 |
| 服务端中转 | 是 | 全部 | 支持 | 第三方无 CORS |
选择建议:能改后端就用 CORS;生产环境想省事就用 Nginx 反向代理;本地开发用 dev server 代理;第三方接口不配合就走服务端中转。JSONP、document.domain、window.name 这类属于历史遗留,只在维护老代码时才会遇到。
小结
- 跨域的本质是同源策略,必须在浏览器之外解决,前端单纯修改代码绕不过去
- CORS 是首选,带 cookie 时不能把源写成
*,并且一定要能正确响应 OPTIONS 预检
- 开发和生产的代理是两套配置:本地
server.proxy,线上 Nginx,别混用
proxy_pass 的末尾斜杠、postMessage 的 origin 校验,是最常出错的两个位置
- JSONP 和
document.domain 只在兼容老项目时用,新项目不要引入
- 无论用哪种方式,服务端都要校验来源,不要为了省事放开
*