彻底搞懂跨域:6 种解决方案全解析

0 阅读7分钟

在前端开发中,跨域是绕不开的高频问题。新手遇到跨域报错往往一头雾水,老手也可能在不同场景下选错解决方案。本文结合实战代码和项目经验,把同源策略、6 种跨域解决方案的原理、实现、优缺点和适用场景讲透,看完就能落地解决跨域问题。

一、先搞懂:什么是同源策略?

跨域的本质是浏览器的同源策略限制,这是浏览器自带的安全机制,目的是防止恶意网站窃取其他网站的数据。

1. 同源的判断标准

一个 URL 由「协议、域名(IP)、端口、路径」组成,只有前三者完全一致,才被视为 “同源”:

https://192.168.1.10:8080/home 
├── 协议:https
├── 域名/IP:192.168.1.10
├── 端口:8080
└── 路径:/home
  • 协议不同:https://www.baidu.comhttp://www.baidu.com

  • 域名不同:https://www.baidu.comhttps://map.baidu.com

  • 端口不同:https://www.baidu.com:80https://www.baidu.com:8080

2. 跨域的常见场景

  • 前端运行于 127.0.0.1:5500,后端接口部署在 localhost:3000端口不一致,引发跨域;
  • 本地开发环境直接请求线上服务接口,域名不同,引发跨域;
  • 同一主域名下的不同子域名之间相互访问(如 a.xxx.com 请求 b.xxx.com),同样触发跨域。

二、6 种跨域解决方案:原理 + 实战

1. JSONP:利用 script 标签突破限制

核心原理

<script>标签的 src 属性不受同源策略约束,可跨域加载资源。通过动态创建 script 标签,拼接回调函数名向后端请求,后端将数据包裹在回调函数中返回,前端执行函数获取数据。

前端实现(jsonp/index.html)

function jsonp(url, cb) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script') 
    script.src = `${url}?cb=${cb}` // 拼接回调函数参数

    // 挂载全局回调函数
    window[cb] = function(data) {
      resolve(data)
      document.body.removeChild(script) // 清理标签
    }
    document.body.appendChild(script)
  })
}

// 调用示例
jsonp('http://localhost:3000', 'callback').then(res => {
  console.log('JSONP获取数据:', res);
})

后端实现(jsonp/server.js)

const http = require('http')
const server = http.createServer((req, res) => {
  const query = new URL(req.url, 'http://localhost:3000').searchParams
  if (query.get('cb')) {
    const cb = query.get('cb')
    const data = 'Hello JSONP'
    // 拼接回调函数执行语句
    const result = `${cb}("${data}")`
    res.end(result)
  }
})
server.listen(3000, () => console.log('JSONP服务启动:localhost:3000'))

优缺点

✅ 优点:兼容性好,支持老旧浏览器; ❌ 缺点:仅支持 GET 请求,需后端配合,存在 XSS 安全风险。

2. CORS:后端配置响应头(最常用)

核心原理

CORS(跨域资源共享)是 W3C 标准,通过后端在响应头中设置允许跨域的规则,告诉浏览器 “该域名可以访问资源”,彻底解决跨域问题。

后端实现(cors/server.js)

const http = require('http')
const server = http.createServer((req, res) => {
  // 允许指定源跨域(* 表示允许所有源,生产环境建议指定具体域名)
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
  // 允许所有请求方法(GET/POST/PUT等)
  res.setHeader('Access-Control-Allow-Methods', '*')
  // 允许自定义请求头
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

  // 返回数据
  res.end(JSON.stringify({ name: '张三', age: 18 }))
})
server.listen(3000, () => console.log('CORS服务启动:localhost:3000'))

前端调用(cors/index.html)

// 普通fetch请求即可,无需额外处理
fetch('http://localhost:3000').then(res => res.json()).then(data => {
  console.log('CORS获取数据:', data);
})

优缺点

✅ 优点:支持所有 HTTP 请求方法,前端无需额外处理,适配现代浏览器; ❌ 缺点:需后端配合配置,复杂场景(如带 Cookie)需额外配置withCredentials

3. 代理跨域:前端 “曲线救国”

核心原理

跨域是浏览器的限制,服务器之间请求无跨域问题。通过搭建代理服务器,前端请求代理服务器,代理服务器转发请求到后端,再将结果返回给前端,绕开浏览器的同源策略。

方案 1:Node 代理(proxy/server.js)

const http = require('http')
const server = http.createServer((req, res) => {
  // 允许前端跨域访问代理服务器
  res.setHeader('Access-Control-Allow-Origin', '*')
  
  // 代理转发请求到目标后端
  http.request({
    hostname: '192.168.31.221', // 后端真实地址
    port: 3000,
    path: '/',
    method: 'GET'
  }, (response) => {
    response.on('data', (chunk) => {
      res.end(chunk.toString()) // 将后端数据返回给前端
    })
  }).end()
})
server.listen(3000, () => console.log('Node代理服务启动:localhost:3000'))

方案 2:Vite 代理(开发环境首选)

vite\.config\.js中配置代理,无需搭建独立 Node 服务:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      // 匹配以/api开头的请求
      '/api': {
        target: 'http://192.168.31.221:3000', // 后端地址
        changeOrigin: true, // 开启跨域模拟
        rewrite: (path) => path.replace(/^\/api/, '') // 去除/api前缀
      }
    }
  }
})

方案 3:Nginx 代理(生产环境)

Nginx 配置示例(核心逻辑):

server {
  listen 80;
  server_name localhost;

  # 代理前端请求到后端
  location /api {
    proxy_pass http://192.168.31.221:3000; # 后端地址
    add_header Access-Control-Allow-Origin *;
  }
}

优缺点

✅ 优点:前端可控,无需后端改代码,开发 / 生产环境均适用; ❌ 缺点:仅转发请求,需维护代理服务器(Nginx/Node)。

4. WebSocket:不受同源策略约束的全双工通信

核心原理

WebSocket 是独立的协议,不属于 HTTP,天生不受同源策略限制,可实现前端和后端的全双工实时通信(前端发请求、后端主动推送)。

后端实现(socket/server.js)

const websocket = require('ws')
const ws = new websocket.Server({ port: 3000 })
let count = 0

ws.on('connection', (socket) => {
  console.log('客户端连接成功');
  
  // 接收客户端消息
  socket.on('message', (data) => {
    console.log('客户端发送:', data.toString());
    
    // 定时向前端推送数据
    setInterval(() => {
      socket.send(JSON.stringify({ count: count++ }))
    }, 2000)
  })
})

前端实现(socket/index.html)

function myWebSocket(url, params) {
  return new Promise((resolve, reject) => {
    const socket = new WebSocket(url)
    // 连接成功
    socket.onopen = function() {
      console.log('WebSocket连接成功');
      socket.send(JSON.stringify(params)) // 发送参数
    }
    // 接收后端推送的消息
    socket.onmessage = function(e) {
      console.log('后端推送:', e.data);
      resolve(e.data)
    }
  })
}

// 调用示例
myWebSocket('ws://localhost:3000', { age: 18 })

适用场景

实时性要求高的业务:滴滴司机位置更新、聊天消息推送、烟感报警器实时报警等。

优缺点

✅ 优点:实时性强,全双工通信,无跨域限制; ❌ 缺点:服务端性能开销大,需维护长连接。

5. postMessage:跨窗口 /iframe 通信

核心原理

HTML5 新增的postMessage方法,允许不同窗口、不同域的页面之间安全地传递数据,适用于 iframe 嵌套、多窗口通信场景。

父页面(postMessage/index.html)

const iframe = document.querySelector('iframe')
const data = { name: '探长', age: 18 }

// 等待iframe加载完成
iframe.onload = function() {
  // 向子页面发送消息(指定目标域)
  this.contentWindow.postMessage(data, 'http://127.0.0.1:8080/')
  
  // 接收子页面回传的消息
  window.onmessage = function(e) {
    console.log('子页面回传:', e.data);
  }
}

子页面(postMessage/index2.html)

// 监听父页面消息
window.onmessage = function(e) {
  console.log('父页面发送:', e.data);
  document.getElementById('name').innerHTML = e.data.name
  
  // 3秒后回传消息
  setTimeout(() => {
    const newData = { ...e.data, age: e.data.age + 1 }
    e.source.postMessage(newData, e.origin)
  }, 3000)
}

优缺点

✅ 优点:适配多窗口 /iframe 场景,不受域名限制; ❌ 缺点:仅适用于浏览器环境,需处理消息来源校验。

6. document.domain:子域名跨域(已废弃)

核心原理

通过设置document\.domain将不同子域名的页面统一为相同域名,实现跨域访问。注意:Chrome 115 版本已废弃该特性,仅作了解。

// 父页面(a.xxx.com)和子页面(b.xxx.com)都设置
document.domain = 'xxx.com'
// 父页面可直接访问子页面的window数据
console.log(iframe.contentWindow.data)

优缺点

✅ 优点:简单易实现(仅适用于子域名); ❌ 缺点:Chrome 已废弃,兼容性差,仅支持子域名。

三、解决方案选型指南

解决方案适用场景核心优势注意事项
JSONP老旧浏览器、仅需 GET 请求兼容性好仅支持 GET,有 XSS 风险
CORS现代浏览器、前后端协同开发支持所有请求方法,前端无侵入需后端配置响应头
代理(Node/Vite/Nginx)前后端分离项目、生产 / 开发环境前端可控,无需后端改代码需维护代理服务
WebSocket实时通信(聊天、推送)全双工、实时性强服务端性能开销大
postMessage跨窗口 /iframe 通信跨域传递数据灵活需校验消息来源
document.domain子域名跨域(仅历史项目)实现简单Chrome 已废弃,不推荐

四、总结

跨域问题的本质是浏览器的同源策略限制,解决思路主要分三类:

  1. 绕开限制(JSONP、WebSocket);

  2. 后端放行(CORS);

  3. 中间层转发(代理)。

实际开发中,CORS 是最常用的方案(前后端协同),Vite/Node 代理 是开发环境首选(前端自主解决),WebSocket 适配实时通信场景,其他方案根据业务兼容性、场景需求灵活选择即可。