+
7
-

回答

要在 uni-app 中实现兼容 Web、小程序及 App 端的 WebSocket 通信,核心在于使用 uni.connectSocket API,并针对不同平台的特性和限制进行适配。以下将提供一个详细的实现方案,包括封装可重用的 WebSocket 模块、心跳机制、自动重连以及各端兼容性处理。

核心 API:uni.connectSocket

uni-app 框架提供了一套兼容多端的 WebSocket API,主要包括:

uni.connectSocket(OBJECT): 创建一个 WebSocket 连接,返回一个 SocketTask 对象。

SocketTask.onOpen(CALLBACK): 监听 WebSocket 连接打开事件。

SocketTask.onMessage(CALLBACK): 监听 WebSocket 接收到服务器的消息事件。

SocketTask.onError(CALLBACK): 监听 WebSocket 错误事件。

SocketTask.onClose(CALLBACK): 监听 WebSocket 连接关闭事件。

SocketTask.send(OBJECT): 通过 WebSocket 连接发送数据。

SocketTask.close(OBJECT): 关闭 WebSocket 连接。

官方建议使用 SocketTask 对象来处理 WebSocket 相关事件,因为它提供了更精细的控制,尤其是在需要管理多个连接或复杂交互的场景中。

封装通用的 WebSocket 模块

为了方便在项目中复用,强烈建议将 WebSocket 的逻辑封装成一个独立的模块。这不仅能让代码更清晰,也便于集中处理多端兼容问题、心跳和重连逻辑。

以下是一个封装的 websocket.js 模块示例,它包含了连接、心跳检测、断线重连等核心功能:

// websocket.js
class WebSocketService {
  constructor(url, heartBeatConfig = {}) {
    this.url = url;
    this.socketTask = null;
    this.isReconnecting = false;
    this.reconnectTimer = null;
    this.heartBeatTimer = null;
    this.userClose = false; // 用户主动关闭

    // 心跳配置
    this.heartBeat = {
      interval: 30000, // 心跳间隔,单位ms
      pingData: 'ping', // 心跳请求消息
      ...heartBeatConfig,
    };

    this.connect();
  }

  // 连接
  connect() {
    if (this.socketTask && this.socketTask.readyState === 1) {
      return;
    }
    this.userClose = false;
    this.socketTask = uni.connectSocket({
      url: this.url,
      success: () => {
        console.log('WebSocket 正在连接...');
      },
      fail: (err) => {
        console.error('WebSocket 连接失败:', err);
        this.reconnect();
      },
    });

    this.initEventListeners();
  }

  // 初始化事件监听
  initEventListeners() {
    if (!this.socketTask) return;

    this.socketTask.onOpen(() => {
      console.log('WebSocket 连接已打开!');
      this.isReconnecting = false;
      this.startHeartBeat();
      // 连接成功后可以触发一个全局事件或回调
      uni.$emit('onSocketOpen');
    });

    this.socketTask.onMessage((res) => {
      // 收到任何消息都说明连接正常,重置心跳
      this.startHeartBeat();
      // 如果服务器返回pong,则不处理业务逻辑
      if (res.data === 'pong') {
        return;
      }
      // 触发消息事件
      uni.$emit('onSocketMessage', res.data);
    });

    this.socketTask.onError((err) => {
      console.error('WebSocket 出现错误:', err);
      this.reconnect();
      uni.$emit('onSocketError', err);
    });

    this.socketTask.onClose((res) => {
      console.log('WebSocket 连接已关闭', res);
      // 非用户主动关闭,则进行重连
      if (!this.userClose) {
        this.reconnect();
      }
      uni.$emit('onSocketClose');
    });
  }

  // 发送消息
  send(data) {
    if (this.socketTask && this.socketTask.readyState === 1) {
      this.socketTask.send({
        data: JSON.stringify(data),
        success: () => {
          console.log('消息发送成功');
        },
        fail: (err) => {
          console.error('消息发送失败:', err);
        },
      });
    } else {
      console.error('WebSocket 未连接,无法发送消息');
    }
  }

  // 心跳机制
  startHeartBeat() {
    this.stopHeartBeat();
    this.heartBeatTimer = setInterval(() => {
      this.send({ type: 'ping', data: this.heartBeat.pingData });
    }, this.heartBeat.interval);
  }

  stopHeartBeat() {
    if (this.heartBeatTimer) {
      clearInterval(this.heartBeatTimer);
      this.heartBeatTimer = null;
    }
  }

  // 重新连接
  reconnect() {
    if (this.isReconnecting || this.userClose) return;
    this.isReconnecting = true;
    this.stopHeartBeat();

    clearTimeout(this.reconnectTimer);
    this.reconnectTimer = setTimeout(() => {
      console.log('尝试重新连接...');
      this.connect();
    }, 5000); // 5秒后重连
  }

  // 关闭连接
  close() {
    if (this.socketTask) {
      this.userClose = true;
      this.stopHeartBeat();
      this.socketTask.close({
        code: 1000,
        reason: '用户主动关闭',
      });
    }
  }
}

export default WebSocketService;

在页面中使用封装的模块

全局挂载或按需引入:你可以在 main.js 中全局实例化并挂载,方便任何页面调用:

// main.js
import Vue from 'vue';
import App from './App';
import WebSocketService from './utils/websocket.js';

Vue.prototype.$socket = new WebSocketService('wss://your-server-url/socket');

const app = new Vue({
  ...App,
});
app.$mount();

在 Vue 页面中监听和发送消息

<template>
  <view>
    <button @click="sendMessage">发送消息</button>
  </view>
</template>

<script>
export default {
  created() {
    // 监听消息
    uni.$on('onSocketMessage', this.handleMessage);
    uni.$on('onSocketOpen', this.handleSocketOpen);
  },
  beforeDestroy() {
    // 移除监听,避免内存泄漏
    uni.$off('onSocketMessage', this.handleMessage);
    uni.$off('onSocketOpen', this.handleSocketOpen);
  },
  methods: {
    handleSocketOpen() {
      console.log("WebSocket连接成功,可以发送消息了");
    },
    handleMessage(data) {
      console.log('收到服务器消息:', data);
      // 在这里处理你的业务逻辑
    },
    sendMessage() {
      this.$socket.send({ content: 'Hello from uni-app' });
    }
  }
};
</script>

多端兼容性注意事项

尽管 uni.connectSocket 实现了跨平台,但在不同环境下仍存在差异和需要注意的点:

URL 协议:

小程序端: 必须使用 wss:// (加密的 WebSocket) 协议。 同时,需要在微信小程序后台、支付宝小程序后台等配置合法的 WebSocket 服务器域名。

Web (H5) 端: 同时支持 ws:// 和 wss://。

App 端: iOS 对网络安全要求较高,建议统一使用 wss:// 以避免连接问题。 在安卓端,ws:// 和 wss:// 通常都支持,但 wss:// 是更安全的选择。

连接数限制:

App 端: 老版本(2.2.6以下)的 App,所有页面共享一个 WebSocket 连接。若需多个连接,可以使用 plus-websocket 插件。 新版本中此限制已放宽。

小程序端: 不同小程序平台对 WebSocket 的并发连接数有限制。例如,微信小程序最多支持 5 个并发连接。

iOS App 的特定问题:

有时会遇到在 Web 和安卓上正常,但在 iOS App 上无法连接的情况。这通常与 SSL 证书或网络配置有关。

解决方案: 可以尝试使用 DCloud 插件市场的 plus-websocket 插件,它能解决部分 iOS 的兼容性问题。

条件编译处理平台差异:虽然上述封装已处理了大部分通用场景,但如果遇到特定平台的特殊需求,可以使用条件编译来编写平台独有的代码。

// #ifdef MP-WEIXIN
// 只在微信小程序平台执行的代码
console.log('This is in WeChat Mini Program');
// #endif

// #ifdef APP-PLUS
// 只在 App 平台执行的代码
console.log('This is in App');
// #endif

关于 uts 文件:如果在 uts (uni-app TypeScript Syntax) 文件中使用 uni.connectSocket,需要注意异步操作的处理,并确保代码与 uni-app 的 JavaScript 环境兼容,否则可能引发底层错误。

通过以上封装和对各端差异的了解,你就可以在 uni-app 项目中构建一个稳定、可靠且兼容 Web、小程序和 App 端的 WebSocket 通信系统。

网友回复

我知道答案,我要回答