SSE(server-sent Event)服务端主动向前端浏览器推送消息

6,454 阅读6分钟

一.适应场景

前端需要订阅后端的某个通知(也就是服务端主动向前端浏览器推送一个消息)。可以实现的方法有:

  1. 较为传统的方式是前端调接口轮询(也就是后端提供一个接口给前端,前端每隔几秒钟调一次看能否拿到想要的通知,没拿到就继续调)
  2. 使用WebSocket通信
  3. 使用SSE通信

二.这几种通信之间的区别

  1. 传统的轮询是最笨的,最不优雅的方式,消息通知也最不及时的方式。但该方式兼容面广,最不容易出问题。
  2. WebSocket是可以支持双向通信的,也就是前端可以主动给后端推送消息,后端也可以主动给前端推送消息。所以也更强大和灵活。但WebSocket是一个和Http不一样的独立协议,该协议相对复杂。而且如果连接断了需要自己实现连接重连。
  3. SSE只能后端主动给前端推送消息。前端只能接收消息。而不能反向把消息推送回去。SSE使用的是Http协议,相对于WebSocket来说属于轻量级,使用简单。SSE默认断线会自动重连,不需要用户处理。SSE支持自定义发生的消息类型

总结:如果只是需要前端接收后端的通知,而不需要反向发送回去。那就可以用SSE。其实有的网页的双向聊天也是用的sse. 它通过一个sse连接接收消息。然后又用一个专门的一般的http接口发送消息

三.SSE相关API

  • 全局window对象下有一个EventSource构造函数,通过实例化该构造函数可以生成一个SSE对象
const sseEventObj = new EventSource(url); 
// 这个sseEventObj就是生成的SSE对象,url就是你后端的sse地址
  • open事件,只要sse成功连接到后台就会立即触发open事件
// 事件监听
sseEventObj.onopen = function () {
  console.log('SSE已成功打开')
};
  • error事件,当sse连接断开时会触发该事件
// 事件监听
sseEventObj.onopen = function () {sseEventObj.onerror = function (event) {
  console.warn('SSE连接断开', event)
};
  • message事件,当后台通过sse发送message类型的消息通知时,会触发该事件
// 接收到消息
sseEventObj.onmessage = function (event) {
  console.log('SSE接收到的message类型消息:', event)
};
  • close关闭SSE连接
sseEventObj.close()

默认情况下后端发过来数据事件应该是message类型。但也可以自定义其他的类型(需要前后端一起定义好)。

// 假设前后端一起定义了一个叫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类型:
// 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的值,然后选择对应的消息处理方法
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的值对应的处理方法
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对应的处理方法挂载
// 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 = ''
})

结束