Vue3 里怎么配 SSE 才能穿过代理:3 种做法与取舍

编程狮(w3cschool.cn) 2026-10-31 07:06:50 浏览数 (22)
反馈

能,而且要先把代理卡在哪一层看清楚。Vue3 前端想稳定收到 SSE 推送,关键不在框架本身,而在服务器有没有把 text/event-stream 这个流原样透传。开发期可以直连,生产环境几乎都要让 Nginx 这类反向代理关掉缓冲、放行分块响应;再不济就用后端做一层聚合转发。这三种走法没有绝对好坏,只有适不适合你的部署。选错了最常见的现象是:本地跑得好好的,一上线就收不到实时流。下面把它们的取舍、各自踩坑和验证手段一次讲清。

Vue3 里怎么配 SSE 才能穿过代理?

先看结论

走法 适合谁 明显代价
前端直连 EventSource 开发期、前后端同域 生产需自建 CORS 与同域
Nginx 反代透传 正式上线跑 SSE 要配对流与缓冲的头
BFF 服务端聚合 需要统一鉴权与协议 多一跳延迟、多一层维护

选哪条取决于代理和跨域卡在哪里。三条路可以并存:本地调试用直连,上线用反代做 SSE 代理,跨团队对接用聚合。

先把概念捋顺。SSE 是 Server-Sent Events 的缩写,底层就是一条保持打开的 HTTP 连接,服务端用 text/event-stream 这个内容类型不断往里推 data: 文本块。它和 WebSocket 不一样,是单向的:只能服务端推给浏览器,不能浏览器回推。理解了这一点,配置 SSE 代理时就知道要保护的是「这条流不能被截断、不能被缓冲成一整块」。

SSE 直连、反代穿透、服务端聚合三种走法怎么选?

一、Vue3 里 SSE 的最小接法

在 Vue3 的 <script setup> 里,最朴素的做法是用浏览器原生的 EventSource 打开一个同源端点,挂上 onmessage 收数据,组件卸载时 close 掉。下面这段代码本身不依赖任何库,先在本地运行通这个最小示例,再谈代理。

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const messages = ref([])
let es = null

onMounted(() => {
  es = new EventSource('/api/sse/notify')
  es.onmessage = (e) => {
    messages.value.push(JSON.parse(e.data))
  }
  es.onerror = () => {
    // 连接断开时浏览器会自动重连,这里只记录,不主动 close
    console.warn('SSE 连接异常,等待浏览器自动重连')
  }
})

onUnmounted(() => {
  es && es.close()
})
</script>

这段能跑通的前提是 /api/sse/notify 和页面同源。想系统学 Vue3 的组件与生命周期,可以先过一遍 Vue3 教程,把 ref、onMounted、onUnmounted 这套响应式与时机的配合吃透,再回来接 SSE 会更顺。

需要强调一点:SSE 的自动重连是浏览器内建的,onerror 里如果顺手调了 es.close(),重连机制就被你亲手关掉了。生产环境要保留重连,只在确实需要停止订阅时主动 close。

二、开发期:前端直连 EventSource

开发期前后端通常跑在同一个域名下(比如前端 5173、后端通过 dev server 反代到 8080),这时 SSE 代理最简单——直接 new EventSource('/api/sse') 即可,不需要任何额外头。

# 后端用 curl 自测这个端点,应当看到逐条不断往外吐 data:
curl -N -H "Accept: text/event-stream" http://localhost:8080/api/sse

预期结果是终端里一行一行冒出 data: {"t":1}、data: {"t":2} 这样的块,而不是等几秒后一次性吐出整段。如果一次性吐出,说明后端或中间层把响应缓冲了,这恰恰是上线后最容易踩的坑。

直连的边界很清楚:它只在同源、且没有任何一层代理夹在中间时好用。一旦你把前端部署到 https://app.example.com、后端在 https://api.example.com,跨域和代理同时出现,直连就走不通了,必须往下看反代。

三、生产环境:让 Nginx 把流原样透传

这是 SSE 代理最稳的一条路。Nginx 默认会缓冲上游响应,攒够一块才发给客户端,这对普通接口没问题,对 SSE 却是灾难——浏览器要的是实时流,Nginx 却把它攒成了「等全部结束再发」。所以反代 SSE 的两个必做动作是:关掉代理缓冲、放行分块传输。

location /api/sse/ {
    proxy_pass http://backend-sse;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;
    proxy_set_header X-Accel-Buffering no;
}

proxy_buffering off 和 X-Accel-Buffering: no 是关键中的关键,后者是告诉 Nginx 以及可能夹在中间的缓存层「别缓冲这条响应」。少了任何一句,流都可能被攒住。(以上反代配置依据 Nginx 官方文档的 SSE 透传建议。)

一条 curl 验证代理后的 SSE 是否真正流式

验证手段和开发期一样,只是换成走代理后的地址:

curl -N -H "Accept: text/event-stream" https://app.example.com/api/sse/notify

看到逐行 data: 实时到达,说明 SSE 代理配置对了;如果还是整段返回,回头查 proxy_buffering 是不是被更上层的配置覆盖。关于服务端如何用 Netty 这类框架正确地以流的方式把数据推出去,Netty 实战教程 里有更底层的推送实现可对照。

一个容易漏的点:如果 SSE 前面还套了一层网关或 CDN,这些组件往往也有自己的缓冲策略,Nginx 这边关了,网关那边没关,流照样被攒。排错时要一层层用 curl -N 验证,定位到底卡在哪一跳;这一步要逐层检查缓冲是否真的关掉,而不是只看 Nginx 配置本身。失败时(例如仍整段返回)先检查 proxy_buffering 是否被更上层覆盖,再修复:把 X-Accel-Buffering: no 一并透传下去。

四、统一鉴权与协议:BFF 服务端聚合

当你的 SSE 源不止一个、或者想在推送前统一做鉴权、格式转换时,直连和反代都不够用,这时用一个后端聚合层(BFF)把上游多个流收拢,再以单一 SSE 端点暴露给前端,是更干净的做法。

// Node 侧把内部多个流聚合成一个 SSE 端点(示意)
const express = require('express')
const app = express()

app.get('/api/sse/aggregate', (req, res) => {
  res.set({
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no',
  })
  const timer = setInterval(() => {
    res.write(`data: ${JSON.stringify({ t: Date.now() })}\n\n`)
  }, 1000)
  req.on('close', () => clearInterval(timer))
})

app.listen(8080)

聚合层的好处是把鉴权、限流、协议转换都收口在一处,前端只认一个端点;代价是多一跳延迟和一层要维护的服务。它本质上也是 SSE 代理的一种——只是代理的对象从「nginx→后端」变成了「前端→BFF→多个上游」。从性能取舍看,多一跳延迟换来了统一鉴权和更干净的前端边界;它的适用边界是「需要跨源、多源或统一鉴权」的场景,源很少时不值得为它单独起一层。想补 Vue3 实战,可以顺带看 Vue3 课程。

做聚合时最容易犯的错误是忘记在客户端断开时清理定时器或订阅,时间一长服务端堆积大量「幽灵连接」。上面用 req.on('close') 清 setInterval 就是这道保险,发布到生产前务必确认这类清理逻辑存在。

总结

  • Vue3 收 SSE 用原生 EventSource 就够了,框架本身不复杂,复杂的是「流能不能一路畅通到达浏览器」。
  • 开发期同源直连最简单;生产环境做 SSE 代理几乎都要 Nginx 反代,且必须 proxy_buffering off + X-Accel-Buffering: no 关掉缓冲。
  • 跨源、多源、要统一鉴权时,用 BFF 聚合层收口,代价是多一跳延迟。
  • 无论是哪条路,验证手段都一样:用 curl -N 看响应是不是逐条实时到达,而不是一次性整段返回。
  • SSE 代理的本质是「保护那条流不被截断、不被缓冲」,抓住这一点,配置就不会跑偏。

常见问题

Q:Nginx 都配了 proxy_buffering off,为什么 SSE 还是一次性返回?

先确认更上层有没有缓存层,比如 CDN、网关、或者另一个 Nginx。任何一层缓冲都会把流攒住,Nginx 这一层关了没用。排查办法是用 curl -N 从每一跳分别测:直接打后端、打这一层 Nginx、再打最外层,看哪一跳变成了整段返回,问题就出在那一层。

Q:SSE 和 WebSocket 该选哪个?

看方向。SSE 是服务端单向推,浏览器自动重连、实现简单,适合通知、进度、行情这类「只下不上」的场景;WebSocket 是双向全双工,适合聊天、协同编辑。如果只需要服务端推,SSE 代理比 WebSocket 轻得多,也更容易过代理。

Q:前端 onerror 里要不要手动重连?

不要。浏览器对 EventSource 有内建的指数退避重连,断线后会自己重连。在 onerror 里再调 es.close() 或重新 new EventSource,会和自我重连机制打架,反而连不上。只在用户主动退出、组件卸载时 close 即可。

延伸学习

  • 系统如何选择:Windows、macOS、Linux 等系统的区别与选择,部署 SSE 服务前先想清楚跑在哪。
  • Sass 编译工具:把样式预处理环节跑通,前端工程化更顺手。
  • Txyvue3 课程:Vue3 配套微课,把组件、生命周期与实战串起来。

0 人点赞