前端知库

跨域有哪些解决方案?JSONP 和代理是怎样工作的?

首选服务端配置 CORS;开发环境用开发服务器代理,生产环境用 Nginx 这类反向代理,让浏览器只请求同源地址;JSONP 利用 script 标签不受同源策略限制来拿数据,只支持 GET,而且等于执行对方返回的脚本,现在基本不用;跨窗口通信用 postMessage。

参考答案

简答

页面在 https://shop.example.com,接口在 https://api.example.com,两者域名不同,浏览器的同源策略默认不让页面脚本读取这个接口的响应,这就是跨域问题。解决办法可以按“谁来放行”分成几类。

最标准的做法是 CORS:服务器在响应里加上 Access-Control-Allow-Origin 等响应头,告诉浏览器“这个来源可以读我的响应”。浏览器看到允许就把响应交给脚本。复杂请求还会先发一个 OPTIONS 预检。它要后端配合,但最规范。

第二类是代理。同源策略只管浏览器,服务器之间互相请求不受限制。所以让浏览器请求同源的地址,比如 /api/user,再由同源的服务器把请求转发给真正的接口。开发时用 Vite、webpack dev server 的 proxy 配置,上线后用 Nginx 反向代理,或者把页面和接口部署在同一个域名下。

第三类是老办法 JSONP。<script> 标签加载别的域名的脚本不受同源策略限制,于是让接口返回一段调用函数的脚本,比如 handleData({...}),页面事先定义好 handleData,脚本一执行,数据就作为参数传进来了。要注意,它只能发 GET,出错很难捕获,最大的问题是等于无条件执行对方返回的代码,接口被篡改就能在你的页面上为所欲为,现在应该用 CORS 代替。

代理为什么能解决跨域

JavaScript
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
};

页面里请求 /api/user,浏览器发给的是开发服务器自己,和页面同源,不存在跨域。Vite 文档的说法是,路径以 /api 开头的请求,会被代理转发到配置的 target。开发服务器收到后,去掉 /api 前缀,向 https://api.example.com/user 发请求,再把结果原样返回给浏览器。

changeOrigin: true 有什么用?它会把转发请求的 Host 头改成目标服务器的地址。有些服务器按 Host 区分站点,不改的话会收到 localhost,可能返回 404 或拒绝请求。

要注意,Vite 的 server.proxy 只在开发服务器上生效,打包出来的静态文件里没有这个代理。上线后要由 Nginx 或网关做同样的转发,不然请求 /api 会直接 404。

JSONP 是怎样拿到数据的

JavaScript
function jsonp(url, callbackName) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    window[callbackName] = (data) => {
      resolve(data);
      script.remove();
      delete window[callbackName];
    };
    script.onerror = () => reject(new Error('JSONP 请求失败'));
    script.src = `${url}?callback=${callbackName}`;
    document.head.append(script);
  });
}

页面先在全局定义一个函数,再插入一个 <script>,把函数名通过 callback 参数告诉服务器。服务器返回的不是 JSON,而是一行脚本 handleData({"name":"小明"})。浏览器加载并执行这段脚本,就调用了页面事先定义的函数,数据作为参数传了进来。

为什么只能发 GET?因为 <script> 加载资源只会发 GET 请求,也没法设置请求头。为什么不安全?返回的内容是直接执行的脚本,接口要是返回了别的代码,页面也会照样执行。开启了 CSP 的页面,还要把接口域名加进 script-src 才能加载。

常见追问

CORS 报错了,请求到底发出去没有?

简单请求已经发出去了,服务器也可能已经执行了,只是浏览器不让脚本读响应。需要预检的请求,预检没通过时真正的请求不会发出。具体规则见《什么是跨域?CORS 为什么需要服务端配合?》。

页面和 iframe、新窗口之间跨域怎么通信?

用 postMessage。发送时指定目标来源,接收时检查 event.origin,见《跨窗口 postMessage 怎样避免把消息发给错误对象?》。document.domain 以前也能让同一主域下的页面互相访问,现在浏览器正在逐步禁用它,不要再用。