跨域、SSE、WebSocket:前端实时通信与跨域方案深度解析

发布时间:2026/9/16 1:17:27
跨域、SSE、WebSocket:前端实时通信与跨域方案深度解析
跨域、SSE、WebSocket这三个词你随便翻一份前端面试题集锦基本都会出现你再翻线上项目的告警日志大概率也能看到它们相关的报错。我自己前后带过大小十来个项目从最早的jQuery/iframe时代到现在的Vue/React单页应用几乎每个项目都会在这三件事上踩坑而且踩的方式还不重样有时候是配置跨域漏了预请求有时候是SSE连接被网关掐断有时候是WebSocket一到移动端就莫名其妙1006。今天这篇就把这三块彻底聊透从跨域方案的底层逻辑到SSE的协议细节再到WebSocket的双向通信封装全部结合真实项目里的例子来写面试也好、排障也好、给后端提需求也好你看完这一篇基本够用。这套内容适合谁刚接触前端但被跨域问题卡住的初中级开发者正在做实时消息、自动刷新、在线状态这类需求的同学以及准备前端面试想把八股文变成真理解的人。我会尽量说人话每个方案都解释清楚为什么这么设计、踩过哪些坑、线上到底怎么配。1. 跨域问题的本质与主流解决思路1.1 同源策略到底拦的是什么很多新人一开始会以为跨域是后端不给返回其实根本不是。跨域这堵墙是浏览器里的同源策略Same-Origin Policy砌起来的。所谓同源指的是协议、域名、端口三个完全一致。比如 http://localhost:8080 和 https://localhost:8080 不同源因为协议不一样http://localhost:8080 和 http://localhost:9090 不同源因为端口不一样。只要三者有一个不同浏览器就会把这次请求判定为跨域请求。服务端其实把数据正常返回了浏览器也收到了但浏览器在把响应交给你的JavaScript代码之前检查了响应头里有没有跨域授权字段CORS相关的Access-Control-*发现没有或者不匹配就直接把响应扣留了控制台报一个熟悉的错误No Access-Control-Allow-Origin header is present。所以记住一句话跨域问题的本质是浏览器对响应数据的拦截不是请求发不出去。这也就引出了解决方案的核心思路要么让浏览器认为这次请求合法CORS授权要么绕开浏览器这个关卡代理转发、JSONP。1.2 CORS最正规、最通用的跨域方案CORSCross-Origin Resource Sharing跨源资源共享是目前最标准的跨域方案。它的原理就是浏览器发现是跨域请求之后自动在请求头上加上Origin字段比如 Origin: http://localhost:5173后端看到这个字段后如果允许这个来源访问就在响应头里带上 Access-Control-Allow-Origin: http://localhost:5173 或者 *。浏览器检查响应头确认白名单匹配才把数据交给业务代码。CORS把请求分成两类简单请求和预检请求。简单请求要求方法只能是GET、POST、HEADContent-Type只能是 application/x-www-form-urlencoded、multipart/form-data、text/plain并且不能有自定义请求头。只有满足这三个条件才算简单请求浏览器直接发送不需要预检。但凡你用了application/json、Authorization头、或者PUT/DELETE方法浏览器都会先发一个OPTIONS请求这个叫预检preflight预检通过之后才会发真正的业务请求。实战里几乎每个项目都会踩这个坑前端配好了Access-Control-Allow-Origin但后端没处理OPTIONS请求结果浏览器预检请求直接404业务请求根本发不出去。所以后端配置CORS时对OPTIONS方法要直接返回204并且要带上所有允许的方法和请求头。最省事的后端写法以SpringBoot为例是配置一个过滤器或者直接用 CrossOrigin 注解。但如果项目里有网关或者统一出口我更推荐在Nginx层统一处理比改一堆后端服务省心。Nginx配置大致如下location /api/ { add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With, token always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Max-Age 86400 always; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend-server; }这里面有几个细节很多人不知道第一Access-Control-Allow-Origin建议用 $http_origin 而不是写死 *因为 * 和 Allow-Credentials携带Cookie不能同时使用浏览器会直接拒绝。第二add_header后面加 always 是为了在响应码非200比如404、500时也带上CORS头否则前端报错时看到的还是跨域拦截而不是真正的业务错误特别误导排查。第三Access-Control-Max-Age可以设置预检缓存时间单位是秒设成8640024小时能减少大量OPTIONS请求。1.3 前端开发环境的代理方案开发阶段最常用的跨域方案其实是代理。原理很简单浏览器不能跨域但服务端之间没有跨域限制所以让开发服务器Vite、Webpack devServer帮前端转发请求浏览器看到的请求是同源的自然就没有跨域问题。Vite的配置是这样// vite.config.js export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端接口不带/api前缀可以重写 rewrite: path path.replace(/^\/api/, ) } } } }这里有一个高频问题配置了代理之后想在前端获取真实的请求地址怎么办很多人发现代理后请求头里的Host变成目标服务器的地址了这不是Bug是 changeOrigin 的作用。如果你想拿到用户真实的客户端地址得让后端看X-Forwarded-For头Nginx转发代理的时候会追加proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;后端从X-Forwarded-For里就能解析出真实IP。前端如果想拼绝对地址直接 window.location.origin 就是当前页面的地址别去请求头里找。开发代理只解决开发环境的问题线上部署必须用Nginx反向代理或后端CORS。很多项目本地跑得欢一上测试环境就跨域十有八九是忘了在Nginx配跨域或者反向代理。1.4 JSONP老方案面试偶尔还会考JSONP的完整名称是JSON with Padding原理是利用 script 标签不受同源策略限制的特性。后端返回一段JS代码把数据包在一个回调函数里前端预先定义好这个函数请求返回后函数被调用数据就到了前端。// 前端 function jsonp(url, callbackName) { return new Promise((resolve, reject) { window[callbackName] data { resolve(data) delete window[callbackName] script.remove() } const script document.createElement(script) script.src ${url}?callback${callbackName} script.onerror reject document.body.appendChild(script) }) }JSONP只能支持GET请求而且需要后端配合返回特定格式现在基本被CORS替代了。但面试里经常作为早期跨域方案被问你至少要知道它解决了什么问题、有什么局限依赖 script 标签、只支持GET、没有HTTP状态码、错误处理能力弱。另外一个容易被忽略的点是Cookie跨域。登录态如果放在Cookie里跨域请求尤其是前后端不同域名的项目会遇到 SameSite 属性限制。Chrome 80之后默认 SameSiteLax跨域携带Cookie默认是禁止的。解决办法是后端设置 SameSiteNone; Secure并且前端 axios 配置 withCredentials: true后端配合 Access-Control-Allow-Credentials: true。这个点线上踩坑概率极高我见过很多回登录接口通了但Cookie没带导致白屏的现场。2. SSE轻量级服务端推送方案2.1 SSE能解决什么问题SSE全称Server-Sent Events是浏览器向服务端建立一条HTTP长连接服务端可以通过这条连接源源不断地推送数据给浏览器。方向是单向的服务端到客户端。为什么在已经有WebSocket的今天还要讲SSE因为有相当一部分实时需求其实不需要双向通道。比如股票行情刷新、AI对话流式输出、任务进度通知、日志实时展示这些场景都是客户端发一个请求服务端持续返回数据。用SSE实现会比WebSocket简单得多前端不需要引入额外的库原生 EventSource 就能用协议基于HTTP穿防火墙、走代理、配合Nginx都很方便断线自动重连是浏览器内置行为。2.2 SSE协议细节与前端写法SSE的Content-Type是 text/event-stream报文的格式非常朴素就是多行文本每行是字段: 值字段之间用空行分隔。常用字段有data、id、event、retry。一个最简单的推送帧长这样data: {status: processing, progress: 45}如果一次要推多行数据就用多个data行浏览器会自动用换行拼接如果想推不同类型的事件可以用event字段声明事件名前端 addEventListener 监听对应事件。前端代码极简单const source new EventSource(/api/stream) source.onopen () console.log(连接建立) source.onmessage e { console.log(JSON.parse(e.data)) } source.addEventListener(custom, e { // 处理event为custom的推送 }) source.onerror () { // 连接异常时会触发EventSource会自动重连 }SSE还有一个内置能力是Last-Event-ID。服务端可以在每条消息里带id字段连接断开时浏览器会自动带上 Last-Event-ID 请求头服务端根据这个ID补发消息实现简单的断点续传。这是一般人没注意到的隐藏技能做消息推送时能省很多事。2.3 SSE的鉴权、超时与Nginx配置SSE有一个比较别扭的限制原生 EventSource 不支持自定义请求头所以没法直接把Token放在 Authorization 里。常见的处理办法有三种第一种通过URL参数携带Token比如 /api/stream?tokenxxx后端从参数里取。缺点是Token可能出现在访问日志里安全要求高的话需要做脱敏处理。第二种走Cookie鉴权和页面共享登录态。第三种也是我现在比较推荐的先用fetch发起一次普通请求拿到鉴权结果然后用EventSource连接。这里有个技巧如果有一定基础可以封装一个带自定义头的SSE客户端利用 fetch 的 ReadableStream 来读流const response await fetch(/api/stream, { headers: { Authorization: Bearer ${token} } }) const reader response.body.getReader() const decoder new TextDecoder() while (true) { const { value, done } await reader.read() if (done) break console.log(decoder.decode(value)) }这种写法本质上还是在读SSE的流数据但请求头、请求方法都能自定义灵活性一下就上来了。SSE踩坑重灾区在Nginx和网关的超时与缓冲。默认情况下Nginx有 proxy_read_timeout 60s如果服务端60秒没推送数据Nginx会断开连接。另外Nginx默认会缓冲后端响应导致前端拿到的是攒了一大坨之后一次性解析出来的数据实时性大打折扣。解决办法是在location里关掉缓冲并拉长超时location /api/stream { proxy_pass http://backend-server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; chunked_transfer_encoding off; }后端也要记得在响应头里加 X-Accel-Buffering: no告诉Nginx别缓冲这个响应。报错 before completion: idle timeout waiting for sse 是另一个高频问题。这个报错的意思是连接空闲超时了客户端和服务端之间太久没有数据交互连接被某个节点Nginx、网关、负载均衡甚至是云厂商的LB给关了。解决方案分两层第一服务端加心跳每15秒或30秒推一条注释行SSE规范里形如 : heartbeat 的行会被浏览器忽略保持连接活跃第二把各层超时时间调大Nginx上面已经写了如果是云SLB/ALB还要在控制台查一下空闲超时配置。这个报错在SpringBoot的SseEmitter环境下特别常见而且排查起来很隐蔽因为本地开发环境大概率没问题一上测试环境就复现千万别上来就怀疑代码。另外用 curl 调试SSE可以直接用 -N 参数禁止curl缓冲curl -N -H Accept: text/event-stream http://localhost:8080/api/stream这样能看到实时推送的每一帧原始数据排查格式问题很管用。3. WebSocket真正意义的双向实时通信3.1 WebSocket的握手和帧格式WebSocket和HTTP的关系是它本身基于TCP但握手阶段使用HTTP协议。浏览器发一个带 Upgrade: websocket 头的HTTP请求GET /ws/chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw Sec-WebSocket-Version: 13 Origin: http://localhost:5173服务端如果同意升级返回101状态码然后连接升级为WebSocket后续通信就不再走HTTP协议了而是WebSocket的二进制帧。这里有个容易忽略的点握手阶段的请求头是服务端做鉴权的最后机会。因为连接一旦建立后续帧里是没有请求头概念的你没法把Token塞进某个帧头里。所以常见的WebSocket鉴权方案本质上都是围绕握手请求做文章第一种URL带Tokennew WebSocket(wss://xxx/ws?tokenxxx)。优点是前端简单缺点是Token会出现在Nginx访问日志、网关注册信息、浏览器开发者工具里泄露风险大。第二种用子协议传Tokennew WebSocket(url, [token, token])服务端在握手时从 Sec-WebSocket-Protocol 里拿Token。这个不污染URL而且子协议本身就是为应用层定义的。第三种先HTTP后WS先调登录接口拿一个临时code再用这个code去握手服务端校验code只用一次、一分钟内有效。这个最安全适合敏感系统。Netty写WebSocket服务端鉴权就是在 pipeline 里的 WebSocketServerProtocolHandler 之前加一个 Handler拦截握手HttpRequest查header或query里有没有合法凭证没有就返回401并拒绝升级。3.2 前端WebSocket封装心跳、重连、鉴权原生WebSocket API其实很简陋直接裸用会出现各种闹心的场景连接闪断没人知道、断网了半天不触发状态变化、服务端过一会儿没消息连接就被静默关闭。所以在项目里我几乎从不裸用WebSocket都会包一层通用类。一个可复用的封装需要解决几个关键问题心跳机制。WebSocket连接长时间没有数据传输中间的网络设备可能会把连接回收。最稳妥的做法是客户端定时比如每30秒发一个 ping服务端收到后回复 pong如果连续几次没收到pong就主动 close 并触发重连。前端没有原生ping帧能力所以我们用普通的JSON消息约定比如 { type: ping }服务端识别后回 { type: pong }。断线重连。不能简单地在 onclose 里 new WebSocket那样会造成频繁断开-重建-再断开的抖动风暴。我用的策略是结合心跳状态判断是否进入重连重连延迟采用指数退避比如第一次1秒、第二次2秒、第三次4秒……直到一个上限比如30秒同时记录重连次数超过N次就弹提示而不是无限循环。消息分发。把所有消息统一交给一个事件总线由业务模块订阅关键事件避免每个页面都写一遍 onmessage 里的switch-case。状态管理。连接状态连接中、已连接、重连中、关闭要暴露给UI让页面能在断线时显示状态提示而不是用户操作半天毫无反应。下面是一个精简但可以实际用的封装class WSClient { constructor(url, { token, reconnectMax 10, heartbeatInterval 30000 } {}) { this.url url this.token token this.reconnectMax reconnectMax this.heartbeatInterval heartbeatInterval this.reconnectCount 0 this.handlers new Map() this.pendingPing 0 } connect() { this.ws new WebSocket(this.url) this.ws.onopen () { this.reconnectCount 0 this.startHeartbeat() this.emit(status, open) } this.ws.onmessage e { const msg JSON.parse(e.data) if (msg.type pong) { this.pendingPing 0 return } this.emit(msg.type, msg.data) } this.ws.onclose () { this.stopHeartbeat() this.emit(status, close) this.scheduleReconnect() } this.ws.onerror () { // onerror之后通常紧跟着onclose这里只做日志 } } startHeartbeat() { this.pingTimer setInterval(() { this.pendingPing if (this.pendingPing 3) { this.ws.close() return } this.send({ type: ping }) }, this.heartbeatInterval) } stopHeartbeat() { clearInterval(this.pingTimer) this.pendingPing 0 } scheduleReconnect() { if (this.reconnectCount this.reconnectMax) { this.emit(status, failed) return } const delay Math.min(1000 * 2 ** this.reconnectCount, 30000) this.reconnectCount this.reconnectTimer setTimeout(() this.connect(), delay) } send(data) { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify(data)) } } on(type, handler) { if (!this.handlers.has(type)) this.handlers.set(type, []) this.handlers.get(type).push(handler) } emit(type, data) { (this.handlers.get(type) || []).forEach(fn fn(data)) } close() { clearTimeout(this.reconnectTimer) this.stopHeartbeat() this.ws this.ws.close() } }有个细节值得注意我先用普通js写成类逻辑清晰脏话少。到Vue项目里可以直接 useconst client new WSClient(ws://${location.host}/ws, { token: store.token }) client.on(status, status { isConnected.value status open }) client.on(message, msg { ... }) onMounted(() client.connect()) onBeforeUnmount(() client.close())这里最容易被忽略的是组件卸载时一定要close否则页面切走之后连接还在后台不断收数据内存和带宽都浪费还会出现这个页面明明关了还能收到弹窗的灵异现场。3.3 WebSocket在Vue、移动端与后端语音场景中的适配Vue项目里用WebSocket还有一个选择直接上第三方库比如vue3中的 vueuse/core 提供的 useWebSocket它对连接状态有响应式管理Api设计也符合组合式API的习惯。但你一旦要处理自定义心跳、鉴权、消息重放这些业务逻辑还是得封装一层。真正头疼的问题是H5里能连上打包成App就连不上。这个场景我在实际工作中遇到过好几次排查方向很有规律H5跑在浏览器里用的可能是 ws://到了App里部署环境变成了 httpsWebSocket就必须升级成 wss://否则浏览器安全策略直接拦。打包后的App如果WebView设置不允许明文流量ws://也会被禁止访问。Android打包时如果没在AndroidManifest.xml里声明 android.permission.INTERNET 权限连普通HTTP请求都发不出去更别说WebSocket。真机测试不能访问模拟器的 localhost需要换成开发机的局域网IP而且后端服务要监听 0.0.0.0而不是 127.0.0.1。更隐蔽的情况是wss证书链的问题。WebView和浏览器的证书信任机制不完全一样某些自签名证书在浏览器里能过或者能手动信任在App的WebView里就直接被拒。所以应用商店风格的项目请务必使用合法证书。关于golang websocket语音长连接和async def voice_socket(websocket) - none这类后端代码前端对接时要注意的点是语音这类场景一般以二进制帧为主音视频数据量大消息频率高后端通常会把原始音频帧直接通过WebSocket推给前端。前端可以配合Web Audio API做播放缓冲需要注意WebSocket收到的分片顺序问题、以及网络抖动造成的卡顿通常要在业务层加序号和时间戳做对齐。用Python FastAPI写语音WebSocket时函数长这样app.websocket(/voice) async def voice_socket(websocket: WebSocket): await websocket.accept() try: while True: data await websocket.receive_bytes() # 处理音频帧... except WebSocketDisconnect: print(客户端断开)协议健壮性建议约定每一帧数据的前4个字节存消息长度后面对应一个消息体这样后端在TCP黏包时能正确切分。这个问题在后端高并发下特别突出前端如果发现收到的二进制数据莫名其妙对不齐多半是协议里缺少长度字段。4. 高发问题排查与工具实测4.1 WebSocket报错1006、以及连接失败类问题WebSocket的错误码有不少最常出现在生产环境的是1006。1006是异常关闭的意思连接在未收到正常的close帧的情况下被断开也就是说这个错误通常不是业务逻辑自己关闭的而是网络层面的问题。常见原因和排查路径是网络断断续续移动端切换Wi-Fi/4G、进入弱网区域。服务端进程崩溃或重启客户端没有收到关闭帧直接断连。中间代理或网关超时比如Nginx的 proxy_read_timeout 到点静默断开连接。这个和SSE的idle timeout本质相同都需要靠心跳来维持活跃连接。使用了不稳定的网络代理工具或者云服务器安全组/防火墙把空闲连接回收了。排查建议服务端必须打断连日志客户端也要在onclose里记录 close code 和 reason。如果服务端在客户端断开瞬间没有收到任何 FIN 包基本可以断定是中转设备把连接干掉了如果服务端有日志显示远程主机强迫关闭连接那可能是客户端或其网络侧的问题。另外连接总是秒断也是高频问题。这时候去看握手是否成功如果握手返回200而不是101说明后端没有正确升级协议多半是Nginx没配置 upgrade 头。Nginx反向代理WebSocket必须显式声明location /ws/ { proxy_pass http://backend-server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }少了 Upgrade 和 Connection 那两行后端根本没机会升级成WebSocket连接自然秒断。这是我在生产环境排查过次数最多的WebSocket问题没有之一。4.2 SSE的报错和心跳实战SSE常见的报错是stream disconnected before completion: idle timeout waiting for sse出现之后连接被中断。前文提到这个报错的核心是空闲超时服务端长时间没有推送数据网关或Nginx主动关闭连接。我的解决方案是双管齐下一方面在Nginx配置调大 proxy_read_timeout另一方面让服务端每15秒推送一条注释帧 : ping 或自定义心跳事件。SpringBoot的SseEmitter里可以这么写SseEmitter emitter new SseEmitter(3600_000L); ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() - { try { emitter.send(SseEmitter.event().comment(heartbeat)); } catch (IOException e) { emitter.completeWithError(e); scheduler.shutdown(); } }, 0, 15, TimeUnit.SECONDS);前端EventSource收到注释行是不会有任何事件触发的所以不会污染业务逻辑但网络层能感知到数据在流动连接就不会闲置。4.3 跨域配置错误的常见坑位速查我整理了一张跨域排查表贴在这里方便大家线上排障照着查现象可能原因处理办法请求发出去了控制台报No Access-Control-Allow-Origin后端没配CORS或者配了*但带了凭证后端加CORS响应头带Cookie时Allow-Origin必须回显具体originOPTIONS请求404或500后端没处理预检后端对OPTIONS返回204并带上Allow-Methods/Allow-Headers浏览器提示跨域访问被拒绝请检查浏览器配置浏览器扩展拦截、或者配置了代理插件检查是否装了跨域插件临时禁用测试前端清了缓存/开了隐身请求反而好了本地缓存了旧的CORS响应头清缓存、或者确认CDN是否缓存了旧响应代理配置了但请求没走代理proxy路径前缀和后端请求路径不对应确认请求URL前缀与proxy key一致开启changeOrigin登录接口通了但Cookie没写入SameSite限制或前端未带withCredentials后端Set-Cookie加SameSiteNone; Secure前端启用credentials尤其最后一行现代浏览器对Cookie跨域的管理越来越严。如果项目是前后端完全分离部署比如前端在a.com后端在b.com登录态跨域携带Cookie现在基本成了标配问题。我的建议是优先使用Token方案把Token放在内存或localStorage里通过Authorization头传递如果业务强依赖Cookie那要把SameSite、Secure、CORS一系列配置一起调对缺一不可。4.4 接口测试工具也能测WebSocket和SSE吗Postman对WebSocket的支持不错可以建立WebSocket连接、发消息、看帧内容。JMeter做WebSocket压测的话需要先安装WebSocket Sampler插件很多团队会忽略这个直接在JMeter里找不到WebSocket相关元件。常见的报错stream disconnected before completion: failed to send websocket request: io除了服务端不稳定的因素外也可能是Sampler里配置的错误比如握手超时、请求头里漏了Upgrade。如果你只是快速验证WebSocket服务通不通我建议用chrome的开发者工具直接在Console里执行const ws new WebSocket(wss://example.com/ws) ws.onopen () ws.send(JSON.stringify({type: hello})) ws.onmessage e console.log(e.data)这比开一整套压测工具快很多。SSE的测试前文提过curl -N 是最直接的方式。平时排障多用浏览器Network面板看请求类型text/event-stream、websocket能快速判断是协议问题还是业务问题。5. 从方案选型看前后端协作聊完具体技术最后想谈一个容易被工程师忽略的维度这三个方案到底该怎么选而不是一股脑全上WebSocket。我这里给一个非常实用的选型建议。如果只是服务端往客户端单向推数据数据频率不高比如每秒几次以内、不需要客户端回传指令SSE往往比WebSocket更合适因为前端代码简单、断线自动重连、日志排查直观也更省资源。只有当数据是双向的高频交互在线协作编辑、聊天、棋牌游戏、实时白板、语音通话才应该上WebSocket。这种选型能力在项目评审时特别加分也是面试官想听到的业务思维。另外SSE和WebSocket还可以组合使用。比如一个AI对话页面请求阶段用SSE流式接收大模型的增量输出提交用户指令和停止生成用WebSocket。这听起来有点复杂但在某些有大量并发轮询的系统里能显著减少无效请求降低后端压力。很长一段时间里我都习惯性地遇到实时需求就上WebSocket。后来在某个高并发消息中心项目里在线人数几万人服务端连接数一直是瓶颈排查完发现80%的连接其实只做了一件事把订单状态推给前端。换成SSE之后连接开销下降明显资源占用也降了一个档次。从那以后我的选型原则变成先搞清楚数据流是单向还是双向再决定协议而不是先选协议再凑需求。还有一个协作层面的心得不管是CORS配置、SSE响应头还是WebSocket握手升级这些都不是纯前端或者纯后端的事。和运维、后端对需求时最好把下面这张信息表直接贴到文档里协议类型http/ws/sse、路径、需要的请求头、响应头要求、超时时间、是否走Nginx/网关、鉴权方式。一次说清楚比让后端猜你要什么高效十倍。我自己在团队里推过一段时间这种约定线上因为配置不对导致互相甩锅的情况明显少了很多。再分享一个我的个人习惯前端本地起服务时我会顺手在Nginx里也配一套反向代理保证开发环境、测试环境、生产环境的跨域策略一致。开发环境即使靠Vite代理能跑通线上如果没配好等于埋了个定时炸弹。这个习惯让我躲过了好几次本地好好的一上线就跨域的事故。