Hono 生产实战 06:SSE、WebSocket 和流式响应
前后端分离项目里,经常会遇到“后端需要持续告诉前端一些东西”的场景。
比如:
1 2 3 4 5 6 7
| AI 对话逐字输出 任务进度实时更新 订单状态变化 日志实时滚动 协作文档在线状态 聊天室消息 监控大屏刷新
|
很多人第一反应是 WebSocket。
但生产里并不是所有实时需求都该上 WebSocket。
Hono 支持普通 HTTP、流式响应、SSE,也可以配合不同运行时使用 WebSocket。真正重要的不是“选最酷的协议”,而是选和业务复杂度匹配的通信方式。
这篇文章讲清楚 Hono 项目里如何选择 SSE、WebSocket 和 streaming。
技术栈快照时间:2026-06-23。Hono 的 streaming helper、WebSocket helper 和各运行时支持情况会更新,生产使用前以官方文档为准。
一、先别急着 WebSocket
实时通信有四种常见选择:
可以这样理解:
1 2 3 4
| 轮询:前端隔几秒问一次 流式响应:一次 HTTP 请求里持续返回数据 SSE:服务端单向推送事件 WebSocket:客户端和服务端双向长连接
|
选择建议:
1 2 3 4
| 任务状态偶尔变化:轮询 AI 文本逐步输出:流式响应 服务端持续通知前端:SSE 前后端需要高频双向通信:WebSocket
|
不要为了“实时”就默认 WebSocket。
如果只是导出任务进度,从 0% 到 100%,轮询或 SSE 通常更简单。
如果是在线聊天、多人协作、实时游戏,那 WebSocket 才更合适。
二、流式响应适合 AI 输出
AI 应用最常见的是流式文本。
用户问一个问题,后端不是等模型完整生成后再返回,而是边生成边返回。
Hono 提供了 streaming helper,可以写出很直观的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { Hono } from 'hono' import { streamText } from 'hono/streaming'
const app = new Hono()
app.post('/api/chat', async (c) => { const { message } = await c.req.json()
return streamText(c, async (stream) => { await stream.write('正在思考:')
for await (const chunk of callModel(message)) { await stream.write(chunk) } }) })
|
前端可以用 fetch 读取 ReadableStream:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: input }), })
const reader = res.body!.getReader() const decoder = new TextDecoder()
while (true) { const { done, value } = await reader.read() if (done) break
const text = decoder.decode(value, { stream: true }) appendMessage(text) }
|
这种方式非常适合:
1 2 3 4
| AI 文本生成 命令执行日志 大文件逐块返回 长耗时计算的阶段性输出
|
它仍然是 HTTP 请求,不需要维护双向连接模型。
三、SSE 适合服务端单向推送
SSE,全称 Server-Sent Events。
它适合服务端持续向浏览器推送事件。
典型场景:
1 2 3 4 5
| 任务进度 系统通知 监控数据 订单状态 后台作业日志
|
Hono 的 streaming helper 也支持 SSE。
一个任务进度接口可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| import { Hono } from 'hono' import { streamSSE } from 'hono/streaming'
const app = new Hono<{ Bindings: Env }>()
app.get('/api/jobs/:id/events', async (c) => { const jobId = c.req.param('id') const user = c.get('user')
return streamSSE(c, async (stream) => { while (true) { const job = await getUserJob(c.env.DB, jobId, user.id)
if (!job) { await stream.writeSSE({ event: 'error', data: JSON.stringify({ code: 'JOB_NOT_FOUND' }), }) break }
await stream.writeSSE({ event: 'progress', data: JSON.stringify({ id: job.id, status: job.status, progress: job.progress, }), })
if (job.status === 'succeeded' || job.status === 'failed') { break }
await stream.sleep(1000) } }) })
|
前端使用浏览器原生 EventSource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const source = new EventSource(`/api/jobs/${jobId}/events`)
source.addEventListener('progress', (event) => { const data = JSON.parse(event.data) renderProgress(data)
if (data.status === 'succeeded' || data.status === 'failed') { source.close() } })
source.addEventListener('error', () => { source.close() })
|
SSE 的优点是简单。
浏览器会自动处理断线重连,协议也比 WebSocket 更接近普通 HTTP。
四、SSE 的生产注意点
SSE 简单,但不是没有坑。
第一,连接时间要受控。
不要让一个 SSE 连接永远挂着。
比如任务完成后立刻结束:
1 2 3 4 5 6 7
| if (job.status === 'succeeded' || job.status === 'failed') { await stream.writeSSE({ event: 'done', data: JSON.stringify(job), }) break }
|
第二,要有心跳。
如果中间代理长时间没有数据,连接可能被断开。
可以定期发送 ping:
1 2 3 4
| await stream.writeSSE({ event: 'ping', data: JSON.stringify({ now: Date.now() }), })
|
第三,要考虑浏览器连接数。
不要给一个页面开十几个 SSE。
如果页面需要多个事件源,尽量合并成一个:
1 2 3 4
| /api/events -> job.progress -> notification.created -> billing.updated
|
第四,鉴权要谨慎。
EventSource 原生不方便设置自定义 header。很多团队会用 cookie 鉴权,或者用短期 token 放在查询参数里。
如果用查询参数 token,必须短有效期,并避免写入日志。
五、WebSocket 适合双向实时
WebSocket 适合前后端都需要主动发送消息的场景。
比如:
1 2 3 4 5 6
| 聊天室 多人协作 实时白板 终端会话 在线游戏 低延迟状态同步
|
在 Hono 里,WebSocket 支持和运行时有关。
Node、Bun、Deno、Cloudflare Workers 的底层模型不完全一样,所以不要只看一段 demo 就直接搬到生产。
一个概念上的 Hono WebSocket 写法大概是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { Hono } from 'hono' import { createBunWebSocket } from 'hono/bun'
const app = new Hono() const { upgradeWebSocket, websocket } = createBunWebSocket()
app.get('/ws', upgradeWebSocket((c) => { return { onOpen(_event, ws) { ws.send('connected') },
onMessage(event, ws) { ws.send(`echo: ${event.data}`) },
onClose() { console.log('closed') }, } }))
export default { fetch: app.fetch, websocket, }
|
具体 import 和 adapter 要按运行时选择。
如果你部署在 Cloudflare Workers,并且需要维护房间、连接列表、广播状态,通常要认真评估 Durable Objects。
因为普通无状态 Worker 不适合保存长连接之间的共享状态。
六、WebSocket 不只是连接
很多 WebSocket 项目真正难的不是“握手成功”。
难的是后面的协议设计:
1 2 3 4 5 6 7 8 9 10 11
| 消息类型 鉴权 心跳 重连 房间 广播 顺序 幂等 限流 状态恢复 断线清理
|
不要直接发送散乱 JSON。
建议统一消息格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| type ClientMessage = | { type: 'chat.send' id: string payload: { roomId: string text: string } } | { type: 'room.join' id: string payload: { roomId: string } }
type ServerMessage = | { type: 'chat.created' id: string payload: { roomId: string messageId: string text: string } } | { type: 'error' id?: string payload: { code: string message: string } }
|
每条客户端消息都带 id。
服务端返回时可以带同一个 id,方便前端做确认、失败提示和重试。
七、WebSocket 鉴权怎么做
WebSocket 鉴权通常在升级连接时完成。
常见方式:
1 2 3
| Cookie session 短期 access token 一次性连接 token
|
连接 token 是比较干净的做法:
1 2 3 4 5
| 1. 前端先调用 /api/ws-token 2. 后端校验用户身份,签发一个 30 秒有效的 token 3. 前端连接 /ws?token=<short-lived-token> 4. WebSocket 服务端校验 token 5. 校验通过后建立连接
|
示例:
1 2 3 4 5 6 7 8 9 10 11 12
| app.post('/api/ws-token', async (c) => { const user = c.get('user')
const token = await signWsToken({ sub: user.id, exp: Math.floor(Date.now() / 1000) + 30, }, c.env.JWT_SECRET)
return c.json({ data: { token }, }) })
|
注意:如果 token 放在 URL 里,要避免被访问日志完整记录。
也可以把 token 放在协议字段或 cookie 里,具体取决于运行时和客户端能力。
八、长连接要有限流
实时接口也要限流。
至少限制三类东西:
1 2 3
| 每个用户最大连接数 每个连接每秒消息数 每个房间广播频率
|
否则一个异常客户端就可能把系统打爆。
可以先做最简单的内存计数。
但如果是多实例、多区域、多房间,应该把连接状态放进更适合的地方,比如 Durable Objects。
限流策略不一定复杂:
1 2 3
| 普通用户:每个账号最多 3 个连接 单连接:每秒最多 10 条客户端消息 房间广播:合并 100ms 内的状态变更
|
关键是要明确边界。
实时系统没有边界,就很难稳定。
九、流式响应、SSE、WebSocket 怎么选
可以用这个表快速判断:
| 场景 |
推荐方案 |
原因 |
| AI 文本逐字输出 |
流式响应 |
一次请求,一路返回,模型简单 |
| 报表导出进度 |
轮询或 SSE |
服务端单向通知即可 |
| 系统通知 |
SSE |
低频推送,浏览器支持好 |
| 日志实时查看 |
SSE 或 streaming |
单向输出为主 |
| 聊天室 |
WebSocket |
双向消息,高频交互 |
| 多人协作 |
WebSocket + 状态服务 |
需要连接状态和广播 |
| 后台任务完成提醒 |
轮询/SSE |
不需要双向协议 |
我的建议是:
1 2 3
| 能轮询就先轮询 需要服务端推送就用 SSE 需要双向实时再用 WebSocket
|
这不是保守。
这是为了让复杂度跟业务价值匹配。
十、一个实用组合
在 Hono 生产项目里,我最常用的组合是:
1 2 3 4 5 6
| 普通 CRUD:HTTP JSON API AI 输出:streamText 后台任务进度:SSE 低频状态刷新:轮询 聊天/协作:WebSocket 连接状态管理:Durable Objects 或专门实时服务
|
这样每类需求都有合适边界。
前后端也更容易协作。
前端同学知道:
1 2 3 4
| 这个接口是一次性 JSON 这个接口是 stream 这个接口是 SSE 这个接口是 WebSocket
|
后端同学也知道:
1 2 3 4
| 哪些接口走请求响应 哪些接口要处理断线 哪些接口要处理连接状态 哪些接口要处理广播和重连
|
Hono 的价值在这里很明显。
它不逼你把所有实时需求塞进一种框架抽象里。
你可以按业务场景,选择最轻但足够可靠的通信方式。