FrankenPHP 用 EventSource 实现实时状态推送需:PHP 脚本关闭缓冲、设 text/event-stream 响应头、循环中 ob_flush()+flush() 推送,前端用 EventSource 监听 message 事件并手动重连;禁止混入框架中间件或轮询逻辑卡主进程。

用 FrankenPHP 的 eventsource 实现实时状态推送
FrankenPHP 本身不内置 WebSocket 或长连接管理,但原生支持 eventsource(Server-Sent Events, SSE),这是最轻量、最符合 PHP 同步模型的实时更新方案。别硬套 WebSocket 或轮询——SSE 在订单状态这类单向、低频、服务端主动推送的场景里更稳、更省资源。
关键点:FrankenPHP 要求 PHP 脚本必须显式关闭输出缓冲,并保持连接不结束;同时需设置正确的响应头。
-
header('Content-Type: text/event-stream')和header('Cache-Control: no-cache')必须存在,缺一不可 - 每次推送前调用
ob_flush()和flush(),否则浏览器收不到数据 - PHP 进程不能因超时或脚本结束而中断连接,建议用
set_time_limit(0)并加简单心跳(如每 15 秒发个: ping)防代理断连 - 前端用
EventSource接收,监听message事件,不要用fetch或axios模拟
后端 PHP 脚本怎么写才不卡死或 502
常见错误是直接在控制器里跑一个 while 循环查数据库,结果被 FrankenPHP 的请求生命周期机制 kill 掉,或者触发 Nginx 的 proxy_read_timeout 导致 502。正确做法是把状态检查逻辑解耦,用独立脚本 + 长生存期处理。
- 创建单独的流端点,比如
/stream-order-status.php?order_id=123,不在主路由中混用 - 用
pcntl_fork()或proc_open()启动子进程监听订单变更(如监听 Redis Pub/Sub 或数据库 binlog),父进程只负责转发事件——但更推荐轻量方案:定时轮询数据库(间隔 2–3 秒),配合SELECT ... FOR UPDATE防并发误读 - 务必在脚本开头加
ignore_user_abort(true),防止用户刷新页面导致连接中断进而终止脚本 - 避免在流脚本中做耗时操作(如渲染模板、调用外部 API),只做状态查询和格式化
data:行
示例片段:
立即学习“PHP免费学习笔记(深入)”;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
ob_end_clean();
set_time_limit(0);
ignore_user_abort(true);
$order_id = $_GET['order_id'] ?? '';
while (connection_status() === CONNECTION_NORMAL) {
$status = get_order_status_from_db($order_id); // 简单查询,不带 JOIN
echo "data: " . json_encode(['status' => $status, 'updated_at' => date('c')]) . "\n\n";
ob_flush();
flush();
sleep(3);
}
前端 EventSource 怎么接才不重连失败
浏览器的 EventSource 默认 3 秒自动重连,但若后端没发 retry: 字段,它会用指数退避策略(越连不上等越久),导致状态“卡住”。更要命的是,FrankenPHP 下若 PHP 进程意外退出,连接断开后前端可能永远等不到重连响应。
- 后端每次发送消息前,加一行
retry: 2000(单位毫秒),强制前端 2 秒后重试 - 监听
error事件,手动调用eventSource.close()再新建实例,避免旧连接残留 - 不要依赖
event: order_update自定义事件名——EventSource对非message事件兼容性差,统一用默认事件 - 前端收到消息后,用
JSON.parse(event.data)解析,注意event.data是字符串,不是对象
简短 JS 示例:
const es = new EventSource('/stream-order-status.php?order_id=123');
es.onmessage = (e) => {
const data = JSON.parse(e.data);
document.getElementById('status').textContent = data.status;
};
es.onerror = () => {
es.close();
setTimeout(() => new EventSource(/*...*/), 1000);
};
为什么不用 Laravel Echo 或 Soketi
FrankenPHP 是面向传统 PHP 应用优化的运行时,它没有内置事件总线或 WebSocket 服务器。强行接入 Laravel Echo 意味着额外部署 Redis + Soketi + 前端 socket.io 客户端,不仅增加运维复杂度,还会让订单状态这种简单推送变得过度工程化。
- Soketi 依赖 Node.js,与 FrankenPHP 的 PHP-CGI 模型无协同优势
- Laravel 的
broadcast机制需要队列驱动,而订单状态变更往往要求亚秒级响应,队列延迟不可控 - EventSource 是 HTTP 协议原生支持,CDN、反向代理(包括 Cloudflare)基本都透传,而 WebSocket 在某些企业网络下会被拦截
- 移动端 WebView 对 SSE 支持良好(iOS 12.2+、Android Chrome 之外需 polyfill,但订单页通常不强依赖旧 WebView)
真正容易被忽略的是:FrankenPHP 的 eventsource 流必须由 PHP 脚本直接输出,不能经过任何中间件(如 Laravel 的 Kernel 中间件栈)——否则响应头被篡改、缓冲无法清空,整个流就失效了。所以务必把流端点放在 public 目录下直出,绕过框架路由。



















