JavaScript前端怎么实现跨域请求

182 阅读9分钟

前端跨域请求实现方案:原理、代码与场景选型

跨域的核心原因是 浏览器同源策略(协议、域名、端口三者必须一致,否则限制资源访问)。前端实现跨域的核心思路分两类:

  1. 客户端方案:前端直接配置(无需后端修改,如 JSONP、CORS 前端适配);
  2. 代理方案:通过中间层转发请求(避开浏览器同源限制,如开发环境代理、Nginx 代理)。

以下是 6 种主流跨域方案,从原理、代码实现到适用场景逐一解析,覆盖开发 / 生产全场景。

一、核心概念铺垫(先懂同源策略)

1. 同源判定标准

两个 URL 需同时满足以下 3 点才是 “同源”,否则为跨域:

  • 协议相同(如 http vs https 不同);
  • 域名相同(如 a.com vs b.a.com 不同);
  • 端口相同(如 8080 vs 3000 不同)。

2. 跨域限制的行为

  • 无法通过 XMLHttpRequest/Fetch 发起跨域 HTTP 请求;
  • 无法访问跨域 DOM(如 iframe 内容);
  • 无法读取跨域 Cookie/LocalStorage。

本文重点解决 跨域 HTTP 请求 问题(前端最常用场景)。

二、方案 1:CORS(跨域资源共享)—— 推荐生产环境

CORS(Cross-Origin Resource Sharing)是 W3C 标准,也是最主流的跨域方案。核心是后端在响应头中添加允许跨域的配置,前端无需额外修改(或仅需简单适配),支持所有 HTTP 方法和复杂请求(如带 Cookie、自定义头)。

1. 原理

  • 简单请求(GET/POST/HEAD,无自定义头、Content-Type 为 application/x-www-form-urlencoded/multipart/form-data/text/plain):直接发起请求,后端响应头返回 Access-Control-Allow-Origin: 允许的域名 即可;
  • 复杂请求(PUT/PATCH/DELETE、带自定义头、Content-Type 为 application/json):先发起 OPTIONS 预检请求,验证后端是否允许跨域,通过后再发起真实请求。

2. 前端实现(无需额外代码,仅需后端配合)

(1)基础跨域请求(无 Cookie)
// Fetch 示例(Axios 用法类似)
fetch('https://api.other-domain.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json', // 触发 OPTIONS 预检
  },
  body: JSON.stringify({ name: '张三' }),
})
.then(res => res.json())
.then(data => console.log('跨域响应:', data))
.catch(err => console.log('跨域错误:', err));
(2)带 Cookie 的跨域请求(需前后端配合)

前端需设置 credentials: true,后端需返回 Access-Control-Allow-Credentials: true 且 Access-Control-Allow-Origin 不能为 *(需指定具体域名):

// Fetch 带 Cookie
fetch('https://api.other-domain.com/data', {
  method: 'GET',
  credentials: 'include', // 允许跨域携带 Cookie(Axios 中是 withCredentials: true)
})
.then(res => res.json())
.then(data => console.log(data));

// Axios 带 Cookie 全局配置
axios.defaults.withCredentials = true;
axios.get('https://api.other-domain.com/data')
.then(res => console.log(res.data));

3. 后端核心配置(以 Node.js/Express 为例)

const express = require('express');
const app = express();

// 全局跨域中间件(核心配置)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://your-frontend-domain.com'); // 允许的前端域名(不可为 * 若带 Cookie)
  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'); // 允许携带 Cookie
  if (req.method === 'OPTIONS') {
    res.sendStatus(204); // 预检请求直接返回成功
  } else {
    next();
  }
});

4. 优点与适用场景

  • 优点:标准方案、支持所有 HTTP 方法、支持复杂请求、安全性高;
  • 适用场景:生产环境(前后端可控,如公司内部前后端分离项目、第三方接口开放 CORS)。

三、方案 2:JSONP —— 兼容旧浏览器(仅支持 GET)

JSONP 是 古老但兼容的跨域方案,核心利用 script 标签不受同源策略限制的特性,仅支持 GET 方法(因为 script 标签只能发起 GET 请求)。

1. 原理

  1. 前端创建 script 标签,src 指向跨域接口,同时传入回调函数名(如 callback=handleData);
  2. 后端接收回调函数名,返回 handleData(响应数据) 格式的 JS 代码;
  3. 前端 script 标签加载后,执行回调函数,获取跨域数据。

2. 前端实现(原生 / 封装版)

(1)原生 JSONP 实现
// 1. 定义回调函数(接收跨域数据)
function handleJsonpData(data) {
  console.log('JSONP 跨域数据:', data);
  // 移除创建的 script 标签(避免冗余)
  document.body.removeChild(scriptTag);
}

// 2. 创建 script 标签,发起跨域请求
const scriptTag = document.createElement('script');
// 跨域接口 + 回调函数参数(后端需解析 callback 参数)
scriptTag.src = 'https://api.other-domain.com/data?callback=handleJsonpData';
document.body.appendChild(scriptTag);
(2)封装 JSONP 工具函数(复用)
/**
 * JSONP 跨域请求工具
 * @param {string} url - 跨域接口地址
 * @param {object} params - 请求参数(含 callback 可选)
 * @returns {Promise} - 返回 Promise 便于异步处理
 */
function jsonp(url, params = {}) {
  return new Promise((resolve, reject) => {
    // 1. 生成唯一回调函数名(避免冲突)
    const callbackName = `jsonpCallback_${Date.now()}`;
    // 2. 拼接参数(添加回调函数名)
    const paramsStr = Object.entries({ ...params, callback: callbackName })
      .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
      .join('&');
    // 3. 创建 script 标签
    const scriptTag = document.createElement('script');
    scriptTag.src = `${url}?${paramsStr}`;
    scriptTag.onerror = (err) => {
      reject(err);
      document.body.removeChild(scriptTag);
    };
    // 4. 挂载回调函数到 window
    window[callbackName] = (data) => {
      resolve(data);
      delete window[callbackName]; // 移除回调,避免内存泄漏
      document.body.removeChild(scriptTag);
    };
    // 5. 插入页面发起请求
    document.body.appendChild(scriptTag);
  });
}

// 使用示例
jsonp('https://api.other-domain.com/data', { name: '张三' })
.then(data => console.log(data))
.catch(err => console.log(err));

3. 后端实现(Node.js/Express 为例)

app.get('/data', (req, res) => {
  const callback = req.query.callback; // 获取前端传入的回调函数名
  const data = { code: 200, message: 'JSONP 跨域成功', data: { name: '李四' } };
  // 返回 JS 代码:回调函数包裹数据
  res.send(`${callback}(${JSON.stringify(data)})`);
});

4. 优点与适用场景

  • 优点:兼容旧浏览器(IE6+)、实现简单、无需后端复杂配置;
  • 缺点:仅支持 GET 方法、不安全(可能遭受 XSS 攻击)、无法捕获 404/500 错误;
  • 适用场景:兼容旧浏览器、第三方接口仅支持 JSONP(如部分老接口)。

四、方案 3:开发环境代理(Webpack/Vite/Vue CLI)—— 本地开发首选

前端开发时(如 Vue/React 项目),可通过构建工具的 代理服务器 转发跨域请求 —— 核心是 “前端请求本地代理服务器,代理服务器转发请求到目标跨域接口”,因代理服务器是后端服务,不受浏览器同源策略限制。

1. 原理

  1. 前端配置代理规则(如 /api 开头的请求转发到 https://api.other-domain.com);
  2. 前端发起请求 http://localhost:8080/api/data(同源,无跨域限制);
  3. 代理服务器接收请求,转发到 https://api.other-domain.com/data
  4. 代理服务器接收目标接口响应,返回给前端。

2. 不同构建工具的配置示例

(1)Vue CLI 配置(vue.config.js
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
  devServer: {
    proxy: {
      // 匹配所有以 /api 开头的请求
      '/api': {
        target: 'https://api.other-domain.com', // 目标跨域接口域名
        changeOrigin: true, // 开启跨域(修改请求头的 Host 为目标域名)
        pathRewrite: {
          '^/api': '' // 路径重写(如 /api/data → 目标接口 /data)
        },
        // 若目标接口是 https,需配置(可选)
        secure: false,
        // 支持 WebSocket(可选)
        ws: true
      }
    }
  }
});
(2)Vite 配置(vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'https://api.other-domain.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, '') // 路径重写
      }
    }
  }
};
(3)Webpack 配置(webpack.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'https://api.other-domain.com',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  }
};

3. 前端请求代码(无跨域感知)

// 无需考虑跨域,直接请求本地代理路径
axios.get('/api/data')
.then(res => console.log('代理跨域响应:', res.data));

// 等价于请求 https://api.other-domain.com/data

4. 优点与适用场景

  • 优点:配置简单、前端无感知(无需修改请求代码)、支持所有 HTTP 方法、开发环境专属(不影响生产);
  • 缺点:仅适用于开发环境(生产环境需用 Nginx 代理);
  • 适用场景:本地开发(前后端分离项目,目标接口未开放 CORS)。

五、方案 4:Nginx 反向代理 —— 生产环境部署首选

生产环境中,前端项目通常通过 Nginx 部署,此时可利用 Nginx 作为 反向代理服务器 转发跨域请求,原理与开发环境代理一致,但用于生产环境(稳定、高性能)。

1. 原理

  1. 前端项目部署在 Nginx 服务器(https://frontend-domain.com);
  2. 前端请求 https://frontend-domain.com/api/data(同源,无跨域);
  3. Nginx 接收请求,转发到目标跨域接口 https://api.other-domain.com/data
  4. Nginx 将目标接口响应返回给前端。

2. Nginx 配置示例(nginx.conf

server {
  listen 80;
  server_name frontend-domain.com; # 前端项目域名

  # 静态资源部署(前端打包后的 dist 目录)
  location / {
    root /usr/share/nginx/html; # 前端文件路径
    index index.html;
    try_files $uri $uri/ /index.html; # 解决 SPA 路由刷新 404
  }

  # 跨域代理配置(匹配 /api 开头的请求)
  location /api {
    proxy_pass https://api.other-domain.com; # 目标跨域接口域名
    proxy_set_header Host $host; # 传递 Host 头
    proxy_set_header X-Real-IP $remote_addr; # 传递真实 IP
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme; # 传递协议(http/https)
    
    # 路径重写(可选,若前端 /api 对应目标接口根路径)
    rewrite ^/api/(.*)$ /$1 break;
  }
}

3. 前端请求代码(与开发环境一致)

// 直接请求 Nginx 代理路径(同源)
axios.get('/api/data')
.then(res => console.log('Nginx 代理响应:', res.data));

4. 优点与适用场景

  • 优点:高性能、稳定可靠、支持所有 HTTP 方法、生产环境推荐、无需前端修改代码;
  • 缺点:需配置 Nginx 服务器(需运维支持);
  • 适用场景:生产环境部署(前端项目通过 Nginx 发布,目标接口未开放 CORS)。

六、方案 5:PostMessage —— 跨域页面通信(非 HTTP 请求)

postMessage 是 HTML5 新增 API,用于 两个跨域页面之间的通信(如 iframe 父子页面、不同域名的窗口),并非直接发起 HTTP 请求,但可间接实现跨域数据交互(如 iframe 内页面请求数据后,通过 postMessage 传给父页面)。

1. 原理

  1. 发送方页面调用 window.postMessage(data, targetOrigin) 发送数据,指定接收方域名;
  2. 接收方页面监听 message 事件,验证发送方域名(避免恶意攻击),接收数据。

2. 代码示例(父页面 + 跨域 iframe 页面)

(1)父页面(https://parent-domain.com
<!-- 嵌入跨域 iframe -->
<iframe id="crossIframe" src="https://child-domain.com/iframe.html" width="500" height="300"></iframe>

<script>
// 1. 向 iframe 发送消息(如请求数据)
const iframe = document.getElementById('crossIframe');
iframe.onload = () => {
  // 第二个参数指定接收方域名(* 表示允许所有域名,不安全,建议指定具体域名)
  iframe.contentWindow.postMessage({ type: 'GET_DATA', params: { name: '张三' } }, 'https://child-domain.com');
};

// 2. 监听 iframe 返回的消息(跨域数据)
window.addEventListener('message', (e) => {
  // 验证发送方域名(安全校验,必须加!)
  if (e.origin !== 'https://child-domain.com') return;
  
  console.log('父页面接收跨域数据:', e.data);
}, false);
</script>
(2)iframe 页面(https://child-domain.com/iframe.html
// 1. 监听父页面消息
window.addEventListener('message', (e) => {
  // 验证发送方域名
  if (e.origin !== 'https://parent-domain.com') return;
  
  // 接收父页面请求参数
  const { type, params } = e.data;
  if (type === 'GET_DATA') {
    // 2. 发起 HTTP 请求(iframe 与目标接口同源,无跨域)
    fetch('https://child-domain.com/api/data', {
      method: 'POST',
      body: JSON.stringify(params),
      headers: { 'Content-Type': 'application/json' }
    })
    .then(res => res.json())
    .then(data => {
      // 3. 将数据返回给父页面
      e.source.postMessage({ code: 200, data }, e.origin);
    });
  }
}, false);

3. 优点与适用场景

  • 优点:支持跨域页面通信、可传递复杂数据;
  • 缺点:需两个页面配合、仅适用于页面间交互(非直接 HTTP 请求);
  • 适用场景:跨域 iframe 通信、不同域名窗口间数据交互(如 OAuth 授权回调)。

七、方案 6:WebSocket —— 跨域实时通信

WebSocket 是 全双工实时通信协议,不受同源策略限制(握手阶段基于 HTTP,但连接建立后是独立协议),适用于实时通信场景(如聊天、实时数据推送)。

1. 原理

  1. 前端通过 new WebSocket('wss://api.other-domain.com/ws') 发起连接(握手阶段会携带 Origin 头);
  2. 后端验证 Origin 允许跨域后,建立 WebSocket 连接;
  3. 连接建立后,前后端可双向实时发送数据,无跨域限制。

2. 前端实现代码

// 建立 WebSocket 连接(wss 是加密协议,ws 是明文协议)
const ws = new WebSocket('wss://api.other-domain.com/ws');

// 连接成功回调
ws.onopen = () => {
  console.log('WebSocket 连接成功');
  // 发送数据给后端
  ws.send(JSON.stringify({ type: 'JOIN', userId: 123 }));
};

// 接收后端数据
ws.onmessage = (e) => {
  const data = JSON.parse(e.data);
  console.log('WebSocket 接收跨域数据:', data);
};

// 连接关闭回调
ws.onclose = (e) => {
  console.log('WebSocket 连接关闭:', e.code, e.reason);
};

// 错误回调
ws.onerror = (err) => {
  console.log('WebSocket 错误:', err);
};

3. 后端配置(Node.js + ws 库为例)

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  // 验证 Origin(允许跨域域名)
  const origin = req.headers.origin;
  const allowedOrigins = ['https://frontend-domain.com'];
  if (!allowedOrigins.includes(origin)) {
    ws.close(403, 'Origin not allowed');
    return;
  }

  // 接收前端数据
  ws.on('message', (message) => {
    console.log('收到前端消息:', message.toString());
    // 发送数据给前端
    ws.send(JSON.stringify({ type: 'MESSAGE', content: '实时推送数据' }));
  });
});

4. 优点与适用场景

  • 优点:实时性高、全双工通信、不受同源策略限制;
  • 缺点:需后端支持 WebSocket 协议、不适用于普通 HTTP 请求;
  • 适用场景:实时聊天、实时数据推送(如股票行情、监控数据)、协作工具。

八、6 种跨域方案对比与选型建议

方案支持方法浏览器兼容安全性适用场景核心优势
CORS所有现代浏览器生产环境(前后端可控)标准方案、支持复杂请求
JSONP仅 GET所有(含 IE)兼容旧浏览器、第三方老接口实现简单、无后端复杂配置
开发环境代理所有开发环境本地开发(Vue/React/Vite 项目)前端无感知、配置简单
Nginx 代理所有生产环境生产环境部署(Nginx 托管前端)高性能、稳定可靠
PostMessage-现代浏览器跨域页面 /iframe 通信页面间数据交互、无需 HTTP 请求
WebSocket-现代浏览器实时通信(聊天、实时推送)全双工、低延迟

选型优先级(按场景)

  1. 本地开发:开发环境代理(Webpack/Vite/Vue CLI);
  2. 生产环境(前后端可控) :CORS(首选)或 Nginx 代理;
  3. 生产环境(第三方接口无 CORS) :Nginx 代理;
  4. 兼容旧浏览器:JSONP;
  5. 跨域页面交互:PostMessage;
  6. 实时通信:WebSocket。

九、常见误区(避坑指南)

1. CORS 配置 Access-Control-Allow-Origin: * 仍跨域

  • 原因:带 Cookie 的跨域请求,Access-Control-Allow-Origin 不能为 *,需指定具体域名;
  • 解决:后端改为 Access-Control-Allow-Origin: 前端域名 + Access-Control-Allow-Credentials: true

2. 开发环境代理生效,生产环境跨域

  • 原因:生产环境未配置 Nginx 代理或 CORS,仅开发环境有代理;
  • 解决:生产环境部署 Nginx 代理,或让后端开启 CORS。

3. JSONP 无法发起 POST 请求

  • 原因:JSONP 基于 script 标签,仅支持 GET 方法;
  • 解决:改用 CORS 或代理方案。

4. PostMessage 未验证 origin 导致安全风险

  • 原因:未验证发送方域名,可能接收恶意网站的消息;
  • 解决:在 message 事件中添加 if (e.origin !== '允许的域名') return

总结

前端跨域的核心是 “避开浏览器同源策略限制”,选择方案时需优先考虑 场景适配性

  • 开发环境:优先用构建工具代理(简单高效);
  • 生产环境:优先用 CORS(标准)或 Nginx 代理(高性能);
  • 特殊场景:旧浏览器用 JSONP、页面通信用 PostMessage、实时通信用 WebSocket。

大多数前后端分离项目的最终选型是:开发环境代理 + 生产环境 CORS/Nginx 代理,兼顾开发效率和生产稳定性。