一.适应场景
前端需要订阅后端的某个通知(也就是服务端主动向前端浏览器推送一个消息)。可以实现的方法有:
- 较为传统的方式是前端调接口轮询(也就是后端提供一个接口给前端,前端每隔几秒钟调一次看能否拿到想要的通知,没拿到就继续调)
- 使用WebSocket通信
- 使用SSE通信
二.这几种通信之间的区别
- 传统的轮询是最笨的,最不优雅的方式,消息通知也最不及时的方式。但该方式兼容面广,最不容易出问题。
- WebSocket是可以支持双向通信的,也就是前端可以主动给后端推送消息,后端也可以主动给前端推送消息。所以也更强大和灵活。但WebSocket是一个和Http不一样的独立协议,该协议相对复杂。而且如果连接断了需要自己实现连接重连。
- SSE只能后端主动给前端推送消息。前端只能接收消息。而不能反向把消息推送回去。SSE使用的是Http协议,相对于WebSocket来说属于轻量级,使用简单。SSE默认断线会自动重连,不需要用户处理。SSE支持自定义发生的消息类型
总结:如果只是需要前端接收后端的通知,而不需要反向发送回去。那就可以用SSE。其实有的网页的双向聊天也是用的sse. 它通过一个sse连接接收消息。然后又用一个专门的一般的http接口发送消息
三.SSE相关API
- 全局window对象下有一个EventSource构造函数,通过实例化该构造函数可以生成一个SSE对象
ts代码解读复制代码const sseEventObj = new EventSource(url); // 这个sseEventObj就是生成的SSE对象,url就是你后端的sse地址
- open事件,只要sse成功连接到后台就会立即触发open事件
ts代码解读复制代码// 事件监听 sseEventObj.onopen = function () { console.log(''SSE已成功打开'') };
- error事件,当sse连接断开时会触发该事件
ts代码解读复制代码// 事件监听 sseEventObj.onopen = function () {sseEventObj.onerror = function (event) { console.warn(''SSE连接断开'', event) };
- message事件,当后台通过sse发送message类型的消息通知时,会触发该事件
ts代码解读复制代码// 接收到消息 sseEventObj.onmessage = function (event) { console.log(''SSE接收到的message类型消息:'', event) };
- close关闭SSE连接
ts代码解读复制代码sseEventObj.close()
默认情况下后端发过来数据事件应该是message类型。但也可以自定义其他的类型(需要前后端一起定义好)。
ts代码解读复制代码// 假设前后端一起定义了一个叫custom的事件。这��情况下发送回来的数据就会触发这个custom监听,而不会触发原来的message监听 sseEventObj.addEventListener(''custom'', event => { })
四.后端实现
后端实现可以参考阮一峰老师的教程, 当初博主自己就是参考该教程,自己用node写了一个后端的sse demo。 再用前端的sse连接和该demo进行联调。没问题之后才去找我们java后台的同事对接的(好处就是你能够保证自己前端的写法肯定是没问题,可以调通的,如果调不通那大概率就是后台的同事写法有问题)
五.可能遇到的问题
博主遇到过sse连接没多久就断开了。并且无法重连。后来排查发现是 nginx 服务器的接口连接最大时长限制导致的。后来把这个时长设置为一个超级长的时间就解决了。中间还遇到过若干其他问题,解决完就忘了……,但这些问题都不是前端写法导致的。多是服务器导致的
六.SSE请求封装
因为sse用的人还不多。所以博主也没找到相关的封装demo。所以自己摸索着封装了一套。如果各位有更好的封装方案。欢迎发给博主互相借鉴学习
- 我司的业务需求是会有很多种类的SSE通知。并且这些通知是需要从打开网页就开始监听。直到网页关闭才停止。但一个域名下SSE连接的数量是有限制的。也不推荐创建多个SSE通知连接。所以我司的方案是:一进入系统就创建一个sse连接,该连接只有在退出系统后才会关闭。在整个系统打开期间,所有类型的sse通知都是通过这一个连接发过来的。发过来的消息内容里面前后端共同定义了一个messageType字段。该字段用于区分该消息的类型。前端接收到消息后先读取messageType的值,然后根据该值选择不同的消息处理方法
- 我司定义的messageType类型:
ts代码解读复制代码// sse通知类型枚举 export enum EventType { ''steadyCalcStart'' = ''0-0'', //稳态计算开始 ''steadyCalcEnd'' = ''0-1'', //稳态计算结束 ''sandBoxSteadyCalcStart'' = ''1-0'', //沙箱稳态计算开始 ''sandBoxSteadyCalcEnd'' = ''1-1'', //沙箱稳态计算结束 ''traceCalcStart'' = ''2-0'', //溯源计算开始 ''traceCalcEnd'' = ''2-1'', //溯源计算结束 ''scada'' = ''3'', //手动拉取scada通知 ''eventAlarm'' = ''4'', // 事件告警通知 ''dynCalcStart'' = ''5-0'', // 瞬态计算开始 ''dynCalcEnd'' = ''5-1'', // 瞬态计算结束 ''dynCalcProgress'' = ''5-2'', //瞬态计算进度 ''sandBoxDynCalcStart'' = ''6-0'', // 瞬态计算开始 ''sandBoxDynCalcEnd'' = ''6-1'', // 瞬态计算结束 ''sandBoxDynCalcProgress'' = ''6-2'', //瞬态计算进度 ''dataAnomaly'' = ''7'' // 数据异常 }
- 接收到sse消息后判断messageType的值,然后选择对应的消息处理方法
ts代码解读复制代码let sseEventObj: any = null; /** * @description: 创建SSE连接 */ export function createSSE() { if(sseEventObj) return; sseEventObj = new EventSource(url); // 事件监听 sseEventObj.onopen = function () { console.log(''SSE已成功打开'') }; sseEventObj.onerror = function (event) { console.warn(''SSE连接断开'', event) }; // 接收到消息 sseEventObj.onmessage = function (event) { console.log(''SSE接收到消息:'', event) const result: { messageType: string, messageData: any } = JSON.parse(event.data) console.log(result) // sseObj对象上挂载了不同messageType的值对应的处理方法 if(sseObj[result.messageType]) { for(const item of sseObj[result.messageType]) { item(result.messageData) } }else { console.warn(''未检测到sse监听对象:'', result.messageData) } }; }
- sseObj类封装:挂载不同messageType的值对应的处理方法
ts代码解读复制代码import { EventType } from ''..''; // sse消息监听对象 class SSE { /** * @description: 添加消息监听 // 如果有多个地方监听同一个EventType,多个监听会同时触发 * @param {EventType} type 监听类型 * @param {function} handle 监听到数据时的回调 * @return {function} 移除监听 */ addMessageListen(type: EventType, handle: (data: any) => void) { if(!this[type]) this[type] = []; this[type].push(handle); // eslint-disable-next-line @typescript-eslint/no-this-alias const _this = this; return { destroy: function () { _this.deleteMessageListen(type, handle); }, listenFun: handle } } /** * @description: 移除消息监听 * @param {string} type 监听类型 * @param {function} listenFun 要移除的监听方法对象 * @return {boolean} 是否移除成功过 */ deleteMessageListen(type: EventType, listenFun: (data: any) => void) { if(this[type]) { for(let i = 0; i < this[type].length; i++) { if(this[type][i] === listenFun) { this[type].splice(i, 1); return true } } } console.warn(''所要删除的类型或监听对象不存在:'', type, listenFun) return false } /** * @description: 清空该类型下所有监听 * @param {EventType} type * @return {*} */ clearMessageListen(type: EventType) { this[type] = []; } } const sseObj = new SSE(); export default sseObj
- messageType对应的处理方法挂载
ts代码解读复制代码// 1.瞬态计算完成 sseObj.addMessageListen(EventType.dynCalcEnd, (data) => { let message = `<p>计算名称:${data.calcName}</p><p>状态:${calcStatusLabel[data.status]}</p>` if(!calcSuccessStatus.includes(data.status)) message += `<p>信息:${data.message}</p>` Notice.messageBox({ title: ''瞬态计算结束'', message: message, Html: true, type: calcSuccessStatus.includes(data.status)? ''success'':''error'' }) }) // 如果只进行一次监听,监听完后就移除挂载 const listenObj = sseObj.addMessageListen(EventType.traceCalcEnd, (data) => { // 接收到消息后就把该挂载方法销毁 listenObj.destroy(); let message: string; if(data.isTrace===1) { message = data.message; } else { message = ''溯源状态码错误'' } calcId.value = '''' })