news 2026/9/17 2:21:34

前端跨域与实时通信:CORS、SSE、WebSocket 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端跨域与实时通信:CORS、SSE、WebSocket 实战指南

先说结论:跨域和实时通信,是前端日常开发里绕不开的两座山。你几乎每天都会遇到“接口跨域了”“推送不实时”“WebSocket 连不上”这类问题。这篇文章我不打算给你念教科书,而是从实际开发场景出发,把跨域方案、SSE、WebSocket 这三块从头到尾捋一遍,顺带把高频面试题和线上踩坑一起解决了。无论你是刚开始写前端、准备面试,还是接手了一个带实时推送的项目想快速上手,这篇都能当一份可直接抄的参考资料。

1. 先搞清楚:跨域到底在跨什么

1.1 同源策略是前端面试的第一道门槛

很多人背过“协议、域名、端口有一个不同就是跨域”,但真问他“为什么会有同源策略”,反而不一定答得上来。同源策略本质上是一种安全机制,目的是防止一个页面里的脚本随便去读另一个源的敏感数据。你想一下,如果你在一个银行页面里登录了,又打开了另一个恶意页面,这个恶意页面里的 JS 如果能直接向银行接口发请求并读取响应,那你的会话信息就全暴露了。浏览器不允许这种情况发生,所以默认情况下跨域请求要么发不出去,要么发出去但响应被拦截。

注意一个细节:跨域请求并不是“一定发不出去”。表单提交、script 标签加载这类请求其实是可以发出去的,被限制的是读取响应。这也是 JSONP 能存在的原因。在面试里你如果能说出“限制的本质是读取”,往往比只会背定义要加分。

另外,同源判断是浏览器做的,不是服务器做的。后端接口如果没做任何跨域配置,请求其实可能已经到达服务器了,只是浏览器拿到响应后发现不符合 CORS 规则,给拦下来了。调试的时候如果后端说“我这边没报错啊”,多半就是这个原因。

1.2 CORS、JSONP、代理:三类主流跨域方案对比

抛开冷门的 document.domain、postMessage 不说,前端日常能用的跨域方案其实就三类:

  • CORS(跨域资源共享):后端在响应头里加Access-Control-Allow-Origin,告诉浏览器“这个接口允许某个源访问”。这是最正规、覆盖面最广的方案。
  • JSONP:利用<script>标签不受同源限制的特性,从服务端返回一段可执行的 JS 代码,把数据包裹在回调函数里。优点是兼容性极好,缺点是只支持 GET,而且没法拿到 HTTP 状态码,错误处理很别扭。
  • 代理转发:浏览器请求同源的代理服务器,再由代理服务器去请求真正的后端服务,然后把结果返回给前端。开发环境最常见的是 Vite/Webpack 的 devServer.proxy,生产环境则是 Nginx 反向代理。

三者的选择逻辑也很简单:能改后端、或者后端团队愿意配合,就优先用 CORS;服务端代码完全动不了、只支持 GET 的旧接口,才考虑 JSONP;前后端分离部署,或者想绕开浏览器限制,就上代理。

下面重点说 CORS 和代理,因为 JSONP 在现代项目里真的越来越少见了。

1.3 CORS 预检与 Cookie 的坑

CORS 看着就是加几个响应头,但实际踩坑点在“预检请求”和“Cookie”。

浏览器会把请求分成简单请求和复杂请求。满足以下条件才是简单请求:请求方法是 GET、HEAD、POST;请求头只有 Accept、Accept-Language、Content-Language、Content-Type(且只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain 之一);没有自定义请求头。一旦不满足,浏览器会先发一个 OPTIONS 预检请求,问服务器“我这个跨域请求你允许吗”,服务器返回允许的源、方法、请求头之后,浏览器才会发真正的请求。

预检请求是面试常考点,也是开发中容易遇到的隐形坑。比如你用Content-Type: application/json发 POST,就一定是复杂请求,前端会先多出来一个 OPTIONS 请求。很多后端在日志里看到 OPTIONS 请求直接报了 403,原因是没处理预检。正确的处理方式是在网关或接口层统一响应 OPTIONS,返回 204 和允许头。

另一个坑是携带 Cookie。如果跨域请求要带 Cookie,后端响应头里不能写Access-Control-Allow-Origin: *,必须写成具体的源,比如https://a.example.com,同时还要写Access-Control-Allow-Credentials: true。前端在 axios 里也要配置withCredentials: true,否则即使后端允许带上凭据,浏览器也不会把 Cookie 发给跨域接口。我第一次做跨域登录时就在这里卡了半天:接口返回正常,但 Cookie 就是没带上,最后才发现是前端忘了开 withCredentials。

另外谷歌浏览器对跨域请求的 SameSite 属性也有影响。登录接口在 A 域,前端页面在 B 域,如果后端种 Cookie 时没设置SameSite=None; Secure,Chrome 很可能在跨域请求里丢弃这个 Cookie。跨域登录问题排查时,打开 DevTools 的 Application 面板看 Cookie 是否存在,重点看 SameSite 列。

1.4 代理方案:开发环境与生产环境的跨域处理

开发环境我用 Vite 的次数比较多,配置非常简单:

// vite.config.ts export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, }, }, }

这段配置的意思是:前端请求/api/user时,Vite 会把它转发到http://localhost:8080/user,并且通过 changeOrigin 把请求头里的 Host 改成目标地址。前端眼里所有请求都发到了自己的域名,自然就不存在跨域了。Webpack 里对应的是devServer.proxy,工作原理一样。

生产环境则普遍用 Nginx。很多团队是后端同事负责 Nginx,但前端最好也懂一点:

server { listen 80; server_name www.example.com; location /api/ { proxy_pass http://backend-service:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

注意location /api/proxy_pass结尾的/:有/表示把匹配到的/api/前缀去掉再转发,没有/则是原样转发。这是 Nginx 反代最容易写错的地方,我见过好几次因为多一个/导致后端 404。

关于 Nginx 跨域,还有一个经典写法。如果你只是想让接口支持跨域访问,可以在 location 里加:

add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; add_header Access-Control-Allow-Headers 'Content-Type, Authorization'; if ($request_method = OPTIONS) { return 204; }

但如果你要带 Cookie,*必须换成具体域名。这里还有个隐蔽的坑:Nginx 的 add_header 在某些情况下不会生效,比如 location 里已经有proxy_pass同时还有add_header,部分版本继承规则不同,需要加上always参数强制输出响应头。用的时候可以 curl 验证一下响应头到底有没有出来。

顺带说一句:很多人问“Windows 上部署 Nginx 还需要跨域吗”。要分情况,如果前端静态页面和后端接口都在同一个域名下部署(比如都是localhost/xxx或同一个域名反向代理),那就没有跨域问题;如果前端页面放在一台机器,后端接口在另一台机器的另一个域名,那不管操作系统是什么,跨域问题依然存在。

1.5 JSONP:老手艺了,但依然有存在意义

JSONP 的核心是前端动态创建 script 标签:

function jsonp(url, callbackName) { return new Promise((resolve, reject) => { const script = document.createElement('script') window[callbackName] = function (data) { resolve(data) delete window[callbackName] script.remove() } script.onerror = function () { reject(new Error('JSONP request failed')) delete window[callbackName] script.remove() } script.src = `${url}?callback=${callbackName}` document.body.appendChild(script) }) }

后端要做的就是把数据包成callback({ ... })的形式返回,并且保证 Content-Type 是 JavaScript 而不是 JSON。PHP 老项目里常见这种写法:

$callback = $_GET['callback']; $data = json_encode(['code' => 0, 'data' => []]); echo $callback . '(' . $data . ');';

JSONP 的问题是没法处理 POST、也没法拿到 HTTP 状态码,而且只要你引入了第三方 script,对方就能在你的页面上执行任意代码,安全风险比较高。所以现在新项目一般不用它,只有在对接老系统,或者后端实在没法加 CORS 响应头时,才考虑 JSONP 兜底。

跨域这个话题还牵扯到资源加载的问题。比如页面里引用了一张图片,提示“此图片未经允许不可引用”,这其实是服务器开启了防盗链(Referer 校验),请求图片时如果 Referer 不在允许名单里就返回 403。解决思路也很直接:只能服务端防盗链白名单加域名,前端这边可以在 img 标签上设置referrerpolicy="no-referrer",或者让后端允许对应来源,硬绕是不行的。

2. 服务端推送到前端:SSE 的正确打开方式

2.1 SSE 是什么,和轮询有什么区别

SSE(Server-Sent Events)很多人只在八股文里见过,实际项目里用得不算多,但它其实是被低估的方案。SSE 是建立在 HTTP 协议之上的服务端单向推送技术:客户端用 EventSource API 建立一条 HTTP 长连接,服务端可以持续地把数据推送给客户端,客户端不需要反复轮询。

轮询的问题很明显:假设你 5 秒轮询一次,服务端在第 1 秒就有数据了,客户端也得等到第 5 秒才能拿到;如果服务端一直没数据,客户端还会空跑一堆请求。SSE 则是一旦连接建立,服务端有新消息就立刻推给客户端,实时性比轮询好很多,请求次数也少很多。它是单向的,适合“服务端通知前端”的场景,比如站内信、告警、任务进度、权限审批流。

面试里问“SSE 与 WebSocket 的区别”时,我最优先想到的是方向性:SSE 是服务端到客户端的单向推送,WebSocket 是双向通信。此外 SSE 基于 HTTP,天然兼容现有基础设施,连 Nginx 都不需要额外配置就能代理;WebSocket 则是一个独立的协议,握手时需要升级协议。SSE 还自带断线重连和事件 id 机制,这点是 WebSocket 原生的短板。

2.2 SSE 协议与 EventSource 用法

SSE 的后端响应头需要设置Content-Type: text/event-stream,然后按固定格式输出数据。格式很简单:

id: 1 event: message data: hello world
  • id:事件序号,客户端断线后会用 Last-Event-ID 告诉服务端该从哪开始补数据。
  • event:事件类型,默认是 message,可以自定义。
  • data:数据内容,多行 data 会被拼成一个多行字符串。

前端写法非常简单:

const source = new EventSource('/api/sse') source.onopen = () => console.log('连接已建立') source.onmessage = (e) => { console.log('收到消息', e.data) } source.addEventListener('customEvent', (e) => { // 处理自定义事件 })

EventSource 和 fetch 不同,它不支持自定义请求头。如果你想在 SSE 请求里带 token,常见的做法是放进 query 参数,或者依赖 Cookie。这个限制很多人第一次用时会踩到。

2.3 SSE 鉴权怎么做

既然 EventSource 不能带自定义 Header,那鉴权就只有几条路:

  • 通过 URL 的 query 参数带 token,比如new EventSource('/api/sse?token=xxx')。注意 token 会出现在访问日志里,如果是敏感系统要加短期过期。
  • 依赖 Cookie,前提是接口和页面同域,或者 CORS 配置允许携带凭据。
  • 先用普通接口换取一个一次性 ticket,再用 ticket 建立 SSE 连接,服务端校验后立即失效。

我更喜欢第三种,安全性更高,也方便限制连接次数。有些框架在实现权限系统时也会用 SSE 来推送审批事件,比如 AgentScope 的权限系统 SSE 接口,本质就是让服务端把权限申请的状态变更实时推给前端:用户提交一个需要审批的操作,前端通过 SSE 等待审批结果,审批一完成服务端立刻推送,避免了前端不停轮询接口。

2.4 线上踩坑:网关超时、buffer 与断流

SSE 最大的坑不在前端,而在中间链路。HTTP 长连接很容易被网关、代理或负载均衡器掐断。最常见的报错是:

  • before completion: idle timeout waiting for sse
  • stream disconnected before completion: failed to send websocket request: io

第一条是典型的网关 idle timeout,意思是连接空闲一段时间后被服务端或网关断开了。SSE 连接建立后,如果服务端长时间不推送数据,网关可能认为连接空闲了,于是主动断开。解决办法有几个方向:

第一,服务端加心跳。即使是 SSE,也要每隔一段时间(比如 20 秒)发一条注释或空数据:

: keep-alive

第二,调大网关超时。Nginx 里需要设置:

proxy_buffering off; proxy_read_timeout 1h; proxy_send_timeout 1h;

proxy_buffering off很关键。Nginx 默认会缓冲上游响应,这会导致 SSE 数据积压在代理缓冲区里,前端迟迟收不到。如果还有gzip开启,也可能影响实时性,建议对 SSE 路径关闭压缩。

idle timeout waiting for sse这种报错,除了看 Nginx,还要看 Java 技术栈里 Servlet 3.1 的异步超时、Spring 的 SseEmitter 超时时间。Spring Boot 里如果用 SseEmitter,注意设置延长超时:

SseEmitter emitter = new SseEmitter(0L); // 0 表示不超时

但生产环境不建议真的设成不超时,最好配合心跳机制,让连接一直有数据流动,这样网关也不会因为空闲而断开。

2.5 Spring Boot 实现 SseEmitter 的简易示例

服务端用 Spring Boot 实现 SSE 很方便:

@RestController public class SseController { private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>(); @GetMapping("/api/sse") public SseEmitter stream() { SseEmitter emitter = new SseEmitter(0L); emitters.add(emitter); emitter.onCompletion(() -> emitters.remove(emitter)); emitter.onTimeout(() -> emitters.remove(emitter)); // 单独开线程推送测试数据 new Thread(() -> { try { for (int i = 0; i < 10; i++) { emitter.send(SseEmitter.event().name("message").data("count: " + i)); Thread.sleep(1000); } } catch (Exception e) { emitter.completeWithError(e); } finally { emitter.complete(); } }).start(); return emitter; } }

这个示例只是演示。实际项目里应该由业务线程在数据产生时调用 emitter.send,而不是像这样自己开线程。多个客户端对应多个 SseEmitter,推送给指定用户时,要维护用户 ID 与 SseEmitter 的映射关系,用户断开后及时移除,防止内存泄漏。

3. WebSocket:真正的双向实时通信

3.1 WebSocket 是怎么建立连接的

WebSocket 并不是从零发明的协议,它的建立过程基于 HTTP。客户端先发一个带 Upgrade 头的 HTTP 请求:

GET /ws HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Version: 13

服务端验证通过后返回:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

之后这条 TCP 连接就从 HTTP 协议切换成了 WebSocket 协议,客户端和服务端可以双向实时发送数据。因为这个握手发生在 HTTP 层,所以浏览器里的跨域规则仍然约束了“能否成功建立 WebSocket 连接”:服务端握手响应里必须带上允许的来源,否则浏览器会拒绝。

跨域在 WebSocket 里同样重要。浏览器跨域协商时,WebSocket 也会带上 Origin 头,服务端需要校验 Origin 是否在白名单内。自己实现时要手动检查,像 Spring WebSocket 或 Netty 的 WebSocketServerProtocolHandler 都有对应的配置方式。如果你发现浏览器控制台报 WebSocket connection failed,很多情况不是服务端没启动,而是 Origin 校验没通过,或代理层没升级成功。

3.2 前端连接 WebSocket 的正确姿势:心跳、重连与封装

原生 WebSocket 使用起来不难,但要用于生产环境,必须加上心跳和重连。直接裸写是很脆弱的,网络一抖连接就断了,而且断的时候可能连 close 事件都没触发,前端根本不知道。

我一般会封装一个可复用的小类,核心逻辑:

  1. 建立连接,监听 onopen、onmessage、onclose、onerror。
  2. 收到服务端消息时记录最近心跳响应时间。
  3. 每隔一段时间发一次 ping。如果连续几次没收到 pong,主动关闭连接,触发重连。
  4. onclose 后延迟 1 到 5 秒自动重连,重连次数越多,延迟越大,避免服务端一挂,所有客户端疯狂重连。
  5. 服务端推进消息后,每次重连都要重新订阅业务事件。

如果用 Vue,可以直接用 VueUse 里的useWebSocket,它把消息、心跳、重连都封装好了:

import { useWebSocket } from '@vueuse/core' const { status, data, send, open, close } = useWebSocket('ws://localhost:8080/ws', { autoReconnect: { retries: 5, delay: 3000, }, heartbeat: { message: 'ping', interval: 10000, pongTimeout: 5000, }, })

VueUse 里我已踩过一些坑,比如需要你在服务端配合响应“pong”字符串,否则心跳就没意义了。心跳不是前端发完就完事,必须靠服务端回包判断连接存活。

调试 WebSocket,我推荐用 WebSocket King 这类客户端工具。它可以直接输入 ws 地址,带 Cookie、自定义 Header、重连测试,非常方便。后端在排查“浏览器连不上,服务端却说没收到连接”的时候,先用 WebSocket King 连一次,能快速区分问题出在哪一端。

3.3 后端 WebSocket 的几种实现:Spring、Netty、Gin、SignalR

后端实现 WebSocket 的选择非常多,我挑几个常见场景来说。

Spring Boot + 原生 WebSocket:适合 Java 技术栈,写起来简单,但真正要广播、群组、设置属性时,其实有个更合适的方案叫做 STOMP。STOMP 是在 WebSocket 之上封装了一层消息语义,可以用@MessageMapping处理客户端发来的消息,用SimpMessagingTemplate向指定用户或指定频道推送:

@MessageMapping("/chat") public void handleChat(@Payload ChatMessage message) { // 广播给订阅了 /topic/messages 的客户端 messagingTemplate.convertAndSend("/topic/messages", message); } @MessageMapping("/chat/private") public void handlePrivate(@Payload PrivateMessage message, Principal principal) { messagingTemplate.convertAndSendToUser(message.getToUser(), "/queue/messages", message); }

客户端这边用@stomp/stompjs订阅/user/queue/messages就能收到点对点消息。Spring 的 session 属性也可以保存用户信息、房间号,方便实现组播和广播。

Netty WebSocket:适合做高并发网关、游戏服务、长连接服务。Netty 需要自己处理握手、编解码和心跳,灵活度很高,但门槛也高。鉴权通常是在握手阶段加一个处理器:从请求 URL 的 query 参数或 Header 里取 token,校验失败就直接关掉连接,不要等到业务消息阶段再处理。示例思路:

public class AuthHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { HttpRequest request = (HttpRequest) msg; String token = new QueryStringDecoder(request.uri()).parameters().get("token").get(0); if (!checkToken(token)) { ctx.close(); return; } ctx.pipeline().remove(this); } super.channelRead(ctx, msg); } }

这段代码需要放在WebSocketServerProtocolHandler之前,因为 WebSocket 的握手消息本质上是 HTTP 请求。鉴权通过后移除自身,后续的 WebSocketFrame 消息就不用再重复鉴权了。

Gin + gorilla/websocket:Go 后端里最主流的组合。Gin 负责 HTTP 路由,gorilla/websocket 负责升级连接。需要注意 gorilla/websocket 需要显式调用Upgrade才有 WebSocket 能力,否则就是普通 HTTP 处理。长连接应用(比如语音对讲、实时位置上报)还要考虑并发读写限制,WriteMessage不能并发调用,通常需要单独 goroutine 做写队列。

SignalR:如果你在用 .NET 体系,SignalR 是首选。前端用官方库@microsoft/signalr,获取数据的模式很清晰:用.on('methodName', callback)监听服务端推送,用.invoke('methodName', args)调用服务端方法,用.stream()处理流式数据。SignalR 自动处理协议协商、连接管理和重连,封装度很高,缺点是和微软技术栈绑定较深。

3.4 鉴权、广播、群组与集群实践

WebSocket 鉴权的核心原则是:在握手阶段完成身份校验。因为握手之后连接变成全双工通道,如果此时才鉴权,你就得在业务数据里传 token,不仅脏,而且容易被绕过。

常见做法有几种:

  1. URL query 带 token:ws://example.com/ws?token=xxx。最简单,但 token 可能会出现在网关访问日志和浏览器历史记录里。
  2. 自定义 Header:原生 WebSocket 是不支持自定义 Header 的,但很多服务端库在后端代理场景下可以读 Sec-WebSocket-Protocol 来透传身份信息,或者在客户端用二次握手协议来处理。
  3. Cookie:如果前后端同域,握手时会自动带 Cookie,服务端从 Cookie 里解出会话用户。
  4. 先通过 HTTP 接口换取一个连接 ticket,然后 WebSocket 连接时带上 ticket,服务端校验后标记为已认证。这种方式最适合把 WebSocket 服务独立部署的场景。

广播、群组、点对点是 WebSocket 服务最常见的三类需求。单机情况下,Spring STOMP 依赖 SubscriptionRegistry,Netty 依赖 ChannelGroup 或者自己维护 Channel 集合。但要上多实例,单机广播就不够了:用户 A 连在实例 1,用户 B 连在实例 2,要给 B 发消息时,必须让实例 2 知道这条消息的存在。常规做法是把消息投递到 Redis Pub/Sub 或消息队列,所有实例订阅同一个频道,收到消息后通过本地的 Channel 找到连接,再推送出去。这套机制是分布式 WebSocket 的核心,面试里如果被问“集群下怎么实现广播”,能说出 Redis Pub/Sub 加本地注册表,就比只回答“用 ChannelGroup 广播”高一档。

3.5 常见连接异常:1006、H5 正常 App 不行、地址写错

WebSocket 1006是特别常见的异常关闭码,含义是“连接异常关闭,且没有收到正常的关闭帧”。前端只在 onclose 里看到 code 1006,根本不知道原因。常见的诱因:

  1. 服务端进程崩溃,没来得及发送 Close 帧。
  2. 代理层(Nginx、网关)在空闲超时后直接断开连接。
  3. 服务端没有实现心跳,长时间空闲导致中间设备把连接清了。
  4. 网络切换,手机从 WiFi 切到 4G/5G,原来的 TCP 连接直接失效。

排查 1006 时,先抓包或者看服务端日志,确认连接是被谁断开的。如果是网关断的,调整代理超时;如果是服务端断的,考虑加心跳和空包保活。

H5 可以连接,打包成 App 连不上这个问题我在真实项目里见过几次。正常情况下,浏览器页面里用new WebSocket('ws://localhost:8080/ws')很顺利,但用 WebView 打包成 App 后却连不上。第一反应是检查地址。App 里如果跑在真机上,localhost指的是手机本身,不是开发电脑,必须改成电脑的局域网 IP。如果用了http://ip:port去拼 WebSocket 地址,要记得把http替换成wshttps替换成wss,写成ws://ip:port/ws。还有就是原生 App 的网络权限、网络安全配置是否允许明文流量(Android 9+ 默认禁止 http),以及证书校验问题。遇到这种“浏览器没问题,App 不行”的情况,先看这几点,命中率很高。

地址写错:这个看着低级,但我做过不少次。ws://wss://的混用、路径漏了、端口不对,都会导致连接直接失败。尤其是有些公司网关会给 WebSocket 加前缀,比如/api/ws,你只连了/ws,就会一直连接不上。自查的办法是用 WebSocket King 连一遍,能连上就把同样的地址替换到代码里。

4. 实时通信方案选型与前端面试问答

4.1 轮询、SSE、WebSocket 对比

给一个常用对比表:

方案方向实时性连接数成本断线重连适合场景主要问题
短轮询客户端主动取决于间隔无,天然重试更新不频繁的后台任务大量无效请求
长轮询双工模拟需要自己实现老系统兼容连接频繁建立销毁
SSE服务端单向低,一条长连接自带通知、进度、日志流不支持双向
WebSocket双向最高需要自己实现聊天、游戏、协作、实时数据复杂度高,网关配置多

面试里问到“为什么选 SSE 不选 WebSocket”,可以从三个角度答:功能上只需要服务端推送;SSE 用普通 HTTP,调试、代理非常简单;EventSource 自带重连,省了自己写心跳的部分逻辑。如果反问“那为什么有的场景必须用 WebSocket”,聊天、在线编辑、多人协作、白板、语音等等,因为客户端要同时往服务端发事件,根本绕不开双向通道。

4.2 方案选型建议

我在实际项目里的决策顺序是这样的:

  1. 先问业务是否需要双向:只要服务端主动推消息,且客户端不需要实时给服务端发业务消息,优先 SSE。
  2. 如果客户端也要持续上报数据,用 WebSocket。
  3. 如果场景只是“定时拉一次数据”,连 SSE 都不用,短轮询就够了。
  4. 如果服务端是用 Spring 技术栈,并且要处理群组、点对点,直接上 STOMP 而不是裸 WebSocket。
  5. 如果用 WebSocket,前端务必封装心跳和重连,别裸写。

SSE 还有一个讨喜的优点:它可以基于 HTTP/2 复用连接。HTTP/1.1 下浏览器对同一域名的连接数是有限制的,如果同时开很多 SSE 连接会占满连接池。WebSocket 在 HTTP/2 里也有自己的问题,所以如果遇到“连接一多,其他请求全部排队”的现象,优先考虑合并通道或换技术。

4.3 高频面试题整理

这里列几道我在面试中常问、也常被问的前端八股文,附上我理解里的正确答法方向:

  1. 跨域是什么?答出同源策略(协议+域名+端口),说明浏览器限制的是“读取响应”,不是“发送请求”。
  2. CORS 预检请求什么时候触发?答出非简单请求,举例Content-Type: application/json或自定义 Header 就会触发 OPTIONS。
  3. CORS 携带 Cookie 有哪些条件?后端 Access-Control-Allow-Origin 不能是*,要具体源;Access-Control-Allow-Credentials 要为 true;前端 withCredentials 要为 true。
  4. SSE 和 WebSocket 的区别?方向性、协议、断线重连、调试难度、适用场景,四点列清楚。
  5. WebSocket 握手过程?重点说 HTTP Upgrade、101、Sec-WebSocket-Key 和 Sec-WebSocket-Accept。
  6. WebSocket 为什么会断开 1006?没有 Close 帧的异常中断,排查代理超时、服务端崩溃、心跳缺失。
  7. 集群下如何推送消息?Redis Pub/Sub 或 MQ + 本地连接存储。
  8. EventSource 能带 token 吗?不能带 Header,用 query 或 Cookie,或先换 ticket。

面试答题时,最好每道题都带一个实际案例。比如答预检请求时,举例“我之前对接支付接口时因为自定义订单头导致多了个 OPTIONS,后端没处理,最后在网关统一加了解析”,这种表达比单纯背概念更有说服力。

5. 我的一些实际体会

讲到最后,说几个真心建议。

跨域问题不要一味追求“前端绕”。除非后端完全改不了,否则优先推动后端把 CORS 配置好,或者由网关统一处理。前端代理只在开发环境里用,生产环境必须靠 Nginx 或网关,否则上线就踩坑。

SSE 被很多人忽略了,但在“通知、进度、审批流”这类场景里,它比 WebSocket 简单可靠得多。前端代码就那么几行,断线重连还是内置的,我后面几个项目都优先用了它。

WebSocket 最大的问题从来不是“连不上”,而是“连上了不知道怎么保持健康”。心跳、重连、鉴权、集群推送,这四件事如果不提前设计好,项目跑一段时间就会出现莫名其妙的掉线、消息丢失和内存泄漏。尤其是心跳,务必让服务端配合回包,否则前端发的 ping 等于自娱自乐。

最后再分享一个排查实时连接问题的习惯:先用客户端工具连一次,再用 curl 看服务端响应头,最后看代理层配置。按这个顺序可以把问题定位到具体环节,比直接怀疑后端要高效得多。希望这篇对你有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 2:21:27

RD算法SAR图像仿真:从原理建模到硬件部署全流程

简介&#xff1a;本资源是一套基于MATLAB实现的SAR&#xff08;合成孔径雷达&#xff09;成像RD&#xff08;距离-多普勒&#xff09;算法仿真代码&#xff0c;面向遥感、雷达信号处理、地球观测等方向的本科生、研究生及工程技术人员&#xff0c;用于深入理解SAR图像形成机理与…

作者头像 李华
网站建设 2026/9/17 2:20:59

2026年广州瓷砖空鼓怎么修?不同情况处理方法不一样

瓷砖空鼓&#xff0c;就是瓷砖和墙面、地面之间“脱了层”&#xff0c;中间有空隙。用手敲一敲&#xff0c;声音发空&#xff0c;严重的踩上去会晃、会响&#xff0c;边缘翘起来。卫生间、厨房、阳台这些贴砖多的地方最常出现。别小看空鼓&#xff0c;它不只是难看&#xff0c;…

作者头像 李华
网站建设 2026/9/17 2:19:39

前缀树(Trie)原理与C++实现详解

1. 前缀树&#xff08;Trie&#xff09;基础与LeetCode 208题意解析前缀树是一种高效的树形数据结构&#xff0c;特别适合处理字符串相关问题。在LeetCode 208题中&#xff0c;我们需要实现一个基本的前缀树结构&#xff0c;包含insert、search和startsWith三个核心操作。这个数…

作者头像 李华
网站建设 2026/9/17 2:18:48

Windows虚拟内存配置指南:从原理到实操,避免OOM崩溃

1. 先说清楚&#xff1a;虚拟内存和 OOM 到底是怎么回事内存不够导致程序崩溃&#xff0c;这个场景几乎所有 Windows 用户都遇到过。游戏正打团突然闪退回桌面&#xff0c;浏览器开着几十个标签页然后系统提示"内存不足"&#xff0c;用 Docker 跑个 MySQL 容器结果容…

作者头像 李华