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

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

相关推荐
叁两1 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
golang学习记1 小时前
GitLens 十大神技:彻底改变你在 VS Code 中的 Git 工作流
前端·后端·visual studio code
SuperEugene1 小时前
后台权限与菜单渲染:基于路由和后端返回的几种实现方式
前端·javascript·vue.js
兆子龙1 小时前
WebSocket 入门:是什么、有什么用、脚本能帮你做什么
前端·架构
是一碗螺丝粉1 小时前
LangChain 链(Chains)完全指南:从线性流程到智能路由
前端·langchain·aigc
月弦笙音1 小时前
【浏览器】这几点必须懂
前端
青青家的小灰灰1 小时前
迈向全栈新时代:SSR/SSG 原理、Next.js 架构与 React Server Components (RSC) 实战
前端·javascript·react.js
SuperEugene1 小时前
弹窗与抽屉组件封装:如何做一个全局可控的 Dialog 服务
前端·javascript·vue.js
UrbanJazzerati1 小时前
事件传播机制详解(附直观比喻和代码示例)
前端
青青家的小灰灰1 小时前
透视 React 内核:Diff 算法、合成事件与并发特性的深度解析
前端·javascript·react.js