1. socket生命周期监听、断线重连、心跳检测 2. socket消息校验、生成、解析 3. 观察者模式分发消息
已修改3个文件
已添加4个文件
267 ■■■■■ 文件已修改
src/App.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/index.js 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components.d.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/enum/socketMessage.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/socket/eventBus.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/socket/index.js 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/socket/socketMessage.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/App.vue
@@ -40,13 +40,15 @@
<script>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs';
import { FYWebSocket } from '@/socket/index.js'
import eventBus from '@/socket/eventBus.js';
export default {
  data() {
    return {
      isCollapsed: false,
      navTitles: [],
      locale: zhCn,
      socket: null,
    };
  },
  methods: {
@@ -56,7 +58,26 @@
    navPage(titles) {
      this.navTitles = titles;
    },
    // è¿žæŽ¥websocket
    connectWebSocket() {
      this.socket = new FYWebSocket()
      this.socket.init({
        time: 4 * 1000,
        timeout: 2 * 1000,
        reconnect: 3 * 1000
      }, true)
    },
    // æ–­çº¿é‡è¿žwebsocket
    startReconnectWebSocket() {
      eventBus.register('reconnect', () => {
        this.connectWebSocket()
      })
    }
  },
  created() {
    this.connectWebSocket()
    this.startReconnectWebSocket()
  }
};
</script>
src/api/index.js
@@ -17,6 +17,9 @@
  // ip2_file = 'https://fyami.com.cn/';
}
// socket
const $socket_base_url = 'ws://192.168.0.150:8080/workstream'
//飞羽监管
const $fysp = axios.create({
  baseURL: ip1,
@@ -106,4 +109,4 @@
  );
});
export { $fysp, $fytz };
export { $fysp, $fytz, $socket_base_url };
src/components.d.ts
@@ -13,32 +13,22 @@
    CompGenericWrapper: typeof import('./components/CompGenericWrapper.vue')['default']
    CompQuickSet: typeof import('./components/search-option/CompQuickSet.vue')['default']
    Content: typeof import('./components/core/Content.vue')['default']
    ElAffix: typeof import('element-plus/es')['ElAffix']
    ElAside: typeof import('element-plus/es')['ElAside']
    ElAvatar: typeof import('element-plus/es')['ElAvatar']
    ElBadge: typeof import('element-plus/es')['ElBadge']
    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
    ElButton: typeof import('element-plus/es')['ElButton']
    ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
    ElCalendar: typeof import('element-plus/es')['ElCalendar']
    ElCard: typeof import('element-plus/es')['ElCard']
    ElCascader: typeof import('element-plus/es')['ElCascader']
    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
    ElCol: typeof import('element-plus/es')['ElCol']
    ElCollapse: typeof import('element-plus/es')['ElCollapse']
    ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
    ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
    ElContainer: typeof import('element-plus/es')['ElContainer']
    ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
    ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
    ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
    ElDialog: typeof import('element-plus/es')['ElDialog']
    ElDivider: typeof import('element-plus/es')['ElDivider']
    ElDrawer: typeof import('element-plus/es')['ElDrawer']
    ElDropdown: typeof import('element-plus/es')['ElDropdown']
    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
    ElEmpty: typeof import('element-plus/es')['ElEmpty']
    ElForm: typeof import('element-plus/es')['ElForm']
    ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -47,29 +37,19 @@
    ElImage: typeof import('element-plus/es')['ElImage']
    ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
    ElInput: typeof import('element-plus/es')['ElInput']
    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
    ElLink: typeof import('element-plus/es')['ElLink']
    ElMain: typeof import('element-plus/es')['ElMain']
    ElMenu: typeof import('element-plus/es')['ElMenu']
    ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
    ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
    ElOption: typeof import('element-plus/es')['ElOption']
    ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
    ElPagination: typeof import('element-plus/es')['ElPagination']
    ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
    ElPopover: typeof import('element-plus/es')['ElPopover']
    ElRadio: typeof import('element-plus/es')['ElRadio']
    ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
    ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
    ElRow: typeof import('element-plus/es')['ElRow']
    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
    ElSegmented: typeof import('element-plus/es')['ElSegmented']
    ElSelect: typeof import('element-plus/es')['ElSelect']
    ElSpace: typeof import('element-plus/es')['ElSpace']
    ElStep: typeof import('element-plus/es')['ElStep']
    ElSteps: typeof import('element-plus/es')['ElSteps']
    ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
    ElSwitch: typeof import('element-plus/es')['ElSwitch']
    ElTable: typeof import('element-plus/es')['ElTable']
    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
    ElTabPane: typeof import('element-plus/es')['ElTabPane']
@@ -77,7 +57,6 @@
    ElTag: typeof import('element-plus/es')['ElTag']
    ElText: typeof import('element-plus/es')['ElText']
    ElTooltip: typeof import('element-plus/es')['ElTooltip']
    ElTransfer: typeof import('element-plus/es')['ElTransfer']
    ElTree: typeof import('element-plus/es')['ElTree']
    ElUpload: typeof import('element-plus/es')['ElUpload']
    Footer: typeof import('./components/core/Footer.vue')['default']
src/enum/socketMessage.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
const messageTypes = [
  { id: 0, label: '心跳检测', value: '0' },
  { id: 1, label: '后台任务', value: '1' },
  { id: 2, label: '业务日志', value: '2' }
];
export { messageTypes };
src/socket/eventBus.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
// è§‚察者模式
class EventBus {
    constructor() {
      // æ¶ˆæ¯ä¸­å¿ƒï¼Œè®°å½•了所有的事件 ä»¥åŠ äº‹ä»¶å¯¹åº”的处理函数
      this.subs = Object.create(null)
    }
    // æ³¨å†Œæ—¶é—´
    // å‚数:1.事件名称  2.事件处理函数
    register(eventType, handler) {
      this.subs[eventType] = this.subs[eventType] || []
      this.subs[eventType].push(handler)
    }
    // è§¦å‘事件
    // å‚数: 1.事件名称 2.接收的参数
    emit(eventType, ...ars) {
      if(this.subs[eventType]) {
        this.subs[eventType].forEach(handler => {
          handler(...ars)
        })
      }
    }
  }
  export default new EventBus()
src/socket/index.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,134 @@
import eventBus from './eventBus.js';
import { messageTypes } from '@/enum/socketMessage.js';
import { encodeMessage, decodeMessage } from './socketMessage.js';
import { $socket_base_url } from '@/api/index.js';
class FYWebSocket extends WebSocket {
  constructor() {
    super($socket_base_url);
    return this;
  }
  /**
   * heartBeatConfig å¿ƒè·³è¿žæŽ¥å‚æ•°
   *    time: å¿ƒè·³æ—¶é—´é—´éš”
   *    timeout: å¿ƒè·³è¶…æ—¶é—´éš”
   *    reconnect: æ–­çº¿é‡è¿žæ—¶é—´é—´éš”
   * isReconnect æ˜¯å¦æ–­çº¿é‡è¿ž
   */
  init(heartBeatConfig, isReconnect) {
    this.onopen = this.openHandler; // è¿žæŽ¥æˆåŠŸåŽçš„å›žè°ƒå‡½æ•°
    this.onclose = this.closeHandler; // è¿žæŽ¥å…³é—­åŽçš„回调 å‡½æ•°
    this.onmessage = this.messageHandler; // æ”¶åˆ°æœåŠ¡å™¨æ•°æ®åŽçš„å›žè°ƒå‡½æ•°
    this.onerror = this.errorHandler; // è¿žæŽ¥å‘生错误的回调方法
    this.heartBeatConfig = heartBeatConfig; // å¿ƒè·³è¿žæŽ¥é…ç½®å‚æ•°
    this.isReconnect = isReconnect; // è®°å½•是否断线重连
    this.reconnectTimer = null; // è®°å½•断线重连的时间器
    this.startHeartBeatTimer = null; // è®°å½•心跳时间器
    this.webSocketState = false; // è®°å½•socket连接状态 true为已连接
  }
  // èŽ·å–æ¶ˆæ¯
  getMessage({ data }) {
    return decodeMessage(data);
  }
  // å‘送消息
  sendMessage(type, data) {
    return this.send(encodeMessage(type, data));
  }
  // è¿žæŽ¥æˆåŠŸåŽçš„å›žè°ƒå‡½æ•°
  openHandler() {
    console.log('====onopen è¿žæŽ¥æˆåŠŸ====');
    // socket状态设置为连接,做为后面的断线重连的拦截器
    this.webSocketState = true;
    // åˆ¤æ–­æ˜¯å¦å¯åŠ¨å¿ƒè·³æœºåˆ¶
    if (this.heartBeatConfig && this.heartBeatConfig.time) {
      this.startHeartBeat(this.heartBeatConfig.time);
    }
  }
  // æ”¶åˆ°æœåŠ¡å™¨æ•°æ®åŽçš„å›žè°ƒå‡½æ•°
  messageHandler(res) {
    const webSocketMsg = this.getMessage(res);
    if (!(webSocketMsg && webSocketMsg != null && webSocketMsg != {})) {
      return;
    }
    const type = webSocketMsg.type;
    const data = webSocketMsg.data
    switch (type) {
      case '1': // åŽå°ä»»åŠ¡
        // å‘送事件
        eventBus.emit('1', data);
        console.log('====onmessage åŽå°ä»»åŠ¡====', data);
        break;
      case '2': // ä¸šåŠ¡æ—¥å¿—
        // å‘送事件
        eventBus.emit('2', data);
        console.log('====onmessage ä¸šåŠ¡æ—¥å¿—====', data);
        break;
      case '0': // å¿ƒè·³æ£€æµ‹
        // å°†è¿žæŽ¥çŠ¶æ€æ›´æ–°ä¸ºåœ¨çº¿
        this.webSocketState = true;
        eventBus.emit('0', data);
        console.log('====onmessage å¿ƒè·³æ£€æµ‹====', data);
        break;
    }
  }
  // è¿žæŽ¥å…³é—­åŽçš„回调 å‡½æ•°
  closeHandler() {
    console.log('====onclose websocket关闭连接====');
    // è®¾ç½®socket状态为断线
    this.webSocketState = false;
    // åœ¨æ–­å¼€è¿žæŽ¥æ—¶ æ¸…除心跳时间器和 æ–­å¼€é‡è¿žæ—¶é—´å™¨
    this.startHeartBeatTimer && clearTimeout(this.startHeartBeatTimer);
    this.reconnectTimer && clearTimeout(this.reconnectTimer);
    this.reconnectWebSocket();
  }
  errorHandler() {
    console.log('====onerror websocket连接出错====');
    // è®¾ç½®socket状态为断线
    this.webSocketState = false;
    // é‡æ–°è¿žæŽ¥
    this.reconnectWebSocket();
  }
  // å¿ƒè·³åˆå§‹åŒ–方法 time:心跳间隔
  startHeartBeat(time) {
    this.startHeartBeatTimer = setTimeout(() => {
      // å®¢æˆ·ç«¯æ¯éš”一段时间向服务端发送一个心跳消息
      this.sendMessage(messageTypes[0].value, Date.now());
      this.waitingServer();
    }, time);
  }
  //在客户端发送消息之后,延时等待服务器响应,通过webSocketState判断是否连线成功
  waitingServer() {
    this.webSocketState = false;
    setTimeout(() => {
      // è¿žçº¿æˆåŠŸçŠ¶æ€ä¸‹ ç»§ç»­å¿ƒè·³æ£€æµ‹
      if (this.webSocketState) {
        this.startHeartBeat(this.heartBeatConfig.time);
        return;
      }
      console.log('心跳无响应, å·²ç»å’ŒæœåŠ¡ç«¯æ–­çº¿');
      // é‡æ–°è¿žæŽ¥æ—¶ï¼Œè®°å¾—要先关闭当前连接
      try {
        this.close();
      } catch (error) {
        console.log('当前连接已经关闭');
      }
      // // é‡æ–°è¿žæŽ¥
      // this.reconnectWebSocket()
    }, this.heartBeatConfig.timeout);
  }
  // é‡æ–°è¿žæŽ¥
  reconnectWebSocket() {
    // åˆ¤æ–­æ˜¯å¦æ˜¯é‡æ–°è¿žæŽ¥çŠ¶æ€(即被动状态断线),如果是主动断线的不需要重新连接
    if (!this.isReconnect) {
      return;
    }
    // æ ¹æ®ä¼ å…¥çš„æ–­çº¿é‡è¿žæ—¶é—´é—´éš” å»¶æ—¶è¿žæŽ¥
    this.reconnectTimer = setTimeout(() => {
      // è§¦å‘重新连接事件
      eventBus.emit('reconnect');
    }, this.heartBeatConfig.reconnect);
  }
}
export { FYWebSocket };
src/socket/socketMessage.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
import { messageTypes } from '@/enum/socketMessage.js'
// å¼€å§‹ç¬¦å·å’Œç»“束符号分别为 '##' å’Œ '%%', åˆ†éš”符为 &&
// å¼€å§‹ç¬¦å·
const startStr = '##';
// åˆ†éš”符号
const splitStr = '&&';
// ç»“束符号
const endStr = '%%';
// æ ¡éªŒæ ¼å¼
function verificationMessahe(message) {
  if (!message || message == '') {
    return false;
  }
  if (!(typeof message == 'string')) {
    return false;
  }
  if (!message.startsWith(startStr)) {
    return false;
  }
  if (!message.endsWith(endStr)) {
    return false;
  }
  return true;
}
/**
 * è§£æžå‡ºç±»åž‹å’Œå†…容
 * @param {*} message socket消息中的data字段
 * @returns
 */
function decodeMessage(message) {
  if (!verificationMessahe(message)) {
    return;
  }
  const parts = message.slice(startStr.length, -endStr.length).split(splitStr);
  const type = parts[0];
  let data = JSON.parse(parts[1]);
  return {
    type: messageTypes.find((item) => item.value == type).value,
    data: data
  };
}
/**
 * ç”ŸæˆæŒ‡å®šæ ¼å¼çš„æ¶ˆæ¯å­—符串
 * @param {*} type æ¶ˆæ¯ç±»åž‹
 * @param {*} data æ¶ˆæ¯å†…容
 * @returns ç”Ÿæˆçš„æ¶ˆæ¯å­—符串
 */
function encodeMessage(type, data) {
  return `${startStr}${type}${splitStr}${JSON.stringify(data)}${endStr}`;
}
export { verificationMessahe, decodeMessage, encodeMessage, messageTypes };