一、跨域的本质:同源策略是什么?
想要解决跨域问题,首先要明白“跨域”从何而来。
1. 同源策略的定义
浏览器的同源策略(Same-Origin Policy) 是跨域的核心根源,它是浏览器最核心也最基本的安全功能。
所谓“同源”,要求两个页面的以下三点必须完全相同:
-
协议(http/https)
-
域名(包括主域名、子域名)
-
端口号(80/443/3000等)
只要三者有其一不同,就会被判定为“跨域”。此时,浏览器会限制非同源页面的以下行为:
- 读取非同源网页的 Cookie、LocalStorage、IndexedDB 等存储数据。
- 获取非同源网页的 DOM 元素。
- 向非同源地址发送 AJAX 请求(XMLHttpRequest/fetch)。
2. 为什么需要同源策略?
同源策略就像一道“防火墙”,从根本上限制了恶意网站的非法操作,保障了用户的信息安全。试想一下,如果没有同源策略:
- 恶意网站可以轻易读取你网银页面的 Cookie,盗取账户信息。
- 钓鱼网站可以嵌入真实的电商页面,篡改支付金额。
- 任意网站都能向你的服务器发送伪造请求,发起 CSRF 攻击。
3. 跨域的常见场景
日常开发中,跨域几乎无处不在:
-
前后端分离项目:前端运行在
localhost:5173,后端接口在 localhost:3000(端口不同)。
-
调用第三方接口:如支付、地图、天气等第三方服务(域名不同)。
-
多端协作:公司内部不同部门的系统对接(子域名不同)。
二、方案 1:JSONP——兼容性拉满的“老古董”
JSONP(JSON with Padding)是跨域方案中的“老前辈”,也是早期前端解决跨域最常用的方式,最大的优势是浏览器兼容性极好(甚至能兼容 IE6/7)。
1. JSONP 的核心原理
浏览器的同源策略限制了 AJAX 请求,但并没有限制 <script> 标签的 src 属性。<script> 可以加载任意域名的资源(比如 CDN 上的 jQuery)。
JSONP 正是利用这一“漏洞”实现跨域:
-
前端动态创建
<script> 标签,通过 src 向跨域接口发送请求,同时传递一个回调函数名。
-
后端接收到请求后,将数据包裹在回调函数中返回(即“JSON with Padding”)。
-
前端的回调函数被执行,从而拿到跨域数据。
2. JSONP 实战实现
前端代码(封装 JSONP 函数)
这段代码封装了一个返回 Promise 的 JSONP 函数,便于处理异步逻辑:
// 封装JSONP请求函数,返回Promise方便异步处理
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
// 1. 创建script标签
let script = document.createElement('script')
// 2. 定义全局回调函数,接收后端返回的数据
window[callback] = function(data) {
resolve(data) // 成功拿到数据,resolve Promise
document.body.removeChild(script) // 移除script标签,避免污染
}
// 3. 拼接请求参数(包含回调函数名)
params = { ...params, callback } // 比如:{wd: 'test', callback: 'show'}
let arrs = []
for (let key in params) {
arrs.push(`${key}=${params[key]}`)
}
// 4. 设置script的src属性,发送请求
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
// 5. 处理请求失败场景
script.onerror = function() {
reject(new Error('JSONP请求失败'))
document.body.removeChild(script)
}
})
}
// 调用JSONP请求
jsonp({
url: 'http://localhost:3000/say',
params: { wd: 'Iloveyou' },
callback: 'show'
}).then(data => {
console.log('JSONP请求结果:', data)
}).catch(err => {
console.error(err)
})
后端代码(Node.js 原生实现)
后端需要接收回调函数名,并将数据包裹在函数调用中返回:
const http = require('http');
const server = http.createServer((req, res) => {
// 匹配/say接口
if (req.url.startsWith('/say')) {
// 解析URL参数
const url = new URL(req.url, `http://${req.headers.host}`);
const callback = url.searchParams.get('callback'); // 获取回调函数名
// 设置响应头:返回JS脚本
res.writeHead(200, { 'Content-type': 'text/javascript' });
// 构造返回数据,包裹在回调函数中
const data = {
id: 1,
username: 'admin',
msg: 'JSONP请求成功'
}
// 核心:返回 "回调函数(数据)" 格式的JS代码
res.end(`${callback}(${JSON.stringify(data)})`);
} else {
res.writeHead(404);
res.end('Not Found')
}
})
server.listen(3000, () => {
console.log('JSONP服务器运行在 http://localhost:3000');
})
3. JSONP 的优缺点
表格
| 维度 |
详细说明 |
| ✅ 优点 |
兼容性极强:支持所有主流浏览器,包括低版本 IE。 实现简单:无需复杂的配置,前端后端少量代码即可完成。 |
| ❌ 缺点 |
仅支持 GET 请求:因为 <script> 标签的 src 只能发起 GET 请求。 安全风险:容易遭受 XSS 攻击(加载的脚本可能包含恶意代码),需确保请求的服务器是可信的。 性能问题**:额外加载的 <script> 标签会阻塞页面渲染,影响首屏加载速度。 |
4. 适用场景
仅推荐在兼容老旧浏览器(如需要支持 IE6/7)的场景下使用。在现代项目中,优先选择其他方案。
三、方案 2:CORS——现代跨域的主流之选
CORS(跨域资源共享,Cross-Origin Resource Sharing)是W3C制定的标准,也是目前解决跨域问题最主流、最推荐的方式。它本质上是在HTTP协议之上,通过增加特定的请求头和响应头,让浏览器和服务器协同工作,来判断一个跨域请求是否被允许。
1. CORS 的核心原理
CORS 的核心思想是:将跨域的控制权从浏览器完全转移到服务器。
1. 浏览器发起请求:当浏览器发起一个跨域请求时,它会自动在请求头中添加 Origin 字段,标明请求的来源(协议、域名、端口)。
2. 服务器决策:服务器接收到请求后,根据 Origin 字段的值来判断是否允许这个来源的请求。
3. 服务器响应:如果服务器允许该请求,它会在响应头中添加 Access-Control-Allow-Origin 字段,其值就是被允许的源。
4. 浏览器检查:浏览器收到响应后,会检查响应头中的 Access-Control-Allow-Origin 是否与请求的 Origin 匹配。如果匹配,则将响应数据返回给前端JS代码;如果不匹配,则浏览器会拦截响应,并在控制台抛出跨域错误。
2. 简单请求 vs 预检请求
CORS 将跨域请求分为两类:简单请求和预检请求。
简单请求 一个请求要成为简单请求,必须同时满足以下条件:
请求方法是以下之一:GET, HEAD, POST。
自定义请求头:除了浏览器自动设置的 Accept, Accept-Language, Content-Language, Content-Type 等,没有添加其他自定义请求头。
Content-Type 的值仅限于:application/x-www-form-urlencoded, multipart/form-data, text/plain。
对于简单请求,浏览器会直接发送,并在请求头中带上 Origin。
预检请求 不满足简单请求条件的,就是预检请求。例如,使用 PUT、DELETE 方法,或者 Content-Type 为 application/json,又或者添加了自定义请求头(如 token)。
对于预检请求,浏览器会先自动发起一个 OPTIONS 方法的请求(这就是“预检”),询问服务器是否允许当前的跨域请求。只有在服务器明确回复“允许”后,浏览器才会真正发起后续的请求。
3. CORS 实战实现
后端代码(Node.js + Express) 使用 Express 框架时,可以借助 cors 中间件轻松实现 CORS。
const express = require('express');
const cors = require('cors'); // 引入cors中间件
const app = express();
// 1. 允许所有来源的跨域请求 (最宽松的配置)
app.use(cors());
// 2. 或者,进行更精细的配置
const corsOptions = {
origin: 'http://localhost:5173', // 只允许这个源访问
methods: ['GET', 'POST', 'PUT'], // 允许的请求方法
allowedHeaders: ['Content-Type', 'Authorization'], // 允许的自定义请求头
credentials: true // 允许携带Cookie等凭证
};
app.use(cors(corsOptions));
// 定义一个需要跨域访问的接口
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS请求成功!', data: [1, 2, 3] });
});
app.listen(3000, () => {
console.log('CORS服务器运行在 http://localhost:3000');
});
后端代码(Node.js 原生实现) 如果不使用框架,也可以手动设置响应头。
const http = require('http');
const server = http.createServer((req, res) => {
// 设置允许跨域的源,* 表示允许所有源
res.setHeader('Access-Control-Allow-Origin', '*');
// 允许的请求方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// 允许的自定义请求头
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 允许携带凭证(如Cookie)
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 处理预检请求
if (req.method === 'OPTIONS') {
res.writeHead(204); // 204 No Content
res.end();
return;
}
// 处理其他业务请求
if (req.url === '/api/data') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'CORS请求成功!', data: [1, 2, 3] }));
}
});
server.listen(3000, () => {
console.log('CORS服务器运行在 http://localhost:3000');
});
4. 关键的 CORS 响应头
| 响应头 |
说明 |
Access-Control-Allow-Origin |
必需。指定允许访问的源,可以是 *(通配符)或具体的源(如 http://example.com)。 |
Access-Control-Allow-Methods |
预检请求必需。指定允许的请求方法,如 GET, POST, PUT。 |
Access-Control-Allow-Headers |
预检请求必需。指定允许的自定义请求头,如 Content-Type, Authorization。 |
Access-Control-Allow-Credentials |
可选。一个布尔值,表示是否允许浏览器发送 Cookie。如果为 true,则 Access-Control-Allow-Origin 不能为 *,必须是具体的源。 |
Access-Control-Max-Age |
可选。指定预检请求的缓存时间(秒),避免频繁发送 OPTIONS 请求。 |
5. CORS 的优缺点
| 维度 |
详细说明 |
| 优点 |
功能强大:支持所有类型的 HTTP 请求(GET, POST, PUT, DELETE 等)。安全性高:通过服务器精确控制允许的源、方法和请求头。现代标准:被所有现代浏览器支持,是前后端分离项目的最佳实践。 |
| 缺点 |
需要后端配合:必须在服务器端进行配置,前端无法单方面解决。配置复杂:对于需要携带凭证或复杂请求头的场景,配置相对繁琐。兼容性问题:不支持 IE9 及以下版本。 |
6. 适用场景
CORS 是现代 Web 开发中解决跨域问题的首选方案,尤其适用于前后端分离的架构。只要后端能够配合修改响应头,就应该优先使用 CORS。
四、方案 3:反向代理——“曲线救国”的万能钥匙
反向代理是开发环境中解决跨域问题最常用、也最省心的方法之一。它的核心思想是 “曲线救国” :既然浏览器禁止前端直接访问后端接口,那我们就让前端请求一个和自己“同源”的代理服务器,再由这个代理服务器去请求真正的后端接口。
1. 反向代理的核心原理
1.
前端请求代理:前端应用(如运行在 localhost:5173)不再直接请求后端接口(如 localhost:3000/api),而是请求一个与自己同源的代理地址(如 localhost:5173/api)。因为源相同,所以不会触发浏览器的跨域限制。
2.
代理服务器转发:这个代理服务器(通常是开发服务器或 Nginx)收到请求后,会以自己的身份向真正的后端接口(localhost:3000/api)发起请求。这个请求是服务器与服务器之间的通信,不受浏览器同源策略的限制。
3. 代理服务器返回:代理服务器拿到后端接口的响应数据后,再原封不动地返回给前端。
4.
通过这种方式,前端巧妙地绕过了浏览器的跨域限制,实现了数据的获取。
2. 反向代理实战实现
反向代理的实现方式多种多样,从开发环境的配置到生产环境的部署,都有其身影。
开发环境:Vite/Webpack 配置 在现代前端框架(如 Vue, React)的开发环境中,我们通常使用 Vite 或 Webpack 作为开发服务器。它们都内置了强大的代理功能,只需几行配置即可解决跨域问题。
Vite 配置示例 ( vite.config.js )
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
// 配置代理规则
proxy: {
// 当前端请求 /api 路径时,触发代理
'/api': {
target: 'http://localhost:3000', // 代理的目标服务器地址
changeOrigin: true, // 修改请求头中的 Origin 为目标服务器的 Origin
// rewrite: (path) => path.replace(/^/api/, '') // 可选:重写路径,去掉 /api 前缀
}
}
}
})
配置完成后,前端请求 http://localhost:5173/api/data,开发服务器会自动将其转发到 http://localhost:3000/api/data。
生产环境:Nginx 配置 在项目部署到生产环境时,Nginx 是最常用的反向代理服务器。它不仅能处理跨域,还能提供负载均衡、静态资源服务、缓存等多种功能。
Nginx 配置示例 ( nginx.conf )
server {
listen 80;
server_name localhost; # 或者你的域名
# 1. 配置前端静态文件
location / {
root /usr/share/nginx/html; # 前端打包文件的路径
index index.html index.htm;
try_files $uri $uri/ /index.html; # 解决前端路由 history 模式刷新404的问题
}
# 2. 配置后端接口代理
location /api/ {
proxy_pass http://backend_server:3000/; # 后端服务器的地址
proxy_set_header Host $host; # 传递原始主机头
proxy_set_header X-Real-IP $remote_addr; # 传递用户真实IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这样配置后,无论是前端页面还是 /api/ 开头的接口请求,都由同一个 Nginx 服务器处理,完美规避了跨域问题。
3. 反向代理的优缺点
| 维度 |
详细说明 |
| ** 优点** |
前端无感:前端代码无需任何修改,完全不用关心跨域问题。功能强大:除了跨域,还能实现负载均衡、请求/响应拦截、日志记录等。通用性强:适用于任何类型的请求,不受请求方法和请求头的限制。 |
| ** 缺点** |
增加服务器成本:需要额外部署和维护一台代理服务器(如 Nginx)。配置相对复杂:相比 CORS,反向代理的配置(尤其是 Nginx)需要一定的运维知识。可能增加延迟:请求多了一次转发,理论上会增加一点点网络延迟。 |
4. 适用场景
● 开发环境:强烈推荐使用 Vite/Webpack 的代理功能,是开发阶段解决跨域问题的首选。
● 生产环境:当你无法控制后端服务器(例如调用第三方API),或者后端团队不方便配合配置 CORS 时,使用 Nginx 反向代理是最佳选择。它也是微服务架构中 API 网关的雏形。
五、方案 4:WebSocket——实时通信的“特权通道”
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它最大的特点是不受同源策略的限制,这使得它在需要实时双向通信的场景下,成为了一个天然的跨域解决方案。
1. WebSocket 的核心原理
WebSocket 的工作流程可以分为三个阶段:
1. 握手阶段:前端通过 JavaScript 创建一个 WebSocket 对象,浏览器会向服务器发起一个特殊的 HTTP 请求。这个请求头中包含 Upgrade: websocket 字段,表示希望将协议从 HTTP 升级到 WebSocket。
2. 协议升级:服务器收到请求后,如果支持 WebSocket,会返回一个状态码为 101 (Switching Protocols) 的响应,同意协议升级。
3. 数据传输:一旦握手成功,客户端和服务器之间就建立了一条持久的 TCP 连接。此后,双方可以随时主动向对方推送数据,而无需像 HTTP 那样由客户端反复发起请求。
正是因为 WebSocket 在握手成功后就脱离了 HTTP 协议的范畴,建立了一条独立的“管道”,所以浏览器不会对其应用同源策略的限制。
2. WebSocket 实战实现
前端代码 前端使用非常简单,只需几行代码即可建立连接并监听事件。
// 1. 创建 WebSocket 连接,传入服务器地址
// 注意:协议是 ws:// (或 wss:// 用于加密连接)
const socket = new WebSocket('ws://localhost:3000');
// 2. 监听连接成功事件
socket.onopen = function(event) {
console.log('WebSocket 连接已建立');
// 连接成功后,可以立即向服务器发送数据
socket.send(JSON.stringify({ type: 'init', data: 'Hello Server!' }));
};
// 3. 监听服务器发来的消息
socket.onmessage = function(event) {
console.log('收到服务器消息:', event.data);
const data = JSON.parse(event.data);
// 根据消息类型处理数据
if (data.type === 'notification') {
alert(data.message);
}
};
// 4. 监听连接关闭事件
socket.onclose = function(event) {
console.log('WebSocket 连接已关闭');
};
// 5. 监听连接错误事件
socket.onerror = function(event) {
console.error('WebSocket 发生错误:', event);
};
// 随时可以向服务器发送数据
function sendMsg() {
socket.send('这是一条新消息');
}
后端代码(Node.js + ws 库) 后端可以使用 ws 这个轻量级的 WebSocket 库来快速搭建服务器。
const WebSocket = require('ws');
// 创建一个 WebSocket 服务器,监听 3000 端口
const wss = new WebSocket.Server({ port: 3000 });
// 监听客户端连接事件
wss.on('connection', (ws) => {
console.log('有客户端连接进来了');
// 向当前连接的客户端发送欢迎消息
ws.send(JSON.stringify({ type: 'welcome', message: '欢迎连接到 WebSocket 服务器' }));
// 监听当前客户端发来的消息
ws.on('message', (data) => {
console.log('收到客户端消息:', data.toString());
// 可以将消息广播给所有连接的客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(`广播: ${data}`);
}
});
});
// 监听客户端断开连接
ws.on('close', () => {
console.log('客户端连接已关闭');
});
});
console.log('WebSocket 服务器运行在 ws://localhost:3000');
3. WebSocket 的优缺点
| 维度 |
详细说明 |
| 优点 |
天然跨域:不受同源策略限制,无需额外配置。实时双向通信:服务器可以主动向客户端推送数据,延迟极低。持久连接:只需一次握手,即可保持长时间通信,减少了 HTTP 反复建立连接的开销。 |
| 缺点 |
协议不同:需要服务器和客户端都支持 WebSocket 协议,不适用于传统的 HTTP 请求场景。兼容性问题:不支持 IE9 及以下版本。连接管理复杂:需要处理连接的建立、维持、断开和重连,比简单的 HTTP 请求更复杂。 |
4. 适用场景
WebSocket 专为实时性要求高的场景而生,例如:
● 在线聊天/即时通讯:如微信网页版、在线客服。
● 实时数据推送:如股票行情、体育比赛比分、新闻快讯。
● 协同编辑:如在线文档、代码编辑器。
● 多人在线游戏:需要实时同步玩家状态。
六、方案 5:Vite 反向代理 —— 本地开发的 “最优解”
在前端本地开发阶段,我们经常会遇到这样的场景:前端项目运行在 http://localhost:5173,而后端接口运行在 http://localhost:3000。虽然这只是开发环境下的端口不同,但在浏览器看来这就是“跨域”。
虽然可以通过后端配置 CORS 来解决,但在开发阶段,更优雅、更安全的方式是利用开发服务器(如 Vite、Webpack)进行反向代理。这种方式不需要后端做任何改动,完全由前端工具链来处理跨域问题。
1. Vite 代理的核心原理
Vite 代理的本质是利用了开发服务器(Dev Server)作为“中间人”。
-
前端视角:前端代码发起请求时,目标地址是 Vite 服务器(例如
/api/user)。
-
同源策略豁免:因为 Vite 服务器就是提供前端页面的服务端,所以前端请求
/api/user 属于“同源请求”,浏览器不会拦截。
-
服务器转发:Vite 服务器接收到请求后,发现这是一个代理请求,于是它会以“服务器身份”向真正的后端接口(例如
http://localhost:3000/api/user)发起请求。
-
响应返回:后端接口将数据返回给 Vite 服务器,Vite 再将数据返回给前端浏览器。
关键点:浏览器与 Vite 服务器之间是同源的(无跨域);Vite 服务器与后端服务器之间是服务器间的通信(不受浏览器同源策略限制)。通过这种“曲线救国”的方式,完美绕过了浏览器的跨域限制。
2. Vite 代理实战配置
Vite 内置了强大的代理功能,基于 http-proxy 中间件实现。我们只需要在 vite.config.js 中进行简单的配置即可。
配置步骤:
- 打开项目根目录下的
vite.config.js 文件。
- 在
server 选项中添加 proxy 配置。
- 定义需要代理的路径前缀(如
/api)。
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
// 配置代理规则
proxy: {
// 1. 定义代理前缀
// 当请求路径以 '/api' 开头时,触发代理
'/api': {
// 2. 目标服务器地址
// 这里填写后端接口的真实地址
target: 'http://localhost:3000',
// 3. 是否改变请求头中的 Origin
// 设置为 true 时,Vite 会将请求头的 Host 改为目标服务器的 Host
// 避免后端因为 Origin 校验不通过而拒绝请求
changeOrigin: true,
// 4. 路径重写 (可选)
// 如果后端不需要 '/api' 这个前缀,可以将其重写为空
// 例如:前端请求 '/api/user' -> 后端接收 '/user'
rewrite: (path) => path.replace(/^/api/, '')
},
// 5. 多个代理配置 (可选)
// 如果有多个不同的后端服务,可以继续添加
'/upload': {
target: 'http://upload-server.com',
changeOrigin: true
}
}
}
})
3. 配置项详解
表格
| 配置项 |
类型 |
说明 |
target |
String |
必需。你要代理到的目标地址,即后端接口的真实域名或 IP。 |
changeOrigin |
Boolean |
推荐开启。设为 true 时,会自动修改请求头中的 host 为 target 的值。很多后端框架(如 Nginx、Java Spring)会校验 host,不开启可能导致 403/404 错误。 |
rewrite |
Function |
可选。用于重写请求路径。例如,如果后端接口不需要前端定义的前缀(如 /api),可以用此函数将其替换或删除。 |
secure |
Boolean |
如果目标是 https 接口,设为 false 可以忽略 HTTPS 证书校验(开发环境常用)。 |
4. 优缺点分析
优点:
-
开发环境专用神器:无需后端配合,前端开发者自己就能搞定,不影响生产环境配置。
-
无跨域风险:完全在开发服务器层面处理,浏览器根本感知不到跨域的存在。
-
配置极其简单:Vite 内置功能,几行代码即可完成,且支持 TypeScript 配置。
-
支持 WebSocket:Vite 代理也支持代理 WebSocket 连接(配置
ws: true)。
缺点:
-
仅限开发环境:Vite 代理只在
vite dev 启动的开发服务器中生效。项目打包上线后,Vite 服务器不再运行,代理配置也随之失效。
-
无法解决生产环境问题:它只是一个开发时的“模拟器”,不能用于解决线上环境的跨域问题。
5. 适用场景
-
前端本地开发:这是该方案的唯一且最佳场景。
-
接口联调阶段:在后端尚未部署或无法修改响应头时,前端通过代理快速进行联调。
-
Mock 数据切换:配合环境变量,可以在代理真实接口和本地 Mock 服务之间灵活切换。
八、方案 7:postMessage —— 跨域通信的“万能信使”
前文提到的 JSONP、CORS、反向代理等方案,主要解决的是“浏览器向服务器请求数据”的跨域问题。但在现代 Web 开发中,我们经常会遇到“页面与页面”、“窗口与窗口”之间需要通信的场景,例如:
- 父页面与嵌入的跨域
iframe 进行数据交互。
- 主窗口与通过
window.open() 打开的跨域子窗口同步状态。
- 主线程与 Web Worker 之间传递消息。
对于这些场景,postMessage 是 HTML5 提供的标准解决方案,它就像一个“万能信使”,允许不同源的窗口之间安全地进行双向通信。
1. postMessage 的核心原理
postMessage 的核心思想是 “消息传递” 而非“直接访问”。它打破了浏览器的同源策略,但并没有完全移除安全限制。
-
发送方:通过
targetWindow.postMessage(message, targetOrigin) 方法,向目标窗口发送一条结构化的数据消息。
-
接收方:通过监听
window 对象的 message 事件,来捕获并处理来自其他窗口的消息。
-
安全校验:整个过程通过
targetOrigin(发送时指定目标源)和 event.origin(接收时检查消息来源)进行双重安全校验,确保消息只在你信任的窗口之间传递。
2. postMessage 实战实现
我们以最常见的 父页面与跨域 iframe 通信 为例,展示如何实现双向通信。
父页面 (parent.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>父页面</title>
</head>
<body>
<h1>我是父页面</h1>
<!-- 嵌入一个跨域的 iframe -->
<iframe id="childFrame" src="https://iframe-example.com/child.html"></iframe>
<script>
const iframe = document.getElementById('childFrame');
// 1. 向 iframe 发送消息
// 注意:必须等待 iframe 加载完成后再发送
iframe.onload = () => {
const data = { type: 'GREETING', text: 'Hello from parent!' };
// 精确指定目标源,这是安全的关键!
iframe.contentWindow.postMessage(data, 'https://iframe-example.com');
};
// 2. 监听来自 iframe 的消息
window.addEventListener('message', (event) => {
// 【安全红线】必须校验消息来源!
if (event.origin !== 'https://iframe-example.com') {
console.warn('收到来自非法源的消息,已忽略');
return;
}
console.log('父页面收到消息:', event.data);
});
</script>
</body>
</html>
子页面 (child.html)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>子页面 (iframe)</title>
</head>
<body>
<h1>我是子页面 (iframe)</h1>
<script>
// 1. 监听来自父页面的消息
window.addEventListener('message', (event) => {
// 【安全红线】同样必须校验消息来源!
if (event.origin !== 'https://parent-example.com') {
return;
}
console.log('子页面收到消息:', event.data);
// 2. 向父页面回复消息
const replyData = { type: 'REPLY', text: 'Hello back!' };
// event.source 是发送消息的窗口对象的引用
event.source.postMessage(replyData, event.origin);
});
</script>
</body>
</html>
3. API 详解
发送消息:targetWindow.postMessage(message, targetOrigin)
表格
| 参数 |
类型 |
说明 |
message |
任意类型 |
要发送的数据。可以是字符串、对象、数组等,数据会被浏览器使用“结构化克隆算法”进行序列化。 |
targetOrigin |
String |
安全关键! 指定接收消息的窗口的源(协议+域名+端口)。必须精确指定,严禁在生产环境使用通配符 '*' ,否则可能导致敏感数据泄露给恶意网站。 |
接收消息:window.addEventListener('message', callback)
回调函数接收一个 MessageEvent 对象,其中包含三个关键属性:
表格
| 属性 |
类型 |
说明 |
event.data |
任意类型 |
发送方传递的实际消息数据。 |
event.origin |
String |
安全关键! 发送消息的窗口的源。接收方必须校验此属性,确保消息来自可信源。 |
event.source |
Window 对象 |
发送消息的窗口对象的引用。可用于向发送方回传消息,实现双向通信。 |
4. 优缺点分析
优点:
-
功能强大:解决了页面间通信的跨域问题,这是 CORS 和代理无法做到的。
-
双向通信:支持父子窗口、主副窗口之间的双向消息传递。
-
安全性高:通过
targetOrigin 和 event.origin 的双重校验,可以有效防止恶意攻击。
-
数据灵活:支持传递复杂的结构化数据。
缺点:
-
使用场景特定:仅适用于窗口间通信,不适用于常规的 AJAX 请求。
-
安全要求高:开发者必须手动进行源校验,任何疏忽都可能导致严重的安全漏洞(如 XSS)。
-
异步通信:基于事件模型,处理复杂交互时逻辑可能变得分散。
5. 适用场景
-
第三方组件集成:如嵌入支付宝/微信支付的
iframe,支付完成后通知父页面。
-
跨域单点登录(SSO) :通过一个中央登录页,使用
postMessage 将登录令牌传递给其他域名的应用。
-
微前端架构:主应用与子应用之间进行状态同步和事件通知。
-
多窗口协作:如在线协作文档,主窗口打开多个编辑窗口,并同步光标位置和编辑内容。
-
Web Worker:主线程与后台线程进行数据交换。