前端必须用 EventSource 连接后端 SSE 通道,URL 需带用户标识(如 token),配合正确响应头(含 X-Accel-Buffering: no)确保消息实时推送,浏览器关闭自动断连,建议监听 onerror 实现重连。

前端怎么连上后端消息通道
不能用 fetch("/api/notice") 拉一次就完事——那只是 HTTP 请求,不是“连接”。首页要持续收通知,得选对协议:轮询、SSE 或 WebSocket。三者里 SSE(Server-Sent Events)最轻量、兼容性好、无需额外长连接服务,适合系统通知类场景(如新订单、审批通过),也是 TP8 官方推荐的落地方式。
关键点:
- 前端必须用
EventSource,不能用fetch或axios模拟 - URL 必须带用户标识,例如
/api/channel/notice?token=abc123,否则后端无法绑定到具体人 - 若部署了 Nginx,必须加
X-Accel-Buffering: no响应头,否则消息会被缓存住不推 - 浏览器关闭标签页时自动断连,无需前端手动
close()(但建议监听onerror做重连)
后端 response()->stream() 怎么写才不丢消息
TP8 的 response()->stream() 是 SSE 的核心出口,但默认行为会缓存响应体、压缩输出、甚至提前结束流——这些都会导致前端收不到实时消息。
必须显式设置以下响应头:
立即学习“PHP免费学习笔记(深入)”;
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive-
X-Accel-Buffering: no(Nginx 环境下必加)
示例控制器逻辑:
return response()->stream(function () {
$userId = auth()->id(); // 从 token 或 session 解析
$redis = app('redis.connection');
$lastId = '0-0';
while (true) {
// 阻塞读取 Redis Stream,超时 30s
$messages = $redis->xread(['COUNT' => 1, 'BLOCK' => 30000], ['streams' => ["notice:{$userId}" => $lastId]]);
if ($messages) {
foreach ($messages[0][1] as [$id, $data]) {
echo "id: {$id}\n";
echo "data: " . json_encode($data) . "\n\n";
ob_flush();
flush();
$lastId = $id;
}
}
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
]);
注意:ob_flush() 和 flush() 缺一不可;PHP-FPM 默认禁用 flush,需在 php.ini 中确认 output_buffering = Off 或设为 0。
为什么前端收不到,但 Redis 里明明有数据
常见卡点不在代码逻辑,而在基础设施链路断层:
- Nginx 缓存了整个 SSE 响应(哪怕加了
no-cache),必须加X-Accel-Buffering: no才真正禁用缓冲 - PHP-FPM 的
request_terminate_timeout触发强制中断,需调大(如设为300秒)或改用pm=static模式 - Redis Stream 的 consumer group 名没带用户维度,比如用了固定 group
notice_group,导致多个用户共用 offset,互相覆盖消费位置 - 前端
EventSourceURL 没传 token 或 session_id,后端查不到$userId,只能往一个公共 stream 写,但前端订阅的是私有 stream
验证方法:用 curl -N http://localhost:8000/api/channel/notice?token=xxx 直连,看是否持续吐出 data: {...} —— 如果 curl 能收到,但浏览器不行,90% 是 Nginx 或浏览器缓存问题。
未读数怎么同步又不拖慢首页加载
首页顶部小红点不能每次打开都查数据库 COUNT(*),也不能全靠前端自己维护状态(容易错乱)。
正确做法是「Redis 主存 + DB 回填」双层结构:
- 用户登录后,初始化
INCR unread_count:{$userId},并触发一次 DB 清零:UPDATE message SET status = 1 WHERE to_user_id = ? AND status = 0 - 首页接口
/api/message/unread先查GET unread_count:{$userId},命中则直接返回;未命中或为空,再查 DB 并回填 Redis - 每条新消息写入时,必须原子执行:
INCR unread_count:{$toUserId}(不能先 GET 再 INCR) - 用户点开消息页前,必须清空计数并更新缓存,避免重复通知
这个设计把首页首屏的未读查询压到 O(1),且不依赖 MySQL 的行锁或索引扫描,上线后首页 TTFB 通常能压到 20ms 以内。



















