uniapp 适配支付宝 / 微信小程序的 WebSocket 封装:解决重连、心跳、跨端兼容问题
在 `uni-app` 开发小程序项目时,`WebSocket` 是实现实时通信(如聊天、消息推送)的核心能力,但`支付宝`和`微信`小程序的 Socket API 存在差异,且原生 API 缺少重连、心跳检测、消息缓存等实用能力,容易出现连接不稳定、消息丢失、跨端兼容问题。本文分享一套适配双端的 `Socket` 封装类,解决上述痛点,实现稳定、易用的实时通信能力。
uniapp 适配支付宝 / 微信小程序的 WebSocket 封装:解决重连、心跳、跨端兼容问题
在 uni-app 开发小程序项目时,WebSocket 是实现实时通信(如聊天、消息推送)的核心能力,但支付宝和微信小程序的 Socket API 存在差异,且原生 API 缺少重连、心跳检测、消息缓存等实用能力,容易出现连接不稳定、消息丢失、跨端兼容问题。本文分享一套适配双端的 Socket 封装类,解决上述痛点,实现稳定、易用的实时通信能力。
一、核心痛点分析
在小程序 Socket 开发中,我们常遇到这些问题:
- 跨端 API 差异:支付宝小程序使用my.connectSocket,微信小程序通过uni.connectSocket返回的socketTask操作,事件监听方式不同;
- 连接稳定性差:网络波动导致连接断开后,无自动重连机制,用户体验差;
- 消息丢失:连接未建立时发送消息,直接失败无缓存;
- 心跳保活缺失:长时间无交互时,连接被服务端 / 网关断开,无检测和重连逻辑;
- 错误处理混乱:认证失败(401)、超时等场景仍无脑重连,浪费资源。
二、封装设计思路
针对上述问题,封装核心遵循「统一接口 + 差异化适配 + 健壮性增强」原则:
- 接口统一:对外暴露一致的connect/send/close/on方法,屏蔽双端 API 差异;
- 稳定性增强:实现自动重连、心跳检测、消息队列缓存;
- 精细化控制:支持重连次数 / 间隔配置、无需重连的错误码过滤、主动关闭标识;
- 易用性提升:内置 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() |
五、避坑指南
- 跨域 / 协议问题:小程序仅支持wss协议,且域名需加入小程序白名单;
- 请求头限制:小程序仅支持content-type等少数请求头,自定义头需后端配合;
- 重连逻辑:认证失败(401)需先刷新令牌再重连,避免无效重试;
- 资源清理:页面卸载时需主动调用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 开发的核心痛点,通过统一接口屏蔽双端差异,内置重连、心跳、消息缓存等能力,大幅提升实时通信的稳定性和易用性。
更多推荐
所有评论(0)