Mercure 订阅无需前端主动关闭,因 EventSource 会随页面卸载自动终止;真正需解决的是后端及时清理无效连接,应通过 Caddyfile 配置 publishers_timeout 和 subscribers_timeout,并结合短期 JWT 过期机制实现可靠失效。

浏览器页面卸载时如何主动关闭 Mercure 订阅
Mercure 订阅基于 EventSource,它本身没有“主动断开”API,连接生命周期由浏览器自动管理:页面关闭、标签页隐藏或导航离开时,EventSource 实例会被自动终止,底层 HTTP 连接关闭。你不需要、也无法在 JS 里调用类似 close() 来“手动关闭订阅”——除非你显式调用了 eventSource.close()。
但问题常出在后端未及时感知断连,导致 Hub 维持无效会话、资源泄漏。真正要解决的不是“前端怎么关”,而是“后端怎么知道它已经关了”。FrankenPHP 内置的 Mercure Hub 默认不提供连接状态回调,所以得靠协议层特征和配置兜底:
-
EventSource关闭后,客户端不再发送心跳(Mercure 不强制心跳,但浏览器默认每 3~5 秒重连一次);Hub 不会立刻清理,需依赖空闲超时 - 确保 Caddyfile 中设置了
publishers_timeout和subscribers_timeout(单位秒),例如:mercure { publishers_timeout 30 subscribers_timeout 60 }否则默认值可能长达数分钟,造成连接堆积 - 不要依赖
window.onbeforeunload或visibilitychange去发注销请求——这些事件不可靠(如进程被强杀、系统休眠),且 Mercure 协议本身不支持“退订”操作
为什么不能用 fastcgi_finish_request() 模拟“后台关闭”
有些 PHP 开发者试图在页面响应前调用 fastcgi_finish_request(),以为能“提前释放连接,再异步清理订阅”。这在 FrankenPHP 下完全无效:
- FrankenPHP 不是 FastCGI,没有
fastcgi_finish_request()函数;调用会直接报Fatal error: Uncaught Error: Call to undefined function fastcgi_finish_request() - 即使你写了个兼容 fallback(比如用
ob_end_flush()+flush()),它只影响当前 HTTP 响应流,对已建立的EventSource长连接毫无作用 - Mercure 订阅是独立于主页面请求的另一个 HTTP GET 连接,它的生命周期与页面 HTML 响应无关
真正可控的连接清理时机:服务端 JWT 过期 + 主动 publish 通知
如果你需要“用户离开即失效订阅”,唯一可靠路径是把订阅绑定到短期凭证,并配合业务逻辑推送离线事件:
立即学习“PHP免费学习笔记(深入)”;
- 给每个用户签发带短过期时间(如
exp: 120)的 subscriber JWT,在 Caddyfile 的subscriber_jwt配置中启用验证 - 用户页面加载时,用该 JWT 初始化
new EventSource("https://example.com/.well-known/mercure?topic=...&authorization=Bearer+xxx") - 当用户触发“退出”或“切换账号”等明确动作时,PHP 后端调用
mercure_publish()推送一条user_status事件,前端监听并自行销毁EventSource实例 - JWT 过期后,Hub 会拒绝新连接请求,已有连接在下次重连时失败,自然退出
注意:Mercure 不维护客户端在线状态快照,也不提供广播“某用户所有连接已断开”的钩子。所谓“关闭订阅”,本质上就是让旧连接失效、新连接拒入——靠的是 token 控制,不是连接管理。



















