uniapp 适配支付宝 / 微信小程序的 WebSocket 封装:解决重连、心跳、跨端兼容问题

uni-app 开发小程序项目时,WebSocket 是实现实时通信(如聊天、消息推送)的核心能力,但支付宝微信小程序的 Socket API 存在差异,且原生 API 缺少重连、心跳检测、消息缓存等实用能力,容易出现连接不稳定、消息丢失、跨端兼容问题。本文分享一套适配双端的 Socket 封装类,解决上述痛点,实现稳定、易用的实时通信能力。

一、核心痛点分析

在小程序 Socket 开发中,我们常遇到这些问题:

  1. 跨端 API 差异:支付宝小程序使用my.connectSocket,微信小程序通过uni.connectSocket返回的socketTask操作,事件监听方式不同;
  2. 连接稳定性差:网络波动导致连接断开后,无自动重连机制,用户体验差;
  3. 消息丢失:连接未建立时发送消息,直接失败无缓存;
  4. 心跳保活缺失:长时间无交互时,连接被服务端 / 网关断开,无检测和重连逻辑;
  5. 错误处理混乱:认证失败(401)、超时等场景仍无脑重连,浪费资源。

二、封装设计思路

针对上述问题,封装核心遵循「统一接口 + 差异化适配 + 健壮性增强」原则:

  1. 接口统一:对外暴露一致的connect/send/close/on方法,屏蔽双端 API 差异;
  2. 稳定性增强:实现自动重连、心跳检测、消息队列缓存;
  3. 精细化控制:支持重连次数 / 间隔配置、无需重连的错误码过滤、主动关闭标识;
  4. 易用性提升:内置 JSON 消息解析、连接超时处理、状态管理。

三、核心封装代码(双端适配版)

1. 核心类结构

封装后的SocketClient类包含配置管理、状态管理、双端适配的连接 / 消息 / 关闭逻辑,以及心跳、重连等核心能力,以下是关键逻辑说明(完整代码见文末):

(1)配置项设计(兼顾灵活性与默认值)
constructor(options) {
  if (!options?.url) throw new Error('Socket 连接地址 url 不能为空!');
  // 基础配置:覆盖双端通用+差异化默认值
  this.config = {
    url: options.url,
    reconnectTimes: options.reconnectTimes ?? 3, // 最大重连次数(-1无限重连)
    reconnectDelay: options.reconnectDelay ?? 3000, // 重连间隔
    autoReconnect: options.autoReconnect ?? true, // 自动重连
    header: options.header ?? { // 请求头(小程序仅支持content-type)
      'content-type': 'application/json',
      'source-code': uni.getStorageSync('source_header') || '',
    },
    connectTimeout: options.connectTimeout ?? 10000, // 连接超时
    heartbeatInterval: options.heartbeatInterval ?? 30000, // 心跳间隔30s
    heartbeatTimeout: options.heartbeatTimeout ?? 60000, // 心跳超时60s
    reconnectErrorCodes: options.reconnectErrorCodes ?? ['auth', '401', '403'] // 无需重连的错误码
  };

  // 状态管理:连接状态、重连次数、消息队列等
  this.readyState = 'CLOSED';
  this.reconnectCount = 0;
  this.messageQueue = [];
  this.isManualClose = false; // 主动关闭标识
  // 定时器:连接超时、心跳、心跳超时
  this.connectTimeoutTimer = null;
  this.heartbeatTimer = null;
  this.heartbeatTimeoutTimer = null;
}
(2)双端连接适配

核心差异点:支付宝小程序通过全局my对象监听事件,微信小程序通过socketTask实例监听。

// 支付宝小程序:全局事件监听
_offAllSocketListeners() {
  my.offSocketOpen(this._onSocketOpenHandler);
  my.offSocketMessage(this._onSocketMessageHandler);
  my.offSocketClose(this._onSocketCloseHandler);
  my.offSocketError(this._onSocketErrorHandler);
}

// 微信小程序:socketTask实例监听(在connect方法内)
this.socketTask = uni.connectSocket({ url: this.config.url, ... });
this.socketTask.onOpen((res) => { /* 连接成功逻辑 */ });
this.socketTask.onMessage((res) => { /* 接收消息逻辑 */ });
(3)核心能力实现

① 自动重连(精细化控制)

区分「主动关闭」「达到最大重连次数」「无需重连的错误码」等场景,避免无效重连:

_reconnect(err = null, closeRes = null) {
  // 1. 主动关闭 → 不重连
  if (this.isManualClose) return;
  // 2. 关闭自动重连 → 不重连
  if (!this.config.autoReconnect) return;
  // 3. 达到最大重连次数 → 不重连
  if (this.config.reconnectTimes !== -1 && this.reconnectCount >= this.config.reconnectTimes) return;
  // 4. 错误码命中无需重连列表 → 不重连
  if (err && this.config.reconnectErrorCodes.some(code => JSON.stringify(err).includes(code))) return;

  // 执行重连
  this.reconnectCount += 1;
  this.callbacks.onReconnect(this.reconnectCount);
  setTimeout(() => this.connect(), this.config.reconnectDelay);
}

② 心跳保活

定时发送心跳消息,检测连接是否存活,超时则触发重连:

// 启动心跳
_startHeartbeat() {
  this._stopHeartbeat();
  this.heartbeatTimer = setInterval(() => {
    if (this.readyState === 'OPEN') {
      this.send({ type: 'heartbeat' }); // 发送心跳消息
      this._resetHeartbeatTimeout(); // 重置心跳超时定时器
    }
  }, this.config.heartbeatInterval);
}

// 心跳超时处理
_resetHeartbeatTimeout() {
  clearTimeout(this.heartbeatTimeoutTimer);
  this.heartbeatTimeoutTimer = setTimeout(() => {
    this.close(); // 超时关闭连接
    this._reconnect(new Error('Heartbeat timeout')); // 触发重连
  }, this.config.heartbeatTimeout);
}

③ 消息队列缓存

连接未建立时,将消息存入队列,连接成功后自动发送:

send(data) {
  if (this.readyState !== 'OPEN') {
    this.messageQueue.push(data); // 缓存消息
    return;
  }
  // 格式化消息并发送
  const sendData = typeof data === 'object' ? JSON.stringify(data) : data;
  // 双端适配的发送逻辑...
}

// 连接成功后发送队列消息
_sendQueueMessages() {
  this.messageQueue.forEach(item => this.send(item));
  this.messageQueue = [];
}
2. 使用示例
// 1. 创建实例
import { createSocketInstance } from '@/utils/SocketClient';
const socket = createSocketInstance({
  url: 'wss://your-server.com/ws',
  reconnectTimes: -1, // 无限重连
  reconnectDelay: 5000,
  reconnectErrorCodes: ['401', '认证失败']
});

// 2. 注册回调
socket.on({
  onOpen: (res) => console.log('连接成功', res),
  onMessage: (data) => console.log('收到消息', data),
  onError: (err) => console.error('连接错误', err),
  onReconnect: (count) => console.log(`${count}次重连中`)
});

// 3. 连接Socket
socket.connect();

// 4. 发送消息
socket.send({ type: 'chat', content: 'Hello World' });

// 5. 主动关闭
// socket.close();

四、双端适配关键差异

特性 支付宝小程序 微信小程序
连接创建 my.connectSocket uni.connectSocket 返回 socketTask
事件监听 全局my.onSocketXxx socketTask 实例onXxx
关闭连接 my.closeSocket socketTask.close()
发送消息 my.sendSocketMessage socketTask.send()

五、避坑指南

  1. 跨域 / 协议问题:小程序仅支持wss协议,且域名需加入小程序白名单;
  2. 请求头限制:小程序仅支持content-type等少数请求头,自定义头需后端配合;
  3. 重连逻辑:认证失败(401)需先刷新令牌再重连,避免无效重试;
  4. 资源清理:页面卸载时需主动调用close()关闭连接,清除定时器,避免内存泄漏。

六、完整代码获取

微信(完整代码)

/**
 * uni-app 小程序 Socket 封装类(优化版)
 * 修复:重连逻辑/参数兼容/错误处理/单例配置/编码格式问题
 */
class SocketClient {
  /**
   * 构造函数
   * @param {Object} options 配置项
   * @param {String} options.url Socket 连接地址(必填,如 wss://xxx.com/ws)
   * @param {Number} [options.reconnectTimes=3] 最大重连次数(-1 表示无限重连)
   * @param {Number} [options.reconnectDelay=3000] 重连间隔(ms)
   * @param {Boolean} [options.autoReconnect=true] 是否自动重连
   * @param {Object} [options.header] 请求头(小程序端仅支持 content-type)
   * @param {String} [options.protocol] 子协议
   * @param {Number} [options.connectTimeout=10000] 连接超时时间(ms)
   * @param {Array} [options.reconnectErrorCodes=[]] 无需重连的错误码/错误关键词
   */
  constructor(options) {
    if (!options?.url) {
      throw new Error('Socket 连接地址 url 不能为空!');
    }

    // 基础配置(移除无效的 timeout 参数,新增连接超时配置)
    this.config = {
      url: options.url,
      reconnectTimes: options.reconnectTimes ?? 3,
      reconnectDelay: options.reconnectDelay ?? 3000,
      autoReconnect: options.autoReconnect ?? true,
      header: options.header ?? {
        'content-type': 'application/json', 'source-code': uni.getStorageSync('source_header') || '',
        'source-channel-code': uni.getStorageSync('channel_header') || '',
      },
      protocol: options.protocol,
      connectTimeout: options.connectTimeout ?? 10000,
      reconnectErrorCodes: options.reconnectErrorCodes ?? ['auth', '401', '403', '拒绝'],
      heartbeatInterval: options.heartbeatInterval ?? 30000, // 心跳间隔,默认 30 秒
      heartbeatTimeout: options.heartbeatTimeout ?? 60000 // 心跳超时,默认 60 秒
    };

    // 状态管理
    this.socketTask = null; // Socket 任务实例
    this.readyState = 'CLOSED'; // 连接状态:CONNECTING/OPEN/CLOSED
    this.reconnectCount = 0; // 当前重连次数
    this.messageQueue = []; // 消息队列(连接未建立时缓存)
    this.connectTimeoutTimer = null; // 连接超时定时器
    this.isManualClose = false; // 新增:主动关闭标志位
    this.heartbeatTimer = null; // 心跳定时器
    this.heartbeatTimeoutTimer = null; // 心跳超时定时器

    // 对外暴露的回调函数(初始化)
    this.callbacks = {
      onOpen: (res) => { }, // 连接成功回调
      onMessage: (data) => { }, // 收到消息回调
      onClose: (res) => { }, // 连接关闭回调
      onError: (err) => { }, // 连接错误回调
      onReconnect: (count) => { } // 重连中回调
    };
  }

  /**
   * 初始化 Socket 连接
   */
  connect() {
    // 避免重复连接
    if (this.readyState === 'CONNECTING' || this.readyState === 'OPEN') {
      console.warn('Socket 已处于连接中/已连接状态,无需重复连接');
      return;
    }

    // 重置主动关闭标志(重连时需要)
    this.isManualClose = false;
    // 更新状态为连接中
    this.readyState = 'CONNECTING';
    // 重置重连次数
    this.reconnectCount = 0;

    // 创建 Socket 连接(移除无效的 timeout 参数)
    this.socketTask = uni.connectSocket({
      url: this.config.url,
      header: this.config.header,
      protocol: this.config.protocol,
      success: (res) => {
        console.log('Socket 连接请求发送成功', res);
      },
      fail: (err) => {
        this.readyState = 'CLOSED';
        this.callbacks.onError(err);
        console.error('Socket 连接请求发送失败', err);
        // 触发重连(会先检查主动关闭/错误类型)
        this._reconnect(err);
      }
    });

    // 监听连接成功
    this.socketTask.onOpen((res) => {
      clearTimeout(this.connectTimeoutTimer); // 清除超时定时器
      this.readyState = 'OPEN';
      this.callbacks.onOpen(res);
      console.log('Socket 连接成功', res);

      // 发送队列中的消息
      this._sendQueueMessages();

      // 启动心跳检测
      this._startHeartbeat();
    });

    // 监听收到消息
    this.socketTask.onMessage((res) => {
      let data = res.data;
      // 尝试解析 JSON 格式消息
      try {
        data = JSON.parse(data);
      } catch (e) {
        // 非 JSON 格式则原样返回
      }

      // 重置心跳超时定时器
      this._resetHeartbeatTimeout();

      this.callbacks.onMessage(data);
    });

    // 监听连接关闭
    this.socketTask.onClose((res) => {
      this.readyState = 'CLOSED';
      this.callbacks.onClose(res);
      console.log('Socket 连接关闭', res);
      // 触发重连(会先检查主动关闭)
      this._reconnect(null, res);
    });

    // 监听连接错误
    this.socketTask.onError((err) => {
      this.readyState = 'CLOSED';
      this.callbacks.onError(err);
      console.error('Socket 连接错误', err);
      // 触发重连(会先检查错误类型/主动关闭)
      this._reconnect(err);
    });

    // 手动处理连接超时(移除 uni.connectSocket 的 timeout 参数,保留手动定时器)
    this.connectTimeoutTimer = setTimeout(() => {
      if (this.readyState === 'CONNECTING') {
        this.close(); // 超时后主动关闭
        const timeoutErr = new Error(`Socket 连接超时(${this.config.connectTimeout}ms)`);
        this.callbacks.onError(timeoutErr);
        console.error(timeoutErr.message);
      }
    }, this.config.connectTimeout);
  }

  /**
   * 发送消息(移除无效的 encoding 参数)
   * @param {Any} data 要发送的消息(支持对象/字符串/二进制)
   */
  send(data) {
    // 未连接时缓存消息
    if (this.readyState !== 'OPEN') {
      console.warn('Socket 未连接,消息已加入队列', data);
      this.messageQueue.push(data);
      return;
    }

    // 格式化消息(对象转为 JSON 字符串)
    let sendData = data;
    if (typeof sendData === 'object' && sendData !== null) {
      sendData = JSON.stringify(sendData);
    }

    // 发送消息(移除 encoding 参数,兼容小程序 API)
    this.socketTask.send({
      data: sendData,
      success: () => {
        console.log('Socket 消息发送成功', sendData);
      },
      fail: (err) => {
        console.error('Socket 消息发送失败', err);
        this.callbacks.onError(err);
      }
    });
  }

  /**
   * 关闭 Socket 连接(新增主动关闭标志)
   * @param {Number} [code=1000] 关闭状态码(遵循 WebSocket 标准)
   * @param {String} [reason='normal close'] 关闭原因
   */
  close(code = 1000, reason = 'normal close') {
    // 设置主动关闭标志
    this.isManualClose = true;

    if (this.socketTask) {
      this.socketTask.close({
        code,
        reason,
        success: (res) => {
          this.readyState = 'CLOSED';
          this.callbacks.onClose(res);
          console.log('Socket 主动关闭成功', res);
        },
        fail: (err) => {
          console.error('Socket 主动关闭失败', err);
        }
      });
    }

    // 清理资源
    this.socketTask = null;
    clearTimeout(this.connectTimeoutTimer);
    this._stopHeartbeat(); // 停止心跳检测
    this.messageQueue = [];
  }

  /**
   * 发送队列中的缓存消息(连接成功后调用)
   */
  _sendQueueMessages() {
    if (this.messageQueue.length === 0) return;

    console.log(`开始发送队列中的 ${this.messageQueue.length} 条消息`);
    this.messageQueue.forEach((item) => {
      this.send(item);
    });
    this.messageQueue = []; // 清空队列
  }

  /**
   * 启动心跳检测
   */
  _startHeartbeat() {
    // 清除可能存在的定时器
    this._stopHeartbeat();

    // 启动心跳定时器
    this.heartbeatTimer = setInterval(() => {
      if (this.readyState === 'OPEN') {
        // 发送心跳消息
        this.send({ type: 'heartbeat' });
        console.log('发送心跳消息');

        // 启动心跳超时定时器
        this._resetHeartbeatTimeout();
      }
    }, this.config.heartbeatInterval);
  }

  /**
   * 停止心跳检测
   */
  _stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }

  /**
   * 重置心跳超时定时器
   */
  _resetHeartbeatTimeout() {
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }

    // 启动新的心跳超时定时器
    this.heartbeatTimeoutTimer = setTimeout(() => {
      console.error('心跳超时,连接可能已断开');
      // 关闭连接并触发重连
      this.close();
      this._reconnect(new Error('Heartbeat timeout'));
    }, this.config.heartbeatTimeout);
  }

  /**
   * 重连逻辑(优化:区分主动关闭/错误类型)
   * @param {Object} [err] 错误信息(用于判断是否需要重连)
   * @param {Object} [closeRes] 关闭信息(用于判断关闭原因)
   */
  _reconnect(err = null, closeRes = null) {
    // 1. 主动关闭 → 不重连
    if (this.isManualClose) {
      console.log('Socket 主动关闭,跳过重连');
      return;
    }

    // 2. 关闭自动重连 → 不重连
    if (!this.config.autoReconnect) {
      console.log('Socket 自动重连已关闭,跳过重连');
      return;
    }

    // 3. 已达最大重连次数 → 不重连
    if (this.config.reconnectTimes !== -1 && this.reconnectCount >= this.config.reconnectTimes) {
      console.log(`Socket 已达最大重连次数(${this.config.reconnectTimes}),停止重连`);
      return;
    }

    // 4. 错误类型命中「无需重连」列表 → 不重连
    if (err) {
      const errStr = JSON.stringify(err);
      const needReconnect = !this.config.reconnectErrorCodes.some(code =>
        errStr.includes(code)
      );
      if (!needReconnect) {
        console.log(`Socket 错误类型命中无需重连列表,停止重连:${errStr}`);
        return;
      }
    }

    // 满足重连条件,执行重连
    this.reconnectCount += 1;
    console.log(`Socket 开始第 ${this.reconnectCount} 次重连(间隔 ${this.config.reconnectDelay}ms)`);
    this.callbacks.onReconnect(this.reconnectCount);

    // 延迟重连
    setTimeout(() => {
      this.connect();
    }, this.config.reconnectDelay);
  }

  /**
   * 注册事件回调
   * @param {Object} callbacks 回调函数集合
   * @param {Function} callbacks.onOpen 连接成功
   * @param {Function} callbacks.onMessage 收到消息
   * @param {Function} callbacks.onClose 连接关闭
   * @param {Function} callbacks.onError 连接错误
   * @param {Function} callbacks.onReconnect 重连中
   */
  on(callbacks) {
    Object.assign(this.callbacks, callbacks);
  }

  // 对外暴露当前连接状态
  getState() {
    return this.readyState;
  }
}

/**
 * 全局 Socket 实例创建函数(替代硬编码单例,让用户灵活配置)
 * @param {Object} options 配置项(同构造函数)
 * @returns {SocketClient} Socket 实例
 */
export const createSocketInstance = (options) => {
  return new SocketClient(options);
};

// 示例:默认实例(可通过环境变量/配置文件注入 url)
// 推荐:将 url 配置在项目的环境变量中,如 process.env.VUE_APP_SOCKET_URL
export const defaultSocketInstance = createSocketInstance({
  url: process.env.VUE_APP_SOCKET_URL || 'wss://default-socket-url.com/ws',
  reconnectTimes: -1,
  reconnectDelay: 3000,
  autoReconnect: true,
  reconnectErrorCodes: ['auth', '401', '403', '认证失败', '拒绝连接']
});

// 导出类,方便创建多个实例
export default SocketClient;

支付宝(完整代码)

/**
 * uni-app 小程序 Socket 封装类(优化版)
 * 修复:重连逻辑/参数兼容/错误处理/单例配置/编码格式问题
 */
class SocketClient {
  /**
   * 构造函数
   * @param {Object} options 配置项
   * @param {String} options.url Socket 连接地址(必填,如 wss://xxx.com/ws)
   * @param {Number} [options.reconnectTimes=3] 最大重连次数(-1 表示无限重连)
   * @param {Number} [options.reconnectDelay=3000] 重连间隔(ms)
   * @param {Boolean} [options.autoReconnect=true] 是否自动重连
   * @param {Object} [options.header] 请求头(小程序端仅支持 content-type)
   * @param {String} [options.protocol] 子协议
   * @param {Number} [options.connectTimeout=10000] 连接超时时间(ms)
   * @param {Array} [options.reconnectErrorCodes=[]] 无需重连的错误码/错误关键词
   */
  constructor(options) {
    if (!options?.url) {
      throw new Error('Socket 连接地址 url 不能为空!');
    }

    // 基础配置(移除无效的 timeout 参数,新增连接超时配置)
    this.config = {
      url: options.url,
      reconnectTimes: options.reconnectTimes ?? 3,
      reconnectDelay: options.reconnectDelay ?? 3000,
      autoReconnect: options.autoReconnect ?? true,
      header: options.header ?? {
        'content-type': 'application/json', 'source-code': uni.getStorageSync('source_header') || '',
        'source-channel-code': uni.getStorageSync('channel_header') || '',
      },
      protocol: options.protocol,
      connectTimeout: options.connectTimeout ?? 10000,
      reconnectErrorCodes: options.reconnectErrorCodes ?? ['auth', '401', '403', '拒绝'],
      heartbeatInterval: options.heartbeatInterval ?? 30000, // 心跳间隔,默认 30 秒
      heartbeatTimeout: options.heartbeatTimeout ?? 60000 // 心跳超时,默认 60 秒
    };

    // 状态管理
    this.socketTask = null; // Socket 任务实例
    this.readyState = 'CLOSED'; // 连接状态:CONNECTING/OPEN/CLOSED
    this.reconnectCount = 0; // 当前重连次数
    this.messageQueue = []; // 消息队列(连接未建立时缓存)
    this.connectTimeoutTimer = null; // 连接超时定时器
    this.isManualClose = false; // 新增:主动关闭标志位
    this.heartbeatTimer = null; // 心跳定时器
    this.heartbeatTimeoutTimer = null; // 心跳超时定时器

    // 对外暴露的回调函数(初始化)
    this.callbacks = {
      onOpen: (res) => { }, // 连接成功回调
      onMessage: (data) => { }, // 收到消息回调
      onClose: (res) => { }, // 连接关闭回调
      onError: (err) => { }, // 连接错误回调
      onReconnect: (count) => { } // 重连中回调
    };

    // 绑定事件处理函数的 this
    this._onSocketOpenHandler = this._onSocketOpen.bind(this);
    this._onSocketMessageHandler = this._onSocketMessage.bind(this);
    this._onSocketCloseHandler = this._onSocketClose.bind(this);
    this._onSocketErrorHandler = this._onSocketError.bind(this);
  }

  /**
   * Socket 打开事件处理
   */
  _onSocketOpen(res) {
    clearTimeout(this.connectTimeoutTimer); // 清除超时定时器
    this.readyState = 'OPEN';
    this.callbacks.onOpen(res);
    console.log('Socket 连接成功', res);

    // 发送队列中的消息
    this._sendQueueMessages();

    // 启动心跳检测
    this._startHeartbeat();
  }

  /**
   * Socket 消息事件处理
   */
  _onSocketMessage(res) {
    let data = res.data;
    // 尝试解析 JSON 格式消息
    try {
      data = JSON.parse(data);
    } catch (e) {
      // 非 JSON 格式则原样返回
    }

    // 重置心跳超时定时器
    this._resetHeartbeatTimeout();

    this.callbacks.onMessage(data);
  }

  /**
   * Socket 关闭事件处理
   */
  _onSocketClose(res) {
    this.readyState = 'CLOSED';
    this.callbacks.onClose(res);
    console.log('Socket 连接关闭', res);
    // 触发重连(会先检查主动关闭)
    this._reconnect(null, res);
  }

  /**
   * Socket 错误事件处理
   */
  _onSocketError(err) {
    this.readyState = 'CLOSED';
    this.callbacks.onError(err);
    console.error('Socket 连接错误', err);
    // 触发重连(会先检查错误类型/主动关闭)
    this._reconnect(err);
  }

  /**
   * 移除所有 Socket 全局事件监听
   */
  _offAllSocketListeners() {
    my.offSocketOpen(this._onSocketOpenHandler);
    my.offSocketMessage(this._onSocketMessageHandler);
    my.offSocketClose(this._onSocketCloseHandler);
    my.offSocketError(this._onSocketErrorHandler);
  }

  /**
   * 初始化 Socket 连接
   */
  connect() {
    // 避免重复连接
    if (this.readyState === 'CONNECTING' || this.readyState === 'OPEN') {
      console.warn('Socket 已处于连接中/已连接状态,无需重复连接');
      return;
    }

    // 重置主动关闭标志(重连时需要)
    this.isManualClose = false;
    // 更新状态为连接中
    this.readyState = 'CONNECTING';
    // 重置重连次数
    this.reconnectCount = 0;

    // 先移除之前的全局事件监听,避免重复注册
    this._offAllSocketListeners();

    // 创建 Socket 连接(移除无效的 timeout 参数)
    my.connectSocket({
      url: this.config.url,
      header: this.config.header,
      protocol: this.config.protocol,
      success: (res) => {
        console.log('Socket 连接请求发送成功', res);
      },
      fail: (err) => {
        this.readyState = 'CLOSED';
        this.callbacks.onError(err);
        console.error('Socket 连接请求发送失败', err);
        // 触发重连(会先检查主动关闭/错误类型)
        this._reconnect(err);
      }
    });

    // 监听连接成功
    my.onSocketOpen(this._onSocketOpenHandler);

    // 监听收到消息
    my.onSocketMessage(this._onSocketMessageHandler);

    // 监听连接关闭
    my.onSocketClose(this._onSocketCloseHandler);

    // 监听连接错误
    my.onSocketError(this._onSocketErrorHandler);

    // 手动处理连接超时(移除 uni.connectSocket 的 timeout 参数,保留手动定时器)
    this.connectTimeoutTimer = setTimeout(() => {
      if (this.readyState === 'CONNECTING') {
        this.close(); // 超时后主动关闭
        const timeoutErr = new Error(`Socket 连接超时(${this.config.connectTimeout}ms)`);
        this.callbacks.onError(timeoutErr);
        console.error(timeoutErr.message);
      }
    }, this.config.connectTimeout);
  }

  /**
   * 发送消息(移除无效的 encoding 参数)
   * @param {Any} data 要发送的消息(支持对象/字符串/二进制)
   */
  send(data) {
    // 未连接时缓存消息
    if (this.readyState !== 'OPEN') {
      console.warn('Socket 未连接,消息已加入队列', data);
      this.messageQueue.push(data);
      return;
    }

    // 格式化消息(对象转为 JSON 字符串)
    let sendData = data;
    if (typeof sendData === 'object' && sendData !== null) {
      sendData = JSON.stringify(sendData);
    }

    // 发送消息(移除 encoding 参数,兼容小程序 API)
    my.sendSocketMessage({
      data: sendData,
      success: () => {
        console.log('Socket 消息发送成功', sendData);
      },
      fail: (err) => {
        console.error('Socket 消息发送失败', err);
        this.callbacks.onError(err);
      }
    });
  }

  /**
   * 关闭 Socket 连接(新增主动关闭标志)
   * @param {Number} [code=1000] 关闭状态码(遵循 WebSocket 标准)
   * @param {String} [reason='normal close'] 关闭原因
   */
  close(code = 1000, reason = 'normal close') {
    // 设置主动关闭标志
    this.isManualClose = true;
    my.closeSocket();
    // 移除所有事件监听
    this._offAllSocketListeners();
    clearTimeout(this.connectTimeoutTimer);
    this._stopHeartbeat(); // 停止心跳检测
    this.messageQueue = [];
  }

  /**
   * 发送队列中的缓存消息(连接成功后调用)
   */
  _sendQueueMessages() {
    if (this.messageQueue.length === 0) return;

    console.log(`开始发送队列中的 ${this.messageQueue.length} 条消息`);
    this.messageQueue.forEach((item) => {
      this.send(item);
    });
    this.messageQueue = []; // 清空队列
  }

  /**
   * 启动心跳检测
   */
  _startHeartbeat() {
    // 清除可能存在的定时器
    this._stopHeartbeat();

    // 启动心跳定时器
    this.heartbeatTimer = setInterval(() => {
      if (this.readyState === 'OPEN') {
        // 发送心跳消息
        this.send({ type: 'heartbeat' });
        console.log('发送心跳消息');

        // 启动心跳超时定时器
        this._resetHeartbeatTimeout();
      }
    }, this.config.heartbeatInterval);
  }

  /**
   * 停止心跳检测
   */
  _stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }
  }

  /**
   * 重置心跳超时定时器
   */
  _resetHeartbeatTimeout() {
    if (this.heartbeatTimeoutTimer) {
      clearTimeout(this.heartbeatTimeoutTimer);
      this.heartbeatTimeoutTimer = null;
    }

    // 启动新的心跳超时定时器
    this.heartbeatTimeoutTimer = setTimeout(() => {
      console.error('心跳超时,连接可能已断开');
      // 关闭连接并触发重连
      this.close();
      this._reconnect(new Error('Heartbeat timeout'));
    }, this.config.heartbeatTimeout);
  }

  /**
   * 重连逻辑(优化:区分主动关闭/错误类型)
   * @param {Object} [err] 错误信息(用于判断是否需要重连)
   * @param {Object} [closeRes] 关闭信息(用于判断关闭原因)
   */
  _reconnect(err = null, closeRes = null) {
    // 1. 主动关闭 → 不重连
    if (this.isManualClose) {
      console.log('Socket 主动关闭,跳过重连');
      return;
    }

    // 2. 关闭自动重连 → 不重连
    if (!this.config.autoReconnect) {
      console.log('Socket 自动重连已关闭,跳过重连');
      return;
    }

    // 3. 已达最大重连次数 → 不重连
    if (this.config.reconnectTimes !== -1 && this.reconnectCount >= this.config.reconnectTimes) {
      console.log(`Socket 已达最大重连次数(${this.config.reconnectTimes}),停止重连`);
      return;
    }

    // 4. 错误类型命中「无需重连」列表 → 不重连
    if (err) {
      const errStr = JSON.stringify(err);
      const needReconnect = !this.config.reconnectErrorCodes.some(code =>
        errStr.includes(code)
      );
      if (!needReconnect) {
        console.log(`Socket 错误类型命中无需重连列表,停止重连:${errStr}`);
        return;
      }
    }

    // 满足重连条件,执行重连
    this.reconnectCount += 1;
    console.log(`Socket 开始第 ${this.reconnectCount} 次重连(间隔 ${this.config.reconnectDelay}ms)`);
    this.callbacks.onReconnect(this.reconnectCount);

    // 延迟重连
    setTimeout(() => {
      this.connect();
    }, this.config.reconnectDelay);
  }

  /**
   * 注册事件回调
   * @param {Object} callbacks 回调函数集合
   * @param {Function} callbacks.onOpen 连接成功
   * @param {Function} callbacks.onMessage 收到消息
   * @param {Function} callbacks.onClose 连接关闭
   * @param {Function} callbacks.onError 连接错误
   * @param {Function} callbacks.onReconnect 重连中
   */
  on(callbacks) {
    Object.assign(this.callbacks, callbacks);
  }

  // 对外暴露当前连接状态
  getState() {
    return this.readyState;
  }
}

/**
 * 全局 Socket 实例创建函数(替代硬编码单例,让用户灵活配置)
 * @param {Object} options 配置项(同构造函数)
 * @returns {SocketClient} Socket 实例
 */
export const createSocketInstance = (options) => {
  return new SocketClient(options);
};

// 示例:默认实例(可通过环境变量/配置文件注入 url)
// 推荐:将 url 配置在项目的环境变量中,如 process.env.VUE_APP_SOCKET_URL
export const defaultSocketInstance = createSocketInstance({
  url: process.env.VUE_APP_SOCKET_URL || 'wss://default-socket-url.com/ws',
  reconnectTimes: -1,
  reconnectDelay: 3000,
  autoReconnect: true,
  reconnectErrorCodes: ['auth', '401', '403', '认证失败', '拒绝连接']
});

// 导出类,方便创建多个实例
export default SocketClient;

总结

这套 Socket 封装类解决了小程序端 WebSocket 开发的核心痛点,通过统一接口屏蔽双端差异,内置重连、心跳、消息缓存等能力,大幅提升实时通信的稳定性和易用性。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐