uni-app WebSocket实战:封装高可用的实时消息推送模块

2026-06-22 0 852

年后接到一个在线客服的需求,要在App、H5和微信小程序三端同时上线,用户和客服之间能实时收发消息。技术栈定的是uni-app,后端用网关统一管理WebSocket连接。原本以为直接拿uni.connectSocket就能搞定,结果在心跳维持、断线重连、多端消息格式兼容上连踩了几个坑。最后花了两天时间封装了一个通用的WebSocket管理器,跑了一个月没出过连接泄漏或消息丢失的情况。这篇就把整个实现过程和踩过的点完整写出来。

一、为什么不用现成的插件

社区里能找到不少uni-app的WebSocket封装,但大部分要么只支持单端,要么没处理心跳和重连,要么把状态管理写死在组件里没法复用。我们这个需求有几个特殊点:

  • 需要同时在App原生渲染、H5浏览器和小程序环境运行
  • 连接断掉后要自动重连,并且有递增的等待间隔
  • 消息要全局分发,多个页面都能监听,不是只在一个聊天页面里用
  • 后端返回的消息格式不统一,需要前端做一层规范化

这些条件综合下来,自己封装一个轻量的管理器反而更可控。下面就是整个模块的完整代码和设计思路。

二、核心封装:WebSocketManager类

思路是把所有与连接有关的逻辑(创建、心跳、重连、消息收发)放进一个类,对外只暴露connectsendclose和事件监听接口。这样无论哪个页面需要用到实时消息,引入同一个实例即可。

2.1 基础结构和连接方法

// utils/websocket-manager.js

class WebSocketManager {
    constructor() {
        this.socketTask = null;        // uni-app的SocketTask对象
        this.url = '';
        this.heartbeatTimer = null;    // 心跳定时器
        this.reconnectTimer = null;    // 重连定时器
        this.reconnectCount = 0;       
        this.maxReconnectCount = 8;    // 最多重连8次
        this.reconnectInterval = 1000; // 初始重连间隔1秒
        this.isManualClose = false;    // 是否为手动关闭(手动关闭不自动重连)
        this.listeners = new Map();    // 事件监听器 Map
    }

    // 初始化连接
    connect(url, options = {}) {
        this.url = url;
        this.isManualClose = false;
        this.reconnectCount = 0;
        this.createSocketTask(url, options);
    }

    createSocketTask(url, options) {
        this.socketTask = uni.connectSocket({
            url,
            ...options,
            success: () => {
                console.log('WebSocket 连接创建成功');
            },
            fail: (err) => {
                console.error('WebSocket 连接创建失败', err);
                this.tryReconnect();
            }
        });

        // 监听连接打开
        this.socketTask.onOpen(() => {
            console.log('WebSocket 已连接');
            this.reconnectCount = 0;
            this.reconnectInterval = 1000;
            this.startHeartbeat();
            this.emit('open');
        });

        // 监听收到消息
        this.socketTask.onMessage((res) => {
            this.handleMessage(res.data);
        });

        // 监听连接关闭
        this.socketTask.onClose((res) => {
            console.log('WebSocket 已关闭', res);
            this.stopHeartbeat();
            if (!this.isManualClose) {
                this.tryReconnect();
            }
            this.emit('close', res);
        });

        // 监听错误
        this.socketTask.onError((err) => {
            console.error('WebSocket 错误', err);
            this.emit('error', err);
        });
    }
}

这里有几个关键点。第一,uni.connectSocket返回的socketTask对象提供了onOpenonMessage等回调,这些回调在App端和H5端行为基本一致,但在小程序里要注意onError并不总是能触发,所以重连逻辑主要依赖onClose来驱动。

第二,isManualClose用来区分是用户主动关闭还是网络原因断开。我们只希望在意外断开时自动重连,用户如果手动点了“断开连接”,就不应该再自动重试。

三、心跳机制的实现

WebSocket如果长时间没有数据交互,中间的网络设备可能会断开连接。通用的做法是客户端每隔一段时间发送一个“ping”帧,服务端回复“pong”,以此保持连接活跃。

WebSocketManager里加上心跳逻辑:

startHeartbeat() {
    this.stopHeartbeat(); // 先清除已有的,避免重复
    this.heartbeatTimer = setInterval(() => {
        if (this.socketTask) {
            this.socketTask.send({
                data: JSON.stringify({ type: 'ping' }),
                fail: () => {
                    // 发送失败说明连接可能已经异常,关闭当前连接触发重连
                    console.warn('心跳发送失败,准备重连');
                    this.closeSocket(false); // 不标记为手动关闭
                }
            });
        }
    }, 30000); // 30秒发一次
}

stopHeartbeat() {
    if (this.heartbeatTimer) {
        clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = null;
    }
}

这里心跳间隔设了30秒,大多数服务端的空闲超时是60秒左右,30秒发一次足够保险。发送失败时直接调用closeSocket(false)来触发重连,避免了长时间无响应的僵死连接。

另外需要注意,App端在应用退到后台时,WebSocket连接可能会被系统暂停。如果需要后台持续在线,必须使用原生插件处理后台任务,这超出了本文范围。对于大多数客服场景来说,退到后台断开连接、前台回来再重连是完全可以接受的。

四、递增间隔的断线重连

重连不能无脑每秒重试一次,那样会拖垮客户端的网络和服务器资源。我们用指数退避的方式,间隔从1秒开始逐次倍增,最大到30秒。

tryReconnect() {
    if (this.reconnectCount >= this.maxReconnectCount) {
        console.warn('已达最大重连次数,停止重连');
        this.emit('reconnect-failed');
        return;
    }
    if (this.reconnectTimer) {
        clearTimeout(this.reconnectTimer);
    }
    this.reconnectTimer = setTimeout(() => {
        this.reconnectCount++;
        console.log(`尝试第${this.reconnectCount}次重连,间隔${this.reconnectInterval}ms`);
        this.createSocketTask(this.url);
        // 递增重连间隔,最大30秒
        this.reconnectInterval = Math.min(this.reconnectInterval * 2, 30000);
    }, this.reconnectInterval);
}

每次连接成功时(onOpen),把reconnectCountreconnectInterval重置为初始值。这样断线后第一次重连快(1秒),后续逐步放缓,8次之后彻底放弃。在实际使用中,8次重连覆盖的时间窗口已经超过2分钟,足以应对大部分网络抖动。

五、消息分发与全局监听

为了在不同页面都能收到实时消息,我们模仿事件总线的方式,给WebSocketManager加上onoffemit方法:

// 注册事件监听
on(eventName, callback) {
    if (!this.listeners.has(eventName)) {
        this.listeners.set(eventName, []);
    }
    this.listeners.get(eventName).push(callback);
}

// 移除事件监听
off(eventName, callback) {
    const callbacks = this.listeners.get(eventName);
    if (callbacks) {
        const index = callbacks.indexOf(callback);
        if (index > -1) {
            callbacks.splice(index, 1);
        }
    }
}

// 触发事件
emit(eventName, data) {
    const callbacks = this.listeners.get(eventName);
    if (callbacks) {
        callbacks.forEach(cb => cb(data));
    }
}

// 处理收到的原始消息,格式化为统一结构后分发
handleMessage(rawData) {
    let parsed;
    try {
        parsed = JSON.parse(rawData);
    } catch {
        parsed = { type: 'raw', content: rawData };
    }
    // 根据消息类型分发到具体事件
    this.emit('message', parsed);
    if (parsed.type) {
        this.emit(parsed.type, parsed);
    }
}

// 发送消息
send(data) {
    if (this.socketTask) {
        this.socketTask.send({
            data: JSON.stringify(data),
            fail: (err) => {
                console.error('发送消息失败', err);
                this.emit('send-error', err);
            }
        });
    }
}

// 主动关闭连接
close() {
    this.isManualClose = true;
    this.stopHeartbeat();
    if (this.reconnectTimer) {
        clearTimeout(this.reconnectTimer);
        this.reconnectTimer = null;
    }
    this.closeSocket(true);
}

closeSocket(keepManualFlag) {
    if (this.socketTask) {
        this.socketTask.close({
            success: () => {
                this.socketTask = null;
            }
        });
    }
}

这样在聊天页面里,只需要websocketManager.on('message', handler)就能收到所有消息,在客服列表页可以监听new-customer这类自定义事件,架构很清爽。

六、单例模式:整个App共享一个连接

WebSocket连接是稀缺资源,整个应用应该只保持一条连接。我们把这个管理器做成单例:

// 最终导出单例
const websocketManager = new WebSocketManager();
export default websocketManager;

然后在App.vueonLaunch中初始化连接:

import websocketManager from '@/utils/websocket-manager.js';

export default {
    onLaunch() {
        const token = uni.getStorageSync('token');
        websocketManager.connect(`wss://api.example.com/ws?token=${token}`);
    }
};

这里把登录token作为查询参数传给服务端做身份校验,是最简单可靠的做法。如果token会过期,可以在收到服务端的“token过期”通知后,先通过HTTP刷新token,再调用websocketManager.close()然后重新connect。这套流程可以封装在WebSocketManager内部,但不在这篇文章展开。

七、完整聊天页面示例

下面是一个使用上面封装的管理器实现的聊天页面关键部分,展示了如何在Vue3组合式API里集成:

<template>
    <view class="chat-page">
        <scroll-view scroll-y class="message-list" ref="msgList">
            <view v-for="(msg, idx) in messages" :key="idx" 
                  :class="msg.from === 'me' ? 'msg-self' : 'msg-other'">
                <text>{{ msg.content }}</text>
            </view>
        </scroll-view>

        <view class="input-area">
            <input v-model="inputText" placeholder="输入消息..." />
            <button @click="sendMsg">发送</button>
        </view>
    </view>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import websocketManager from '@/utils/websocket-manager.js';

const messages = ref([]);
const inputText = ref('');

const handleMessage = (msg) => {
    // 只关注聊天消息(假设type为chat)
    if (msg.type === 'chat') {
        messages.value.push({
            from: msg.from,
            content: msg.content,
            time: Date.now()
        });
        // 滚动到底部
        nextTick(() => {
            // 实际项目中需要获取scroll-view的scrollTop并设置
        });
    }
};

const sendMsg = () => {
    if (!inputText.value.trim()) return;
    const msg = {
        type: 'chat',
        to: 'customer-service',
        content: inputText.value.trim()
    };
    websocketManager.send(msg);
    // 也在本地消息列表里加一条
    messages.value.push({
        from: 'me',
        content: msg.content,
        time: Date.now()
    });
    inputText.value = '';
};

onMounted(() => {
    websocketManager.on('message', handleMessage);
});

onUnmounted(() => {
    // 组件卸载时移除监听,避免内存泄漏
    websocketManager.off('message', handleMessage);
});
</script>

实际项目中还需要处理键盘弹起、消息时间格式化、发送中状态等,但核心的WebSocket交互已经完整了。注意到在onUnmounted里移除了监听,这是有必要的——如果不清理,多个页面来回切换会导致同一个事件被重复触发,出现消息重复显示的问题。

八、多端兼容的几个实测差异

在App、H5和小程序三端测试时,遇到了几个值得记录的行为差异:

  • 小程序端最大连接数限制:微信小程序同时只允许维护一条WebSocket连接。如果你的应用里还需要与其他服务建立WebSocket连接(比如实时日志),就会冲突。解决方法是只使用我们封装的管理器连接主服务,其他数据需求通过HTTP轮询或复用同一条WebSocket传输。
  • H5端跨域和安全策略:H5跑在浏览器里,连接wss://时如果服务端没有正确配置SSL证书,连接会静默失败。另外浏览器对localhost使用WebSocket时也可能遇到混合内容拦截。建议生产环境全部走wss,开发期只在真机或模拟器上测试。
  • App端后台运行:iOS和Android对后台WebSocket连接的处理不同。Android一般能维持几秒到十几秒,iOS进入后台后很快断开。如果需求要求后台持续接收消息,必须配合原生推送(如APNs、厂商通道)。我们的处理方式是在onShow生命周期里检查连接状态,如果断开就手动调一次websocketManager.connect

九、容易漏掉的内存释放

WebSocket和定时器是两种容易造成内存泄漏的东西。在我们的管理器里,heartbeatTimerreconnectTimer在连接关闭时都有清理逻辑,listeners也在off方法中移除。但有一种情况容易被忽略:如果页面没有调用off就销毁了,导致对已卸载组件实例的引用一直存在。

这个问题在当前示例中是安全的,因为我们在组件里显式调用了onUnmounted。但为了更健壮,可以在管理器上加一个destroy方法,清空所有监听器,在应用全局销毁时调用一次:

destroy() {
    this.listeners.clear();
    this.stopHeartbeat();
    this.close();
}

在实际项目里一般不需要,但如果是在测试环境反复热重载,没有彻底清理的话会看到多个心跳同时跑的现象。

十、总结

这套封装在目前的项目里跑得很稳,最直观的收益是:所有需要实时消息的页面都不再关心连接状态,只需要监听事件;重连逻辑完全透明,用户几乎感觉不到网络中断。代码量压缩在200行以内,没有引入任何第三方依赖。

如果你也面临类似的多端实时通信需求,可以直接把上面的代码拿去用,根据业务调整消息格式和重连策略即可。下一步可以考虑的增强方向包括:与服务端约定的心跳超时协商、断线期间未发送消息的本地队列缓存、以及消息已读回执的Ack机制。不过这些都属于锦上添花,核心框架已经足够支撑大多数场景了。

uni-app WebSocket实战:封装高可用的实时消息推送模块
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uni-app WebSocket实战:封装高可用的实时消息推送模块 https://www.taomawang.com/web/uniapp/2264.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务