broadcastingManager.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /**
  2. * Laravel Reverb 广播统一管理模块
  3. * 提供通用的 WebSocket 连接管理、频道订阅和事件处理功能
  4. */
  5. class BroadcastingManager {
  6. constructor() {
  7. this.channels = new Map();
  8. this.pollingIntervals = new Map();
  9. this.errorDisplayed = false;
  10. this.connectionState = "unknown";
  11. }
  12. /**
  13. * 检查 Echo 是否可用
  14. * @returns {boolean}
  15. */
  16. isEchoAvailable() {
  17. return typeof Echo !== "undefined" && Echo !== null;
  18. }
  19. /**
  20. * 检查连接是否正常
  21. * @returns {boolean}
  22. */
  23. isConnected() {
  24. if (!this.isEchoAvailable()) return false;
  25. const conn = this.getConnection();
  26. if (!conn) return false;
  27. const state = conn.state?.current ?? conn.readyState;
  28. return state === "connected" || state === "open" || state === 1;
  29. }
  30. /**
  31. * 获取连接对象
  32. * @returns {Object|null}
  33. */
  34. getConnection() {
  35. if (!this.isEchoAvailable()) return null;
  36. return (
  37. Echo.connector?.pusher?.connection || Echo.connector?.socket || null
  38. );
  39. }
  40. /**
  41. * 显示错误信息
  42. * @param {string} message
  43. */
  44. handleError(message) {
  45. if (!this.errorDisplayed && !this.isConnected()) {
  46. if (typeof showMessage !== "undefined") {
  47. showMessage({
  48. title: i18n("broadcast.error"),
  49. message: message,
  50. icon: "error",
  51. showConfirmButton: true,
  52. });
  53. } else {
  54. console.error(message);
  55. }
  56. this.errorDisplayed = true;
  57. }
  58. }
  59. /**
  60. * 清除错误状态
  61. */
  62. clearError() {
  63. this.errorDisplayed = false;
  64. }
  65. /**
  66. * 订阅频道并监听事件
  67. * @param {string} channelName - 频道名称
  68. * @param {string} event - 事件名称
  69. * @param {Function} handler - 事件处理函数
  70. * @returns {boolean} 是否订阅成功
  71. */
  72. subscribe(channelName, event, handler) {
  73. // 清理同名频道(如果存在)- 确保彻底清除旧监听器
  74. this.unsubscribe(channelName);
  75. if (!this.isEchoAvailable()) {
  76. this.handleError(i18n("broadcast.websocket_unavailable"));
  77. return false;
  78. }
  79. try {
  80. // 创建新频道并监听事件
  81. const channel = Echo.channel(channelName);
  82. channel.listen(event, handler);
  83. this.channels.set(channelName, channel);
  84. // 绑定连接状态事件
  85. const conn = this.getConnection();
  86. if (conn?.bind) {
  87. conn.bind("connected", () => {
  88. this.connectionState = "connected";
  89. this.clearError();
  90. });
  91. conn.bind("disconnected", () => {
  92. this.connectionState = "disconnected";
  93. this.handleError(i18n("broadcast.websocket_disconnected"));
  94. });
  95. }
  96. return true;
  97. } catch (e) {
  98. if (!this.isConnected()) {
  99. this.handleError(
  100. `${i18n("broadcast.setup_failed")}: ${e?.message || e}`,
  101. );
  102. }
  103. return false;
  104. }
  105. }
  106. /**
  107. * 取消订阅频道
  108. * @param {string} channelName
  109. */
  110. unsubscribe(channelName) {
  111. if (this.channels.has(channelName)) {
  112. try {
  113. // Laravel Echo 官方推荐方式:直接调用 Echo.leave()
  114. // 它会自动清除频道对象、所有监听器和内部缓存
  115. if (typeof Echo.leave === "function") {
  116. Echo.leave(channelName);
  117. }
  118. } catch (e) {
  119. console.warn(`Failed to unsubscribe from ${channelName}:`, e);
  120. }
  121. this.channels.delete(channelName);
  122. }
  123. }
  124. /**
  125. * 处理 AJAX 请求的错误 - 统一错误处理逻辑
  126. * @param {string} title - 错误标题
  127. * @param {string} message - 错误消息
  128. */
  129. handleAjaxError(title = null, message = null) {
  130. if (!this.isConnected()) {
  131. this.handleError(i18n("broadcast.websocket_unavailable"));
  132. } else if (message || title) {
  133. if (typeof showMessage !== "undefined") {
  134. showMessage({
  135. title: title || i18n("common.error"),
  136. message: message,
  137. icon: "error",
  138. showConfirmButton: true,
  139. });
  140. } else {
  141. console.error(title, message);
  142. }
  143. }
  144. }
  145. /**
  146. * 生成频道名称
  147. * @param {string} type - 频道类型
  148. * @param {string|number} id - 资源 ID(可选)
  149. * @returns {string} 频道名称
  150. */
  151. getChannelName(type, id = null) {
  152. return id ? `${type}.${id}` : `${type}.all`;
  153. }
  154. /**
  155. * 清理所有频道
  156. */
  157. cleanup() {
  158. for (const channelName of this.channels.keys()) {
  159. this.unsubscribe(channelName);
  160. }
  161. this.channels.clear();
  162. }
  163. /**
  164. * 启动轮询降级机制
  165. * @param {string} intervalId - 轮询ID
  166. * @param {Function} pollFunction - 轮询函数
  167. * @param {number} interval - 轮询间隔(毫秒)
  168. */
  169. startPolling(intervalId, pollFunction, interval = 3000) {
  170. this.stopPolling(intervalId);
  171. const pollInterval = setInterval(pollFunction, interval);
  172. this.pollingIntervals.set(intervalId, pollInterval);
  173. return pollInterval;
  174. }
  175. /**
  176. * 停止轮询
  177. * @param {string} intervalId
  178. */
  179. stopPolling(intervalId) {
  180. if (this.pollingIntervals.has(intervalId)) {
  181. clearInterval(this.pollingIntervals.get(intervalId));
  182. this.pollingIntervals.delete(intervalId);
  183. }
  184. }
  185. /**
  186. * 停止所有轮询
  187. */
  188. stopAllPolling() {
  189. for (const intervalId of this.pollingIntervals.keys()) {
  190. this.stopPolling(intervalId);
  191. }
  192. this.pollingIntervals.clear();
  193. }
  194. /**
  195. * 断开 Echo 连接
  196. */
  197. disconnect() {
  198. try {
  199. if (this.isEchoAvailable()) {
  200. Echo.connector?.disconnect?.();
  201. }
  202. } catch (e) {
  203. console.error(i18n("broadcast.disconnect_failed"), e);
  204. }
  205. }
  206. /**
  207. * 等待连接建立
  208. * @param {number} timeout - 超时时间(毫秒)
  209. * @returns {Promise<boolean>}
  210. */
  211. waitForConnection(timeout = 5000) {
  212. return new Promise((resolve) => {
  213. if (this.isConnected()) {
  214. resolve(true);
  215. return;
  216. }
  217. const startTime = Date.now();
  218. const checkConnection = () => {
  219. if (this.isConnected()) {
  220. resolve(true);
  221. } else if (Date.now() - startTime > timeout) {
  222. resolve(false);
  223. } else {
  224. setTimeout(checkConnection, 100);
  225. }
  226. };
  227. checkConnection();
  228. });
  229. }
  230. }
  231. // 导出单例
  232. const broadcastingManager = new BroadcastingManager();
  233. export default broadcastingManager;