阅读视图

发现新文章,点击刷新页面。

跨越边界的艺术:现代 Web 开发跨域解决方案终极指南

一、跨域的本质:同源策略是什么?

想要解决跨域问题,首先要明白“跨域”从何而来。

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

预检请求 不满足简单请求条件的,就是预检请求。例如,使用 PUTDELETE 方法,或者 Content-Typeapplication/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 中进行简单的配置即可。

配置步骤:

  1. 打开项目根目录下的 vite.config.js 文件。
  2. 在 server 选项中添加 proxy 配置。
  3. 定义需要代理的路径前缀(如 /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:主线程与后台线程进行数据交换。

HTTP 演进史:每次升级都在解决什么痛点?

模块一:引言

HTTP(HyperText Transfer Protocol) 是基于 TCP/IP 的应用层协议,是互联网数据通信和网页传输的基石。

它采用请求-响应模式,是无状态协议,特别适合海量用户的高 并发访问场景。

HTTP 是一种请求-响应模式的协议:客户端发起请求,服务器返回响应。过程非常 简单,却极其强大——正是这种简洁性,让 HTTP 得以快速普及,成为互联网的"通用语言"。

同时,它也是无状态协议——服务器不会记得你上一次请求说了什么。每次请求都是 独立的。这种设计让它能够轻松应对海量并发访问,但也催生了 Session、Cookie、Token 等会话管理机制。

从 1991 年 HTTP 0.9 诞生至今,经历了多次重大升级。本文 将从历史演进的角度,带你系统回顾 HTTP 协议的发展脉络。

模块二:HTTP 0.9 和 HTTP 1.0 — 协议的诞生与初探

HTTP 0.9(1991年)— 一切的开始

1991年,万维网(World Wide Web)的发明者 Tim Berners-Lee 发布了 HTTP
的第一个版本。

这是一个极其简单的协议,只有一行命令:

GET /index.html

服务器直接返回 HTML 文档内容,传输完成就断开连接。

它的特点:

  • 仅支持 GET 方法
  • 无请求头、无响应头
  • 只能传输纯 HTML 文本
  • 每次请求都是一个新的 TCP 连接

现在的眼光看,HTTP 0.9 简陋得难以置信。但正是这个简单的开始,开启了互联网时代的大门。

HTTP 1.0(1996年)— 走向标准化

随着互联网蓬勃发展,HTTP 0.9 已经不够用了。1996年,HTTP 1.0 正式发布,协议开始走向标准化。

1. 多种请求方法

HTTP 1.0 扩展了请求方法:

  • GET — 从服务器读取资源
  • POST — 向服务器提交数据
  • HEAD — 仅获取响应头,不返回正文

POST 的出现意义重大——它让表单提交成为可能,Web 开始从"只读"走向"可写"。

2. 请求头(Headers)

HTTP 1.0 创新性地引入了请求头和响应头的概念,让客户端和服务器能够传递元数据。

常用请求头:

  • User-Agent — 客户端信息,比如浏览器版本、操作系统
  • Cookie — 会话标识,服务端下发的会话 ID
  • Content-Type — 请求体格式,比如 application/json
  • Accept — 可接受的响应类型,比如 text/html, */
  • Authorization — 认证信息,比如 Bearer token 或 Basic auth

User-Agent 是其中最有意思的一个。它的格式大致如下:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36

这一串标识的意思是:

image.png

  • Mozilla/5.0 最初是 Netscape 浏览器的标识,后来所有浏览器都兼容这个标识
  • (Macintosh; Intel Mac OS X 10_15_7) 告诉你操作系统和硬件信息
  • AppleWebKit/537.36 是渲染引擎
  • Chrome/146.0.0.0 是浏览器版本号
  • Safari/537.36 是为了兼容 Safari

这就是为什么国内早期要区分 PC 站和移动站——不同的 User-Agent 告诉服务器你用的是什么设备,服务器就返回不同的页面。

3. 短连接

HTTP 1.0 依然是短连接模式:每次请求都要先 TCP三次握手建立连接,请求完成后四次挥手断开连接,下一次请求再重新建连。

一个网页可能有几十个资源——HTML、CSS、JS、图片、字体……每个都要单独建立和断开 TCP 连接。这种方式在当时互联网规模还不大的时候勉强够用,但随着网页资源越来越多,性能问题开始凸显。

HTTP 1.0 为协议奠定了基本框架:确立了请求-响应模式,引入了 Headers
的概念,支持多种请求方法。 但它也有很多局限:短连接效率低、无状态导致会话管理困难、明文传输不安全…… 这些问题的解决方案,都留给了下一代的 HTTP 1.1。

模块四:HTTP 2.0 — 性能飞跃

HTTP 1.1 虽然大大提升了 Web的能力,但它的核心问题——对头阻塞——始终没有解决。

2015年,HTTP 2.0发布,专门针对这个痛点进行了底层重构。

1. 二进制分帧

HTTP 1.1 的数据是明文传输的,所有数据混在一起,没有边界,没有编号,想插队 是不可能的。

举个例子:

假设浏览器同时请求:

  - 流 1:index.html
  - 流 3:style.css
  - 流 5:app.js

  HTTP 2.0 的传输可能是这样的——帧交错在一起:

  流1帧头 → 流3帧头 → 流1数据 → 流5帧头 → 流3数据 → 流1数据 → ...

  到达接收端之后,按流 ID 分开重组:

  流1:帧1-1 + 帧1-2 + 帧1-3 → 拼成 index.html
  流3:帧3-1 + 帧3-2          → 拼成 style.css
  流5:帧5-1 + 帧5-2          → 拼成 app.js

为什么能解决对头阻塞?

因为每个流都有自己的 ID,独立重组。假设流 1 的某个帧丢了,只影响流1,流3和流5 照样传输、照样重组,完全不受影响。

2. 多路复用

基于二进制分帧,HTTP 2.0 实现了真正的多路复用(Multiplexing):

  • 一个 TCP 连接中可以并发多个请求
  • 每个请求都有一个独立的流 ID
  • 帧可以交错发送,按流 ID 归类重组

举个例子:

 浏览器要请求 index.html、style.css、app.js 三个资源。

  HTTP 1.1 时代:

  同一个域名只能开 1-6 个 TCP
  连接(浏览器限制),每个请求必须等上一个响应回来才能发下一个:

  TCP连接1:GET /index.html → 等 → 收到响应 → GET /style.css → 等 →        
  收到响应 → GET /app.js ...
  TCP连接2:GET /app.js     → 等 → 收到响应
  ...

  如果 index.html 卡住了,后面 style.css 和 app.js 只能在后面排队等。      

  HTTP 2.0 时代:

  一个 TCP 连接就够了。三个请求分属三个流,同时发送:

  一个 TCP 连接里:
  → 流1: GET /index.html
  → 流3: GET /style.css
  → 流5: GET /app.js
  ← 流1: 返回 index.html 数据帧
  ← 流3: 返回 style.css 数据帧
  ← 流5: 返回 app.js 数据帧

  三个流并行跑,互不等待,互不阻塞。

这从根本上解决了 HTTP 1.1 的对头阻塞问题

3. 服务器推送

传统的请求模式是:浏览器请求 HTML → 服务器返回 HTML → 浏览器解析 HTML
发现需要 CSS/JS → 再去请求 CSS/JS。

HTTP 2.0 支持服务器推送(Server Push):服务器知道浏览器需要什么资源,主动把 CSS/JS 推送给浏览器,浏览器还没请求就已经收到了。

这样就省去了浏览器反复请求的延迟。

4. 头部压缩

HTTP 2.0 还对 Header 进行了压缩。因为一个请求的 Header 往往有几百字节,而实际数据可能只有几字节,Header 的开销非常大。

HTTP 2.0 使用 HPACK 算法压缩 Header,进一步减少了传输量。


HTTP 2.0 的意义

HTTP 2.0 通过二进制分帧和多路复用,从根本上解决了 HTTP 1.1 的对头阻塞问题,让 Web 性能有了质的飞跃。

但它仍然有一个隐患——底层还是 TCP。TCP 在传输层也有对头阻塞问题,一旦丢包,整个 TCP 连接上的所有流都会受影响。

这个问题的最终解决方案,就是 HTTP 3.0。

模块五:HTTP 3.0 — QUIC 革命

HTTP 2.0 虽然解决了应用层的对头阻塞,但它的底层还是 TCP。TCP在传输层同样存在对头阻塞——一旦丢包,整个 TCP连接上的所有数据都要等待重传,所有流都被卡住。

HTTP 3.0 就是为了彻底解决这个问题。

HTTP 3.0 的核心是 QUIC(Quick UDP Internet Connections),一种基于 UDP的传输协议。

UDP 的特点是什么?无连接、不重传、不管顺序——简单粗暴,速度快,但不可靠。

QUIC

在 UDP 之上实现了自己的可靠传输逻辑,把 TCP 的优点移植过来,同时避免了
TCP 的缺点。

HTTP 3.0 的核心改进

1. 彻底抛弃 TCP,全程 UDP

HTTP 3.0 不再使用 TCP,而是直接基于 QUIC。没有 TCP 三次握手,改用 QUIC
自己的连接建立逻辑。

2. 每个流独立,不再互相影响

QUIC 把连接分成多个流(Stream)。丢包了?只影响当前流,其他流照常跑。这就 是真正的无对头阻塞。

3. 0-RTT 快速建立连接

第一次连接需要 1-RTT,之后可以做到 0-RTT——客户端直接发送数据,连接已经建立了。

4. 内置 TLS

HTTP 3.0 把加密直接做进了传输层,而不是像 HTTPS 那样单独跑一个 TLS
层。QUIC 本身就支持加密,而且比 TLS 更快。

一句话总结

HTTP 3.0 = HTTP 2.0 的所有特性 + QUIC(基于 UDP)= 彻底解决对头阻塞 +
更快建立连接 + 内置加密


模块六:总结

一部解决痛点的历史回顾 HTTP 的演进,有一条清晰的脉络:

 HTTP 0.9HTTP 1.0HTTP 1.1HTTP 2.0HTTP 3.0

每一次升级,都是为了解决上一代暴露出来的核心问题。


  • HTTP 0.9 太简陋,只能发 GET 请求,于是 HTTP 1.0 加入了 POST、HEAD、请求头和响应头。

  • HTTP 1.0 每次请求都要重新建连,效率太低,于是 HTTP 1.1 引入了长连接,多个请求可以复用同一个 TCP 连接。

  • HTTP 1.1 的管道化听起来很美,但响应没有编号,一个请求卡住后面全部排队,实际被浏览器弃用。

  • HTTP 2.0 用二进制分帧和流 ID 彻底解决了这个问题,一个 TCP 连接里可以并发多个请求,互不阻塞。

HTTP 2.0 看起来很完美了,但底层还是 TCP。TCP本身的传输层对头阻塞没有解决,一旦丢包,所有流都受影响。

  • HTTP 3.0 直接抛弃TCP,改用基于 UDP 的 QUIC,每个流独立可靠传输,彻底告别了对头阻塞。

各版本一句话定位

  • HTTP 0.9:一行 GET,一切的开端
  • HTTP 1.0:引入 Header,走向标准化
  • HTTP 1.1:长连接为主,但应用层对头阻塞无法根治
  • HTTP 2.0:二进制分帧 + 多路复用,从根本上解决对头阻塞
  • HTTP 3.0:基于 QUIC(UDP),彻底解决对头阻塞,更快、更安全

面试该怎么答

当面试官问起 HTTP 的演进时,不要只背区别,要讲清为什么需要这些升级。

比如被问到"HTTP 2.0 相比 1.1 有什么改进",可以这样答:

HTTP 1.1 虽然有长连接,但响应没有编号,存在对头阻塞问题。HTTP 2.0
通过二进制分帧和流 ID,把每个请求拆成带编号的帧,在同一个 TCP 连接里并发传输,按流 ID 重组,彻底解决了对头阻塞。

顺着"问题→解决方案→新问题→再解决"的逻辑讲下去

❌