前端与后端通信的几种方式
Category(分类): Browser, HTML, Network Status: 已更新
原文从 AJAX、XHR、Fetch、WebSocket、Form、JSON/XML、REST、GraphQL、Socket.IO、SSE、消息队列和 CORS 介绍前后端交互。本文保留这些主题和历史脉络,但把已经不推荐的同步 XHR、
vue-resource、过时示例域名、错误的“WebSocket 没有同源限制”和“CORS 能阻止请求”等说法改为当前 Web 平台的表述。
一、先按通信特征选择方案
| 需求 | 常用方案 | 方向和特点 |
|---|---|---|
| 普通查询、提交、上传下载 | fetch() | HTTP 请求-响应,现代浏览器首选 |
| 需要上传进度或老项目兼容 | XMLHttpRequest | HTTP 请求-响应,支持进度事件 |
| 原生表单提交 | <form> | 浏览器原生导航或提交,简单可靠 |
| 服务器持续推送文本事件 | SSE / EventSource | 单向:服务器 → 浏览器,自动重连 |
| 双向低延迟消息 | WebSocket | 客户端 ↔ 服务器,需自定义鉴权和消息协议 |
| QUIC 上的可靠流/不可靠数据报 | WebTransport | 更底层、更复杂,支持情况和服务端要求更高 |
| 浏览器之间点对点通信 | WebRTC DataChannel | 通常仍需要信令服务,不是普通后端 API 替代品 |
| 关闭页面时发送少量分析数据 | navigator.sendBeacon() | 异步 POST,适合小型统计数据 |
| 简单实时但不需要长连接 | 轮询/长轮询 | 实现容易,但要处理频率、超时和重复请求 |
REST、GraphQL 和 Socket.IO 主要是 API 风格或库/协议栈,不应与 HTTP、WebSocket 这些底层传输能力混为一谈。RabbitMQ、Kafka 等消息队列通常位于服务端内部,浏览器一般不直接连接它们。
二、HTTP 请求-响应模型
前端和后端最常见的通信基于 HTTP:浏览器发送请求,服务器返回响应。HTTP 是无状态的应用层协议,状态通常由 Cookie、服务端会话、Token 或数据库等应用机制维护。

一次 HTTP 交互包含:
- 请求方法:
GET、POST、PUT、PATCH、DELETE、OPTIONS等; - URL 和查询参数;
- 请求头:
Accept、Content-Type、Authorization、Origin等; - 可选请求体:JSON、表单、文件或二进制数据;
- 响应状态码、响应头和响应体。

HTTP/2 和 HTTP/3 可以在一个连接上复用多个流,不能再把“每个请求都新建一个 TCP 连接”当成固定流程。浏览器的缓存、Service Worker、代理、连接复用和重定向也可能改变实际网络过程。
三、AJAX:一种历史称呼,不是一种协议
AJAX 是 Asynchronous JavaScript and XML 的缩写,最初强调使用 JavaScript 异步请求 XML 并更新页面局部内容。今天 AJAX 通常泛指“在页面不整体导航的情况下用 JavaScript 发 HTTP 请求”,数据格式早已不限于 XML,JSON 更常见。

1. XMLHttpRequest
XHR 仍然适合需要上传/下载进度、老浏览器兼容或已有封装的项目。不要使用原文中的同步 XHR:xhr.open('GET', url, false) 会阻塞页面主线程,现代浏览器已不鼓励在 Window 中使用,部分场景会抛出异常或被限制。
function requestJson(url, options = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(options.method || 'GET', url, true)
xhr.responseType = 'json'
for (const [name, value] of Object.entries(options.headers || {})) {
xhr.setRequestHeader(name, value)
}
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response)
} else {
reject(new Error(`HTTP ${xhr.status}`))
}
})
xhr.addEventListener('error', () => reject(new Error('Network error')))
xhr.addEventListener('abort', () => reject(new DOMException('Aborted', 'AbortError')))
xhr.send(options.body ?? null)
})
}
requestJson('/api/repos')
.then(data => console.log(data))
.catch(error => console.error(error))
如果要观察上传进度:
const xhr = new XMLHttpRequest()
xhr.open('POST', '/api/upload')
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
console.log(`${(event.loaded / event.total * 100).toFixed(1)}%`)
}
})
xhr.send(file)
XHR 的 status 通常表示最终 HTTP 响应;资源命中浏览器缓存时,不要把 304 当成业务成功的唯一判断条件。应用层应该根据 2xx、响应数据和错误协议处理结果。
2. jQuery、vue-resource 和 Axios
jQuery 的 $.ajax() 和 Axios 都是对 XHR/HTTP 能力的封装。它们可以统一拦截器、超时、序列化和错误处理,但不改变浏览器的同源策略、CORS 或 HTTP 语义。
import axios from 'axios'
const api = axios.create({
baseURL: '/api',
timeout: 10_000,
headers: { Accept: 'application/json' }
})
const { data } = await api.get('/repos')
console.log(data)
vue-resource 是 Vue 1.x 时代常见的插件,现有项目可能仍能看到它,但新项目不应因为历史文章就选用它。原文中直接引用的 cdn.bootcdn.net、Axios beta 版本和 echo.websocket.org 示例也不应作为生产依赖;生产环境应锁定版本并使用项目自己的依赖管理。
JSONP 也是早期跨域方案:通过插入 <script> 请求一个只支持 GET 的接口,再让服务器返回函数调用。它不支持安全的任意 HTTP 方法和标准响应读取,并且把远端响应当作脚本执行,供应链或接口被污染时会直接造成代码执行。现代项目应优先使用 CORS;只有维护历史系统时才保留 JSONP,并严格限制可信域名和返回内容。
四、Fetch:现代 HTTP API
Fetch API 使用 Request、Response、Headers 和可读流抽象网络请求,浏览器和 Worker 都可以使用。Fetch 的一个关键区别是:HTTP 404/500 不会让 Promise 自动 reject,只有网络错误、请求被中止等异常通常才会 reject,因此必须检查 response.ok。

const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10_000)
try {
const response = await fetch('/api/repos', {
method: 'GET',
headers: { Accept: 'application/json' },
credentials: 'same-origin',
signal: controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const data = await response.json()
console.log(data)
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求超时或被取消')
} else {
console.error('请求失败', error)
}
} finally {
clearTimeout(timeoutId)
}
1. Fetch 常见注意事项
response.json()、text()、blob()等会消费响应体,一个响应体通常只能读取一次;- 大响应可以使用
response.body的ReadableStream逐块读取; fetch默认对跨源请求使用credentials: "same-origin",跨源 Cookie 需要显式credentials: "include",且服务端必须正确配置 CORS 和 Cookie;mode: "no-cors"得到的是受限的 opaque response,不能用来读取跨源 JSON,不是解决 CORS 的办法;- 上传
FormData时不要手动设置Content-Type,浏览器需要自动生成 multipart boundary; cache、redirect、referrerPolicy、keepalive等选项改变的是请求行为,不是绕过服务器权限。
2. JSON 请求和文件上传
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify({ name: 'Ada' })
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const form = document.querySelector('#upload-form')
const formData = new FormData(form)
const uploadResponse = await fetch('/api/upload', {
method: 'POST',
body: formData
})
五、WebSocket:双向长连接
WebSocket 在一次 HTTP(S) 握手后升级为消息型的全双工连接。它适合聊天室、协同编辑、实时状态和游戏信令等需要服务器主动推送且客户端也频繁发送消息的场景。

const socket = new WebSocket('wss://example.com/realtime', ['chat.v1'])
socket.addEventListener('open', () => {
socket.send(JSON.stringify({ type: 'subscribe', topic: 'orders' }))
})
socket.addEventListener('message', event => {
try {
const message = JSON.parse(event.data)
console.log('server message:', message)
} catch {
console.error('忽略格式错误的消息')
}
})
socket.addEventListener('error', () => {
console.error('WebSocket error')
})
socket.addEventListener('close', event => {
console.log('closed:', event.code, event.reason)
})
ws:// 是明文 WebSocket,wss:// 是 TLS 加密 WebSocket,默认端口通常分别对应 80 和 443,但可以显式指定其他端口。生产环境优先使用 wss://。
原文说 WebSocket“没有同源限制,可以任意与服务器通信”不准确:浏览器允许脚本构造跨源 WebSocket URL,但握手会携带 Origin,服务端必须验证来源、认证用户并决定是否接受。WebSocket 不使用 Fetch/CORS 的响应读取流程,但不代表服务端自动获得跨源安全性;Cookie 也可能随握手发送,必须防范跨站 WebSocket 劫持。
服务端还应:
- 使用短期认证信息或握手鉴权,不把密码放进 URL;
- 对
Origin使用严格 allowlist; - 校验每条消息的结构、权限和大小;
- 限制速率和连接数,处理心跳、断线和重连退避;
- 根据
bufferedAmount观察发送队列,避免无限制发送造成内存增长; - 设计协议版本、幂等消息 ID 和重复消息处理。
六、Server-Sent Events(SSE)
SSE 使用 HTTP 长连接向浏览器持续发送文本事件,是服务器到客户端的单向通道。EventSource 支持自动重连和 Last-Event-ID 机制,适合通知、进度、日志和实时 feed;如果客户端也需要频繁向服务器发送消息,通常组合普通 Fetch 或选择 WebSocket。

const events = new EventSource('/api/events')
events.addEventListener('message', event => {
console.log('default event:', event.data)
})
events.addEventListener('order-updated', event => {
const data = JSON.parse(event.data)
console.log('order updated:', data)
})
events.addEventListener('error', event => {
console.error('SSE connection error', event)
})
// 页面不再需要时主动关闭,避免连接长期占用。
// events.close()
服务端响应至少需要:
Content-Type: text/event-stream
Cache-Control: no-cache
HTTP/1.1 服务可以使用持久连接;HTTP/2/HTTP/3 不应发送逐跳的 Connection 头,连接复用由协议本身处理。服务端还应定期发送注释或心跳,避免代理把空闲流关闭。
每条事件以空行结束:
event: order-updated
id: 42
data: {"status":"paid"}
SSE 传输的是 UTF-8 文本,不是任意二进制消息。跨源 SSE 需要服务端 CORS;带 Cookie 时客户端使用 { withCredentials: true },服务端不能把 Access-Control-Allow-Origin 写成 *。
七、WebTransport 和 WebRTC:更专业的实时能力
1. WebTransport
WebTransport 建立在 HTTP/3/QUIC 之上,提供可靠的双向流和不可靠数据报,适合对低延迟、多路复用或丢包容忍有特殊要求的应用。它需要 HTTPS、支持 WebTransport 的 HTTP/3 服务端和合适的浏览器版本:
const transport = new WebTransport('https://example.com:4999/transport')
await transport.ready
const writer = transport.datagrams.writable.getWriter()
await writer.write(new Uint8Array([1, 2, 3]))
await transport.closed
数据报不保证到达或有序;可靠流才适合必须完整到达的消息。WebTransport API、服务端部署和网络环境仍比 Fetch/WebSocket 复杂,不能因为“基于 QUIC”就默认性能更好。
2. WebRTC DataChannel
WebRTC 的 DataChannel 可以让浏览器之间建立加密的点对点数据通道,适合音视频协作、文件传输和实时游戏。它通常需要服务端信令交换 SDP/ICE 信息,STUN/TURN 也可能参与连接建立,因此不是简单替换 WebSocket 的后端 API。
八、表单:最朴素但可靠的提交方式
<form> 的默认行为是浏览器导航到响应页面。表单可以使用原生校验、键盘提交、可访问的 label 和浏览器密码管理能力:
<form action="/api/subscribe" method="post">
<label>
邮箱
<input name="email" type="email" autocomplete="email" required>
</label>
<button type="submit">订阅</button>
</form>
method="get"会把成功控件拼到 URL 查询参数中,适合无副作用查询;method="post"默认使用application/x-www-form-urlencoded请求体;- 文件上传需要
enctype="multipart/form-data"; - 控件必须有
name才会进入提交数据; action可被提交按钮上的formaction覆盖;novalidate会关闭浏览器原生校验,应谨慎使用。
不刷新页面的提交可以监听 submit,但仍要保留可访问的表单语义:
const form = document.querySelector('#profile-form')
form.addEventListener('submit', async event => {
event.preventDefault()
if (!form.reportValidity()) return
const method = (form.method || 'get').toUpperCase()
const url = new URL(form.action, location.href)
const requestInit = { method }
const formData = new FormData(form)
if (method === 'GET' || method === 'HEAD') {
url.search = new URLSearchParams(formData).toString()
} else {
requestInit.body = formData
}
const response = await fetch(url, requestInit)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
})
如果是 GET 表单,不应把 FormData 直接作为 GET body;可以使用 new URLSearchParams(new FormData(form)) 拼到 URL 中。
九、数据格式:JSON、XML、表单和二进制
1. JSON

JSON 结构简单、跨语言支持广泛,适合大多数 REST/Fetch API:
{
"id": 42,
"name": "Ada",
"tags": ["web", "api"],
"active": true
}
JSON 不直接表示 Date、Map、Set、BigInt、undefined 和循环引用。接口应约定日期格式、数字精度、空值语义和错误结构,不要把 JavaScript 对象可以序列化等同于跨语言完全一致。
2. XML

XML 能表达复杂层级、命名空间和模式,在 RSS、SOAP、部分企业系统中仍然存在。新 API 不应因为文章历史就强行选择 XML,也不能假设 XML 自动比 JSON 更安全;服务端仍需防范 XML 外部实体等解析风险,并限制输入大小。
3. FormData、文本和二进制
application/x-www-form-urlencoded:简单键值表单;multipart/form-data:文件和字段混合上传;text/plain:调试或极简接口;Blob、ArrayBuffer、TypedArray:图片、音视频和二进制协议;- NDJSON/流式文本:逐行处理大批量数据,但需要服务端明确协议。
数据格式由接口契约决定,不能只因为 JSON 流行就把文件上传、流式音视频和二进制协议都编码成巨大 JSON。
十、REST、GraphQL、Socket.IO 和消息队列
1. RESTful API

REST 是一组资源导向的架构约束,不是一个新的网络传输协议。常见设计会使用:
GET /api/articles 查询文章
POST /api/articles 创建文章
GET /api/articles/42 获取文章
PATCH /api/articles/42 部分更新
DELETE /api/articles/42 删除文章
接口应明确方法语义、状态码、幂等性、分页、排序、过滤、缓存、错误格式和权限。URL 版本号只是方案之一,也可以使用媒体类型或兼容的演进策略。
2. GraphQL

GraphQL 是查询语言和服务端运行时。客户端可以声明所需字段,减少过度获取,但服务端需要处理查询复杂度、深度、缓存、权限、N+1 查询和错误返回。GraphQL 通常仍通过 HTTP 传输,也可以组合订阅或 WebSocket/SSE 方案。
3. Socket.IO
Socket.IO 是一个实时通信库和协议栈,通常优先使用 WebSocket,也可以在特定环境使用轮询等传输,并提供自己的事件、重连和房间能力。它不是“原生 WebSocket 的简单别名”,Socket.IO 客户端不能直接连接任意原生 WebSocket 服务端,反之亦然。
4. 消息队列

RabbitMQ、Kafka 等消息队列主要用于服务端之间的异步解耦、削峰和事件分发。浏览器通常通过 HTTP/WebSocket/SSE 连接业务网关,而不是直接暴露消息队列凭据。需要关注消息至少一次/至多一次投递、幂等、顺序、重试、死信和权限。
十一、其他前后端通信模式
1. 轮询和长轮询
- 短轮询:浏览器按固定周期发送请求,简单但可能产生大量空请求和延迟;
- 长轮询:服务器暂不立即结束请求,有事件或超时后返回,客户端再发起下一次;
- SSE/WebSocket:在实时性和连接管理成熟时通常更适合。
轮询必须设置超时、停止条件、指数退避和页面可见性策略,不要在后台页面无休止地每秒请求。
2. sendBeacon
页面即将隐藏时发送小型分析数据可使用:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'hidden') return
const payload = JSON.stringify({
path: location.pathname,
time: Date.now()
})
navigator.sendBeacon(
'/analytics',
new Blob([payload], { type: 'application/json' })
)
})
Beacon 适合小型 POST,排队大小有限,不适合需要读取响应、重试控制或上传大文件的业务。不要依赖 unload/beforeunload 发送关键数据;重要状态应在变化时保存。
3. postMessage 不是后端通信
跨窗口、iframe 或 Worker 间可以通过 postMessage 通信,但它不等于跨域读取后端数据:
otherWindow.postMessage(
{ type: 'ready', version: 1 },
'https://trusted.example'
)
window.addEventListener('message', event => {
if (event.origin !== 'https://trusted.example') return
if (event.data?.type !== 'ready') return
console.log(event.data)
})
发送时不要随意使用 '*',接收时必须校验 origin、数据结构和权限,绝不能把消息直接当作 HTML 或代码执行。
十二、CORS 与同源策略
1. 什么是跨源
源由协议、主机和端口共同决定:
https://app.example.com:443
与协议、主机或端口任一项不同,就是不同源。浏览器同源策略主要限制脚本读取其他源的响应和访问对方 DOM,不是“所有跨源请求都不会发出”。图片、表单和某些简单请求本来就可能向跨源地址发送。
CORS 是服务器通过响应头向浏览器声明“允许哪些源读取响应”的机制。它不替代服务端认证、授权和 CSRF 防护;非浏览器客户端也不会自动遵守 CORS。
2. 简单请求和预检
满足方法、请求头和 Content-Type 条件的跨源请求可能不触发预检,但服务器仍然必须防范 CSRF,因为请求可能已经到达并产生副作用。
带自定义请求头、非简单方法或非简单 Content-Type 时,浏览器通常先发 OPTIONS 预检:
OPTIONS /api/profile HTTP/1.1
Origin: https://app.example
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type, x-request-id
服务端允许后返回:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: GET, PATCH, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Request-Id
Access-Control-Max-Age: 600
Vary: Origin
3. 带凭证请求
Fetch:
fetch('https://api.example.com/profile', {
credentials: 'include'
})
服务端必须返回明确的 Origin 和:
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true
带凭证时不能使用 Access-Control-Allow-Origin: *。Cookie 还要受到 SameSite、第三方 Cookie 策略和 Secure 等规则影响。
4. 一个 allowlist 思路
const allowedOrigins = new Set([
'https://app.example.com',
'https://admin.example.com'
])
app.use((req, res, next) => {
const origin = req.get('Origin')
res.vary('Origin')
if (origin && allowedOrigins.has(origin)) {
res.set('Access-Control-Allow-Origin', origin)
res.set('Access-Control-Allow-Credentials', 'true')
res.set('Access-Control-Allow-Methods', 'GET,POST,PATCH,OPTIONS')
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-Id')
}
if (req.method === 'OPTIONS') {
return res.sendStatus(origin && allowedOrigins.has(origin) ? 204 : 403)
}
return next()
})
不要把请求头中的 Origin 原样反射回响应,不要为了“解决跨域”给整个域名开放 *,也不要把 CORS 错误当成后端没有收到请求。安全策略应在服务端授权层再次执行。
十三、接口设计和安全清单
接口契约
- 明确输入参数、类型、范围、默认值和必填项;
- 明确响应 JSON schema、空值、分页、排序和错误结构;
- 使用合适 HTTP 方法和状态码,不把所有结果都返回
200; - 约定幂等性、重试、超时、请求 ID 和去重键;
- 对大列表使用分页或游标,对上传设置大小和类型限制;
- 通过 OpenAPI、GraphQL schema 或其他文档保持前后端一致;
- 设计版本兼容和弃用周期,不要突然删除客户端依赖的字段。
安全
- 全链路使用 HTTPS/WSS;
- 服务端验证认证、授权和租户边界,不能只相信前端隐藏按钮;
- 对 Cookie 会话设置
HttpOnly、Secure、合适的SameSite,并使用 CSRF 防护; - 对 JSON、表单、WebSocket、SSE 和 Worker 消息都做输入校验;
- 解析响应时使用安全 DOM API,不把后端字符串直接塞进
innerHTML; - CORS 使用明确 allowlist,带凭证时禁止通配符;
- 限制请求大小、速率、连接数和 WebSocket 消息队列;
- 日志中不要记录密码、完整 Token、Cookie 和敏感个人信息。
总结
- AJAX 是历史称呼,现代普通请求优先考虑 Fetch,XHR 仍适合进度和兼容场景;
- Fetch 遇到 404/500 不会自动 reject,必须检查
response.ok; - WebSocket 可双向通信,但跨源不等于无安全限制,必须校验 Origin、认证和消息;
- SSE 适合服务器单向推送,WebTransport 适合更底层的 HTTP/3 实时场景;
- Form 是可靠的原生请求入口,
FormData可以和 Fetch/XHR 组合; - REST/GraphQL 是 API 设计方式,Socket.IO 是库和协议栈,消息队列主要是服务端基础设施;
- CORS 控制浏览器是否向脚本暴露跨源响应,不替代服务端授权和 CSRF 防护;
- 通信方案应由方向、实时性、可靠性、数据大小、重连、鉴权和部署成本共同决定。