小程序 -- webSocket

2,517 阅读6分钟

在我们的项目中,实际运用WebSocket的场景。
一个冰柜,内置三方的软件系统,用来操作硬件的一些属性(开锁、开门、关门、关锁等), 服务端需调用三方软件系统(设备系统的)的api来操作硬件。
例: 服务端向三方发送开门指令后,会收到设备系统的开门成功、开门失败、关门成功、订单商品信息等事件回调,服务端需要将这几种事件推送给前端,前端根据不同的事件处理不同的交互逻辑。

1  查阅资料,了解什么是webSocket ?

什么是webSocket ?与"轮询"的区别是什么?webSocket的优缺点?webSocket的常用属性及API?

菜鸟教程-HTML5 WebSocket

阮一峰的网络日志-WebSocket 教程

2 了解微信小程序的webSocket?

微信小程序 WebSocket

Taro WebSocket

3  提前了解常见问题和解决方案。 

 网上搜几篇文章,简单了解一下行业前辈在开发中遇到的问题及解决方案。在设计的时候能避免就直接避免掉。

4  项目开发

4.1  长链接接口设计

          因为我们使用的业务场景比较简单且都是操作冰柜后的一系列回调信息,推送消息体内容格式也是一致的,所以设计了一个长链接。

        入参:userId 

        出参: actionType 事件类型、 orderNo 订单号

        前端根据actionType判断,处理对应的交互逻辑。根据orderNo判断消息的有效性。

4.2  前端调用

4.2.1  webSocket.js 建立连接、重连等方法封装

import Taro from "@tarojs/taro";
import store from "../models/dva";
import config from "../common/config";

let socketOpen = false; //建立长链接的状态, 避免重复建立连接。

// 建立链接
// webSocket 接收到的设备开锁成功、开锁失败、关门成功事件推送
// actionType : (2100, "开锁成功"), (2200, "开锁失败"), (3000, "关门成功")
export function getWebSocket() {
  if (socketOpen) {
    return;
  }
  const dispatch = store.getDispatch();
  const userId = Taro.getStorageSync(`${config.env}userId`);
  Taro.connectSocket({
    url: `${config.baseWssUrl}/link/websocket/action/${userId}`,
    header: {
      "content-type": "application/json",
    },
    success: () => {
      console.log("connect success");
    },
  }).then((task) => {
    // 监听 WebSocket 连接打开事件
    task.onOpen((res) => {
      socketOpen = true;
    });
    // 监听 WebSocket 接受到服务器的消息事件
    task.onMessage((res) => {
      console.log("onMessage success", res);
      const data = JSON.parse(res.data || "");
      dispatch({
        type: "Fridge/setWebSocketFridgeState",
        payload: data,
      });
    });
    // 监听 WebSocket 错误事件
    task.onError((res) => {
      console.log("onError", res);
      socketOpen = false;
      reconnect();
    });
    // 监听 WebSocket 连接关闭事件
    task.onClose((res) => {
      console.log("onClose", res);
      socketOpen = false;
      reconnect();
    });
  });
}

// 断开重连
export function reconnect() {
  setTimeout(function() {
    getWebSocket();
  }, 2000);
}

4.2.2  goods.js  点击开门按钮, 创建订单成功后,跳转微信支付分小程序前建立长链接。

import React, { Component } from "react";
import Taro from "@tarojs/taro";
import { connect } from "react-redux";
import { View } from "@tarojs/components";
import { AtButton } from "taro-ui";
import { getWebSocket } from "../../utils/webSocket";

import styles from "./index.module.scss";

class Goods extends Component {
  componentDidShow() {}

  onClickOpenDoor = async () => {
    //  点击立即开门,开门状态变为开门中,建立长链接;避免因为后台已收到开门成功回调,链接还没建立。
    getWebSocket();
    //  跳转微信支付分小程序
    Taro.navigateToMiniProgram({
      appId: "wxd8f3793ea3b935b8",
      path: "pages/use/use",
      extraData: data.sign || {},
    });
  };

  render() {
    const { webSocketActionType } = this.props;
    return (
      <View className={`container ${styles.goods}`}>
        <View className={styles.bottom_box}>
          <AtButton
            disabled={
              webSocketActionType == 1 || webSocketActionType == 2100
                ? true
                : false
            }
            onClick={this.onClickOpenDoor}
            className={styles.bottom_button}
          >
            {/*  0 为初始化状态,前端定义。  1 为开门中,前端定义,建立连接时调用。*/}
            {webSocketActionType == 1
              ? "开门中,请稍后..."
              : webSocketActionType == 2100
              ? "门已打开,拉门购物"
              : "立即开门"}
          </AtButton>
        </View>
      </View>
    );
  }
}

export default connect(({ Fridge }) => ({
  fridgeUrlParmas: Fridge.fridgeUrlParmas,
  webSocketActionType: Fridge.webSocketActionType,
  webSocketOrderNo: Fridge.webSocketOrderNo,
  goods: Fridge.goods || [],
}))(Goods);

4.2.3  fridge.js  接收到websocket消息后的逻辑处理。

import Taro from "@tarojs/taro";
import { toast } from "../utils/utils";

export default {
  namespace: "Fridge",
  state: {
    webSocketActionType: 0, // 商品页开门按钮状态 (根据实际业务场景改变,目前一共五种状态, 0、1为前端定义,2100、2200、3000为后端定义的推送消息事件 )
    webSocketOrderNo: "",  //  推送消息携带的订单号。 前端用来做消息有效性验证。 
  },
  subscriptions: {},
  effects: {
    // webSocket 接收到的设备开锁成功、开锁失败、关门成功等事件推送。
    // actionType:
    /**
     * actionType=0 初始状态,按钮提示“立即开门”
     * // 场景:
     * 1、支付分小程序头部路由返回。
     * 2、重新扫描二维码。
     * 3、2200、3000状态处理后,初始化。
     *
     *
     * actionType=1  开门中,按钮置灰,提示“开门中请稍后”
     * // 场景
     * 1、支付分小程序点击返回按钮(确认订单后,多笔订单进行中), 开门状态变为开门中。
     *
     *
     * actionType=2100 开锁成功, 按钮置灰,提示“门已开,请购物”
     * // 场景
     * 1、 轻购云开锁成功回调。
     * 2、美智开门成功回调。
     *
     * actionType=2200 开门失败, toast提示 “开门失败,请点击立即开门重新尝试!”
     * // 场景
     * 1、轻购云开锁接口报错。
     * 2、轻购云开锁失败回调。
     * 3、美智开门接口报错。
     * 4、美智开门失败回调。
     *
     * actionType=3000 开门成功, 跳转完成页。
     * 1、轻购云关门成功回调。
     * 2、美智关门成功回调。
     */
    *setWebSocketFridgeState({ payload = {} }, { select, call, put }) {
      const { createOrderResult } = yield select((state) => state.Order);
      // 当actionType 非0,非1时,需要用当前创建订单号与接收消息的订单号比较,判断消息有效性,失效不处理。
      let webSocketActionType = 0;
      if (payload.actionType == 0) {
        //初始化
        webSocketActionType = 0;
      } else if (payload.actionType == 1) {
        //开门中
        webSocketActionType = 1;
      } else if (
        createOrderResult.orderNo &&
        createOrderResult.orderNo == payload.orderNo
      ) {
        // 开门成功,按钮置灰
        if (payload.actionType == 2100) {
          webSocketActionType = 2100;
        }
        // 开锁失败,弹窗提示
        if (payload.actionType == 2200) {
          toast("开门失败,请点击立即开门重新尝试!");
          webSocketActionType = 0;
          yield put({
            type: "Order/setCreateOrderResult",
            payload: {
              orderNo: "",
            },
          });
        }
        // 关门成功,跳转完成页。【关门成功,请放心离开】
        if (payload.actionType == 3000) {
          Taro.reLaunch({
            url: "/pages/resultPage/index?type=closeTheDoor",
          });
          webSocketActionType = 0;
          yield put({
            type: "Order/setCreateOrderResult",
            payload: {
              orderNo: "",
            },
          });
        }
      }
      yield put({
        type: "updateAction",
        payload: {
          webSocketActionType: webSocketActionType,
        },
      });
    },
  },
  reducers: {
    updateAction(state, action) {
      return { ...state, ...action.payload };
    },
  },
};

5  实际开发中可能遇到的一些问题?

5.1  小程序中长链接的并发数?

1.7.0 及以上版本,最多可以同时存在 5 个 WebSocket 连接。

1.7.0 以下版本,一个小程序同时只能有一个 WebSocket 连接,如果当前已存在一个 WebSocket 连接,会自动关闭该连接,并重新创建一个 WebSocket 连接。

5.2  如何对长链接进行管理?

推荐使用 SocketTask 的方式去管理 webSocket 链接,每一条链路的生命周期都更加可控。同时存在多个 webSocket 的链接的情况下使用 wx 前缀的方法可能会带来一些和预期不一致的情况

5.3  小程序中建立全局的、页面级别的长链接?

根据项目的使用场景, 来决定是建立什么级别的的长链接。

我们的业务场景为购物流程中的交互才使用长链接, 所以在用户进入小程序时不需要建立长链接,只需要用户在进入购买流程后建立即可。

5.4  长链接推送的消息在多个页面使用,如何保证参数传到页面上?

我们的项目技术栈为taro+react,在这个项目里我们引入了dva来做数据管理。 长链接的消息也是通过dva来管理。

5.5  如何保证长链接一直保持连接状态?

前端建立重连机制。(需避免重复连接,频繁断开重连等场景)

前后端都需要有心跳检测机制,避免因为网络问题接收不到连接失败消息等问题。

注:我们项目中,后端设置了若5分钟内没有消息推送, 自动断开连接。

一期因为项目时间比较紧张,没有做心跳包检测机制,目前只做了重连功能。

因为我们在进入开门流程后,判断连接状态为false会重新连接,使用场景比较简单, 不建立心跳包影响不大。

5.6  如何保证消息的即时性和可靠性?消息不丢失?处理重复消息?消息有序性?

 这些是后端同事在处理,细节我也不是很清楚。 有兴趣可以上网搜一搜。