FrankenPHP Mercure订阅重复创建的典型表现是页面刷新、后退/前进或SPA路由切换时EventSource未销毁,导致多个相同URL连接并存;应通过AbortController在新建前主动终止旧连接,并避免URL中添加动态query参数。

FrankenPHP Mercure 订阅重复创建的典型表现
页面刷新、后退/前进、SPA 路由切换时,EventSource 实例没被销毁,导致多个相同 URL 的连接同时存在;浏览器 DevTools 的 Network 面板里看到多个 mercure?topic=... 请求持续 pending;服务端日志中同一客户端 IP 出现高频 mercure:subscribe 事件。这不是 FrankenPHP 的 Bug,而是前端未管理好 EventSource 生命周期的自然结果。
用 AbortController 主动终止旧连接
不能只靠 eventSource.close() —— 它不触发网络层中断,且无法覆盖未挂载前就发起的请求。必须在新建连接前显式 abort 上一个:
- 声明一个模块级或组件级的
abortController变量(不要放在事件回调内) - 每次调用
new EventSource(url, { signal })前,先执行abortController.abort() - 再新建
AbortController()并赋值给该变量 - 确保
signal传入EventSource构造函数(现代浏览器支持,FrankenPHP 无额外要求)
示例关键片段:
let abortController = new AbortController();
function connectToMercure(topic) {
abortController.abort();
abortController = new AbortController();
const url = new URL('/.well-known/mercure', location.origin);
url.searchParams.set('topic', topic);
const es = new EventSource(url.toString(), { signal: abortController.signal });
es.onmessage = (e) => console.log(e.data);
}
监听页面卸载与路由变化做兜底清理
仅靠 abort 不够:用户关闭标签页、切出浏览器、或 SPA 路由跳转时,JS 可能来不及执行。需双保险:
立即学习“PHP免费学习笔记(深入)”;
- 在
beforeunload事件中调用abortController.abort()(注意:此事件中不能再新建 controller) - 使用
pagehide替代visibilitychange,它在页面被缓存(如安卓后台)时仍会触发 - 若用框架(如 Vue/React),在组件
unmounted或useEffect cleanup中调用abortController.abort() - 避免在
unload中发 fetch —— 大部分浏览器已禁止
服务端别依赖客户端“主动断连”
FrankenPHP 内置 Mercure Hub 默认 30 秒自动踢掉空闲连接,但这个超时不可靠。关键点在于:订阅 ID(即客户端连接)由 URL + query 参数决定。如果前端反复用相同 topic 和参数重建连接,Hub 就认为是新订阅 —— 即使内容一样。所以:
- 不要在 URL 中拼接动态时间戳或随机数(如
?t=123456),这会让 Hub 无法复用连接 - 避免在
topic后加无意义后缀(如user/123?_v=2),Hub 会当作不同主题处理 - 如果业务需要区分上下文(如多 tab 编辑同一资源),应把上下文编码进 topic 名本身(如
user/123/tab-a),而不是靠 query 参数
真正难防的是用户开多个标签页手动访问同一页面 —— 这种场景下,每个标签页天然就是独立订阅,Hub 必须接受。这时候要靠业务逻辑去重,而不是试图让前端“不创建”。



















