
先说结论跨域和实时通信是前端日常开发里绕不开的两座山。你几乎每天都会遇到“接口跨域了”“推送不实时”“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 时没设置SameSiteNone; SecureChrome 很可能在跨域请求里丢弃这个 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 标签上设置referrerpolicyno-referrer或者让后端允许对应来源硬绕是不行的。2. 服务端推送到前端SSE 的正确打开方式2.1 SSE 是什么和轮询有什么区别SSEServer-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 worldid事件序号客户端断线后会用 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?tokenxxx)。注意 token 会出现在访问日志里如果是敏感系统要加短期过期。依赖 Cookie前提是接口和页面同域或者 CORS 配置允许携带凭据。先用普通接口换取一个一次性 ticket再用 ticket 建立 SSE 连接服务端校验后立即失效。我更喜欢第三种安全性更高也方便限制连接次数。有些框架在实现权限系统时也会用 SSE 来推送审批事件比如 AgentScope 的权限系统 SSE 接口本质就是让服务端把权限申请的状态变更实时推给前端用户提交一个需要审批的操作前端通过 SSE 等待审批结果审批一完成服务端立刻推送避免了前端不停轮询接口。2.4 线上踩坑网关超时、buffer 与断流SSE 最大的坑不在前端而在中间链路。HTTP 长连接很容易被网关、代理或负载均衡器掐断。最常见的报错是before completion: idle timeout waiting for ssestream 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 CopyOnWriteArrayListSseEmitter 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 事件都没触发前端根本不知道。我一般会封装一个可复用的小类核心逻辑建立连接监听 onopen、onmessage、onclose、onerror。收到服务端消息时记录最近心跳响应时间。每隔一段时间发一次 ping。如果连续几次没收到 pong主动关闭连接触发重连。onclose 后延迟 1 到 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/websocketGo 后端里最主流的组合。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不仅脏而且容易被绕过。常见做法有几种URL query 带 tokenws://example.com/ws?tokenxxx。最简单但 token 可能会出现在网关访问日志和浏览器历史记录里。自定义 Header原生 WebSocket 是不支持自定义 Header 的但很多服务端库在后端代理场景下可以读 Sec-WebSocket-Protocol 来透传身份信息或者在客户端用二次握手协议来处理。Cookie如果前后端同域握手时会自动带 Cookie服务端从 Cookie 里解出会话用户。先通过 HTTP 接口换取一个连接 ticket然后 WebSocket 连接时带上 ticket服务端校验后标记为已认证。这种方式最适合把 WebSocket 服务独立部署的场景。广播、群组、点对点是 WebSocket 服务最常见的三类需求。单机情况下Spring STOMP 依赖 SubscriptionRegistryNetty 依赖 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根本不知道原因。常见的诱因服务端进程崩溃没来得及发送 Close 帧。代理层Nginx、网关在空闲超时后直接断开连接。服务端没有实现心跳长时间空闲导致中间设备把连接清了。网络切换手机从 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 方案选型建议我在实际项目里的决策顺序是这样的先问业务是否需要双向只要服务端主动推消息且客户端不需要实时给服务端发业务消息优先 SSE。如果客户端也要持续上报数据用 WebSocket。如果场景只是“定时拉一次数据”连 SSE 都不用短轮询就够了。如果服务端是用 Spring 技术栈并且要处理群组、点对点直接上 STOMP 而不是裸 WebSocket。如果用 WebSocket前端务必封装心跳和重连别裸写。SSE 还有一个讨喜的优点它可以基于 HTTP/2 复用连接。HTTP/1.1 下浏览器对同一域名的连接数是有限制的如果同时开很多 SSE 连接会占满连接池。WebSocket 在 HTTP/2 里也有自己的问题所以如果遇到“连接一多其他请求全部排队”的现象优先考虑合并通道或换技术。4.3 高频面试题整理这里列几道我在面试中常问、也常被问的前端八股文附上我理解里的正确答法方向跨域是什么答出同源策略协议域名端口说明浏览器限制的是“读取响应”不是“发送请求”。CORS 预检请求什么时候触发答出非简单请求举例Content-Type: application/json或自定义 Header 就会触发 OPTIONS。CORS 携带 Cookie 有哪些条件后端 Access-Control-Allow-Origin 不能是*要具体源Access-Control-Allow-Credentials 要为 true前端 withCredentials 要为 true。SSE 和 WebSocket 的区别方向性、协议、断线重连、调试难度、适用场景四点列清楚。WebSocket 握手过程重点说 HTTP Upgrade、101、Sec-WebSocket-Key 和 Sec-WebSocket-Accept。WebSocket 为什么会断开 1006没有 Close 帧的异常中断排查代理超时、服务端崩溃、心跳缺失。集群下如何推送消息Redis Pub/Sub 或 MQ 本地连接存储。EventSource 能带 token 吗不能带 Header用 query 或 Cookie或先换 ticket。面试答题时最好每道题都带一个实际案例。比如答预检请求时举例“我之前对接支付接口时因为自定义订单头导致多了个 OPTIONS后端没处理最后在网关统一加了解析”这种表达比单纯背概念更有说服力。5. 我的一些实际体会讲到最后说几个真心建议。跨域问题不要一味追求“前端绕”。除非后端完全改不了否则优先推动后端把 CORS 配置好或者由网关统一处理。前端代理只在开发环境里用生产环境必须靠 Nginx 或网关否则上线就踩坑。SSE 被很多人忽略了但在“通知、进度、审批流”这类场景里它比 WebSocket 简单可靠得多。前端代码就那么几行断线重连还是内置的我后面几个项目都优先用了它。WebSocket 最大的问题从来不是“连不上”而是“连上了不知道怎么保持健康”。心跳、重连、鉴权、集群推送这四件事如果不提前设计好项目跑一段时间就会出现莫名其妙的掉线、消息丢失和内存泄漏。尤其是心跳务必让服务端配合回包否则前端发的 ping 等于自娱自乐。最后再分享一个排查实时连接问题的习惯先用客户端工具连一次再用 curl 看服务端响应头最后看代理层配置。按这个顺序可以把问题定位到具体环节比直接怀疑后端要高效得多。希望这篇对你有用。