微信小程序里STOMP的使用和踩坑总结

STOMP 概念

STOMP 是一种简单易用的长链接协议,他是一种基于帧frame的协议,其帧在HTTP上建模。

它内部实现了心跳检测,使用的是window.setInterval

也可以根据环境自定义心跳包。

自定义长链接

STOMP 通过 over 方法接收自定义长链接对象。

微信小程序长链接

1. wx.connectSocket

使用 wx.connectSocket 创建长链接,它会返回一个 socketTask

js 复制代码
let socketTask = wx.connectSocket({
    url: socketUrl,
});

2. 监听 WebSocket 连接打开事件

js 复制代码
socketTask.onOpen((frame) => {
    // 处理打开事件
});

3. 监听 WebSocket 接收到服务器的消息事件

js 复制代码
socketTask.onMessage((frame) => {
    // 处理接收消息
});

4. 监听 WebSocket 连接关闭事件

js 复制代码
socketTask.onClose((frame) => {
    // 处理关闭事件,比如重连
});

与STOMP绑定

1. 创建 ws 对象与 socketTask 绑定

创建wx.connectSocket后,创建 ws 对象

scss 复制代码
let ws = {
    send(frame) {
      socketTask.send({ data: frame });
    },
    close(frame) {
      socketTask.close(frame);
    },
};
  • 定义 send 方法,内部使用 socketTask.send 发送消息
  • 定义 close 方法,内部使用 socketTask.close 关闭长链接

2. 修改 socketTask.onOpen 绑定 open 事件

js 复制代码
socketTask.onOpen((frame) => {
+    ws.onopen(frame);
+    if (this.RECONNECT_COUNT > 0) { // 连接成功后回调
+      this.reconnectCallback && this.reconnectCallback();
+    }
+    this.RECONNECT_COUNT = 0;
});

3. 修改 socketTask.onMessage 绑定 onmessage 事件

js 复制代码
socketTask.onMessage((frame) => {
    // 处理接收消息
+    ws.onmessage(frame);
});

4. 修改 socketTask.onClose 绑定 close 事件

js 复制代码
socketTask.onClose((frame) => {
    // 处理关闭事件,比如重连
+    this.stompClient._cleanUp(); // 清理 stomp客户端
+    setTimeout(() => {
+       // 处理重连逻辑代码
+       // ......省略代码
+    }, this.RECONNECT_TIME_INTERVAL);
});

5. 使用 Stomp.over 方法绑定 ws 对象

js 复制代码
let stompClient = Stomp.over(ws);
this.stompClient = stompClient;

6. Stomp 客户端进行连接

js 复制代码
stompClient.connect(header, (frame) => {
    let timer = setInterval(() => {
      let socketStatus = this.getSocketStatus();
      if (socketStatus) {
        clearInterval(timer);
        return;
      }
      this.connectCallback = connectCallback; // 连接成功的回调
      this.socketTask = socketTask; // wx.connectSocket返回的对象
      this.reconnectCallback = reconnectCallback; // 重连的回调
      connectCallback(stompClient); // 连接回调后可以订阅
    }, 60);
});

7. 订阅subscribe

js 复制代码
stompClient.subscribe(
    `/topic/message/${this.roomId}`,
    (message) => {
      const wsResult = JSON.parse(message.body);
      this.handleWSInfo(wsResult);
    }
)

它会返回一个对象,可以调用对象的 unsubscribe 取消订阅。

完整代码

stomp 代码

raw.githubusercontent.com/jmesnil/sto...

封装微信小程序长链接并与stomp绑定

js 复制代码
import { Stomp } from './stomp';

class WebSocket {
  /**
   * 微信 WebSocket 任务
   */
  socketTask = null;

  /**
   * Stomp代理
   */
  stompClient = null;

  /**
   * 默认监听的消息频道
   */
  channel = null;

  /**
   * 重连成功的回调
   */
  reconnectCallback = null;

  /**
   * 主动断开连接的标识
   */
  disconnectFlag = false;

  /**
   * 默认最大重连次数
   */
  RECONNECT_MAX_COUNT = 30;

  /**
   * 默认重连时间间隔(单位:ms)
   */
  RECONNECT_TIME_INTERVAL = 1000;

  /**
   * 断线重连计数
   */
  RECONNECT_COUNT = 0;

  constructor() {
    /* setInterval是用来发心跳包的,而小程序没有window对象 */
    Stomp.setInterval = function (interval, f) {
      return setInterval(f, interval);
    };
    Stomp.clearInterval = function (id) {
      return clearInterval(id);
    };
  }

  /**
   * 建立websocket连接和频道监听,绑定消息处理器
   * @param header            消息头
   * @param socketUrl      连接地址
   * @param connectCallback  连接成功回调
   * @param reconnectCallback 重连成功回调
   */
  init(params) {
    const {
      header = {},
      socketUrl,
      connectCallback,
      reconnectCallback,
    } = params;

    if (!this.getSocketStatus()) {
      let socketTask = wx.connectSocket({
        url: socketUrl,
      });

      let ws = {
        send(frame) {
          socketTask.send({ data: frame });
        },
        close(frame) {
          socketTask.close(frame);
        },
      };

      socketTask.onOpen((frame) => {
        ws.onopen(frame);
        if (this.RECONNECT_COUNT > 0) {
          this.reconnectCallback && this.reconnectCallback();
        }
        this.RECONNECT_COUNT = 0;
      });

      socketTask.onMessage((frame) => {
        console.log('websocket接收消息', frame);
        ws.onmessage(frame);
      });

      socketTask.onClose((frame) => {
        this.stompClient._cleanUp();
        /* 客户端主动断开连接,不启动重连。 */
        if (this.disconnectFlag) {
          this.disconnectFlag = false;
          return;
        }
        setTimeout(() => {
          this.RECONNECT_COUNT += 1;

          if (this.RECONNECT_COUNT >= this.RECONNECT_MAX_COUNT) {
            console.log('websocket连接失败');
            return;
          }

          this.init({
            header,
            socketUrl,
            connectCallback,
            reconnectCallback: this.reconnectCallback,
          });
        }, this.RECONNECT_TIME_INTERVAL);
      });

      let stompClient = Stomp.over(ws);
      this.stompClient = stompClient;

      stompClient.connect(header, (frame) => {
        let timer = setInterval(() => {
          let socketStatus = this.getSocketStatus();
          if (socketStatus) {
            clearInterval(timer);
            return;
          }
          this.connectCallback = connectCallback;

          this.socketTask = socketTask;
          this.reconnectCallback = reconnectCallback;
          connectCallback(stompClient);
        }, 60);
      });
    }
  }

  /**
   * 发送消息
   * @param channel 频道
   * @param header  消息头
   * @param body    消息体
   */
  sendMessage(channel, header, body) {
    if (this.getSocketStatus()) {
      this.stompClient.send(channel, header, JSON.stringify(body));
    }
  }

  /**
   * 关闭连接
   */
  close() {
    if (this.getSocketStatus()) {
      this.stompClient.disconnect();
      this.disconnectFlag = true;
    }
  }

  /**
   * 获取连接状态
   * @return boolean
   */
  getSocketStatus() {
    let boolean = false;
    if (this.socketTask && this.socketTask.readyState) {
      boolean = this.socketTask.readyState === 1;
    }
    return boolean;
  }
}

export default WebSocket;

注意:constructor 函数里自定义了心跳包

封装本地

js 复制代码
import WebSocket from '@/utils/websocket';
import { socketUrl } from '@/api/config';
import tokens from './tokens';
let wsStomp = null;
const WS_URL = `${socketUrl}/ws/stomp`;
export const SEND_URL = '/app/message/';
let connectCount = 0;

export function initWsStomp(roomId, connectCallback, reconnectCallback) {
  if (wsStomp) {
    connectCallback(wsStomp.stompClient);
    return wsStomp;
  }
  wsStomp = new WebSocket();
  const token = tokens.get();
  const params = {
    socketUrl: `${WS_URL}?Authorization=${token}`,
    connectCallback,
    reconnectCallback,
    header: {
      Authorization: `${token}`,
    },
  };
  wsStomp.init(params);
}

export function closeWsStomp() {
  if (wsStomp) {
    wsStomp.close();
    wsStomp = null;
  }
}

export function sendWsStomp(roomId, message, header = {}) {
  const token = tokens.get();
  if (roomId && wsStomp) {
    wsStomp.sendMessage(
      `${SEND_URL}${roomId}`,
      {
        Authorization: `${token}`,
        ...header,
      },
      message
    );
  }
}

使用

  1. 在业务合适的时机调用 initWsStomp
js 复制代码
initWsStomp(
    this.roomId,
    (stompClient) => {
      // 连接成功开始订阅
      this.topicUnsub = stompClient.subscribe(
        `/topic/message/${this.roomId}`,
        (message) => {
          // 接收服务器推送的消息
          const wsResult = JSON.parse(message.body);
          this.handleWSInfo(wsResult);
        }
      );
      // 第二个订阅
      this.userUnsub = stompClient.subscribe(
        `/user/${uid}/message/${this.roomId}`,
        (message) => {
          // 接收服务器推送的消息
          const wsResult = JSON.parse(message.body);
          this.handleWSInfo(wsResult);
        }
      );
    },
    () => {
      console.log('reconnectResult');
    }
  );
  1. 在合适的时机取消订阅
js 复制代码
if (this.topicUnsub) {
    this.topicUnsub.unsubscribe();
}
if (this.userUnsub) {
    this.userUnsub.unsubscribe();
}

比如页面跳转,就需要取消当前页面长链接的订阅 3. 合适的时机关闭长链接

js 复制代码
closeWsStomp();

踩坑

iPhone 手机总断链

苹果手机接收消息的最后一个字符不是 0x00 导致内部进行分割失败。

修复方法是在 onMessage 里判断最后一个字符是否是0x00,不是就添加上。

js 复制代码
socketTask.onMessage((frame) => {
+    if (frame && frame.data) {
+      let value = frame.data;
+      let code = value.charCodeAt(value.length - 1);
+      if (code !== 0x00) {
+        value += String.fromCharCode(0x00);
+        frame.data = value;
+      }
+    }
    ws.onmessage(frame);
});

有时候客户端发送消息后收不到对应的消息

客户端可能发送了一个不在前后端约定的 COMMAND 里,导致收不到消息。

最好让服务端再接收到消息后,如果 COMMAND 不在处理范围内,返回一个 ERROR

同一个页面处理两种身份的逻辑

长链接是为了即时处理状态。当一方状态变更,另一方也需要即时知道。

如果把两种身份处理逻辑混在一个页面或组件里,会很难处理。需要很多标识。

简单处理方式就是,按身份抽取组件。

在微信小程序里,可以根据 onloadquery 获取不同的身份,然后渲染不同身份的逻辑,这样简单,不容易混乱。

调试

微信小程序里长链接调试很不方便,开发者工具是抓不到长链接的包的。

特别类似双人对战这种模式,在手机上只能看到日志,不能 debug。

如果可以,找同事的电脑配合,用两台电脑,就有两个开发者工具,这样就可以 debug,快速发现问题,解决问题。

欢迎关注微信公众号 闹闹前端

相关推荐
豆豆(设计前端)8 分钟前
总结 Vue 请求接口的各种类型及传参方式
前端·javascript·vue.js
一生躺平的仔30 分钟前
# Rust遇上WebAssembly:让JavaScript的计算性能起飞!🚀
前端·javascript
IT 古月方源32 分钟前
跨站脚本攻击(XSS)详解
运维·服务器·前端·网络·tcp/ip·网络安全·xss
你的码,就是我的码37 分钟前
web服务器架构,websocket
服务器·前端·架构
彭友圈1011 小时前
HTML基础入门——简单网页页面
前端·前端框架·html
Onlooker-轩逸1 小时前
WebSocket
网络·websocket·网络协议
m0_749317522 小时前
VUE学习
前端·javascript·vue.js·学习
16年上任的CTO2 小时前
一文大白话讲清楚ES6关于函数的扩展
前端·javascript·ecmascript·es6·es6函数扩展
yuehua_zhang2 小时前
uni app 写的 小游戏,文字拼图?文字拼写?不知道叫啥
前端·javascript·uni-app
weixin_472183542 小时前
uniapp使用sm4加密
前端·javascript·uni-app