微信小程序里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,快速发现问题,解决问题。

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

相关推荐
m0_7482382731 分钟前
项目升级Sass版本或升级Element Plus版本遇到的问题
前端·rust·sass
升讯威在线客服系统1 小时前
如何通过 Docker 在没有域名的情况下快速上线客服系统
java·运维·前端·python·docker·容器·.net
AsBefore麦小兜1 小时前
Vite vs Webpack
前端·webpack
LaughingZhu1 小时前
PH热榜 | 2025-02-23
前端·人工智能·经验分享·搜索引擎·产品运营
道不尽世间的沧桑3 小时前
第17篇:网络请求与Axios集成
开发语言·前端·javascript
diemeng11194 小时前
AI前端开发技能变革时代:效率与创新的新范式
前端·人工智能
bin91536 小时前
DeepSeek 助力 Vue 开发:打造丝滑的复制到剪贴板(Copy to Clipboard)
前端·javascript·vue.js·ecmascript·deepseek
晴空万里藏片云8 小时前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一8 小时前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球8 小时前
el-button按钮的loading状态设置
前端·javascript