uni-app中使用api式组件实现全局弹窗,用于后台消息推送

707 阅读4分钟

需求

最近在用uni app开发H5项目,其中的一个模块需要和PC端项目配合进行流程交互,场景是PC端编辑紧急维修单后,选择维修人员进行下令,要求实时通知,维修人员在H5端接令后进行现场维修,完成维修后进行回令,实时通知PC端进行归档。

解决思路

后端的同事使用了数据库触发器,PC端进行下令后,表单对应表的state字段会被修改,更改时会通知Java后端,之后就是使用WebSocket协议从Java后端推送到H5端。

前端方面遇见的问题

由于这个模块具有多个页面,要求进入该模块后,无论在何页面都能接到后台的消息推送。因为是uni app,页面之间的切换不是通过router-view,把弹窗写在router-view所在父组件里面的方案直接pass。

于是打算写好一份弹窗然后将代码复制到其他页面中。后端同事看了一眼说,这不合适吧。emm...好吧,确实不是很合适。

要不写在 App.vue 中吧,看了眼官网:

所有页面都是在App.vue下进行切换的,是应用入口文件。但App.vue本身不是页面,这里不能编写视图元素,也就是没有<template>

uni app 没办法在 App.vue 中写标签,而且就算能写,在指定模块弹窗和在全局弹窗还是需要额外的代码进行处理的。

经过一定的思索后,想到了vant ui的Toast组件,这个组件是函数式调用的:

Toast('提示内容');

直接扒源码,发现小小的组件,大大的复杂。

遇到困难后果断直接去查阅博客,找到一篇: VUE2开发API式组件_vue组件api_出了名的下班早的博客-CSDN博客

VUE中有标签式组件和API式组件,我们在开发过程中很多时候都是在写标签式组件,API式组件写的比较少,但如果我们使用了一些VUE的UI框架的话,那一定会使用过API式组件。API式组件一般用于弹窗、消息提醒等方面,也就是暂时显示类型的组件,比如ElementUI的$message、$alert等。

弹窗组件

<template>
    <!-- 维修单接令弹窗 -->
    <van-popup v-model="jlShow" position="bottom" closeable>
      <!-- 标题 -->
      <view class="title">维修单接令</view>
      <!-- 内容 -->
      <view class="content">
          <!-- 维修单编号 -->
          <view class="label_value">
            <view class="label">编号:</view>
            <view class="value">WX_202303001</view>
          </view>
          <!-- 下令单位 -->
          <view class="label_value">
            <view class="label">下令单位:</view>
            <view class="value">XXX</view>
          </view>
          <!-- 计划完成时间 -->
          <view class="label_value">
            <view class="label">计划完成时间:</view>
            <view class="value">2023年03月07日 18:00</view>
          </view>
          <!-- 抢修内容 -->
          <view class="nr">
               一区工作板异常,需要检查。
          </view>
        </view>
        <!-- 操作:拒绝 接令 -->
        <view class="action">
          <!-- 拒绝 -->
          <view class="no" @click="handleRejectJl">拒绝</view>
          <!-- 接令 -->
          <view class="yes" @click="handleJl">接令</view>
        </view>
      </view>
        <!-- 拒绝接令确认弹窗 -->
        <van-dialog v-model="rejectJlShow" title="" :show-confirm-button="false">
          <view class="reject_text"> 是否确认拒绝接令? </view>
          <view class="reject_action">
            <view class="cancel" @click="rejectJlShow = false">取消</view>
            <view class="confirm" @click="handleRejectJlConfirm">确认</view>
          </view>
        </van-dialog>
    </van-popup>
</template>

<script>
export default {
  data() {
    return {
    };
  },
};
</script>

<style lang="scss" scoped>
    // 样式部分就不贴代码了
</style>
import Vue from 'vue';
// 引入模板
import template from "./template";
 
const Component = Vue.extend(template) // 拿到继承模板后的类
 
const baseData = {
    jlShow: true, // 接令弹窗
    rejectJlShow: false, // 拒绝接令
  }

// 维修单接令弹窗
export const jl = (data)=>{
  //初始化组件实例对象
  const node = new Component({
    // data: data || {} // 将调用API组件时传入的参数赋值给组件的data
    data: data ? {...baseData, ...data} : baseData,
  })
  
  // 挂载组件
  node.$mount()

  // 将节点插入到页面中
  document.body.appendChild(node.$el)
}

如何与WebSocket配合使用

WebSocket实例对象有个onmessage属性,可用于监听后端推送的消息。

WS实例.onmessage = (event) => { 
    var msg = JSON.parse(event.data);
    jl({ wxd: msg.data, }); 
};

将WebSocket挂载到vue原型上,在进入指定模块时实例化ws,离开该模块时关闭ws。

import globalWs from "./utils/globalWs.js";
Vue.prototype.$globalWs = globalWs;

在进入指定模块时实例化ws:

onLoad() {
    this.$globalWs.initWs();
}

在 main.js 加入路由监控,离开该模块后进行WS的关闭:

Vue.mixin({
  onShow() {
    // 监听路由变化
    uni.onAppRoute((res) => {
      console.log('页面跳转:', res)
      // 在这里可以进行相应的处理和逻辑操作
      //  this.$globalWs.ws.close();
    })
  },
})

增加心跳检测用于断网重连

import store from "@/store";
import config from "@/config";
import { jl } from "@/components/jl";

// 全局 ws
export default {
  wsurl: `/websocket/message?${store.getters.user_id}`, // ws 连接url
  ws: null, // ws 实例对象
  ws_heart: "", // ws心跳定时器
  lockReconnect: false, // 是否真正建立连接,一把锁
  timeoutnum: null, // 断开情境下的重连倒计时
  heart_time: 10000, // 心跳时间,设置为 10 秒
  reconnect_time: 10000, // 重连间隔时间,设置为 10 秒
  initWs: function () {
    this.wsurl =
      config.baseUrl.replace("http", "ws").replace("https", "ws") + this.wsurl;

    if (typeof WebSocket === "undefined") {
      console.log("浏览器或所使用的浏览器引擎不支持WebSocket");
    } else {
      // 如果 WebSocket 已经建立连接进行关闭,避免重复连接
      if (this.ws) {
        this.ws.close();
      }
      // 实例化 WebSocket,建立连接
      this.ws = new WebSocket(this.wsurl);

      // 当连接成功时,开启心跳检测
      this.ws.onopen = () => {
        console.log("已经打开连接!");
        // 使用心跳重置的方式开启心跳检测
        this.reset();
      };

      this.ws.onmessage = (event) => {
        // 接收到新消息,使用心跳重置的方式进行心跳重启
        this.reset();
        // console.log("获取到服务器推送数据:", event.data);
        // 如果是心跳检测获取的数据则不做处理
        if (event.data == "ping_ok") {
        }
        // 其他情况,进行处理
        else {
          var msg = JSON.parse(event.data);
          jl({
            wxd: msg.data,
          });
        }
      };
      this.ws.onclose = () => {
        // 断开连接后进行重连
        this.reconnect();
      };
      this.ws.onerror = () => {
        // 断开连接后进行重连
        this.reconnect();
      };
    }
  },
  // 重新连接(断开或错误后需要进行重连)
  reconnect() {
    if (this.lockReconnecting) {
      return;
    }

    // 锁:没连接上会一直调用reconnect(),这里设置一把锁,避免触发过多不应该的实际重连操作
    this.lockReconnect = true;

    // 清除重连定时器
    this.timeoutnum && clearTimeout(this.timeoutnum);

    this.timeoutnum = setTimeout(() => {
      console.log("重连中");
      // 重连
      this.initWs();

      // 解锁,再次调用 reconnect 时,实际触发重连
      this.lockReconnect = false;
    }, this.reconnect_time);
  },
  // 建立连接及有新消息接收后进行心跳重置
  reset: function () {
    // console.log("重启心跳检测");
    // 移除心跳检测
    this.ws_heart && clearInterval(this.ws_heart);
    // 重启心跳检测
    this.start();
  },
  // 心跳检测
  start: function () {
    // 实时推送 ping 消息,查看连接是否断开
    this.ws_heart = setInterval(() => {
      const actions = "ping";
      try {
        this.ws.send(JSON.stringify(actions));
      } catch (e) {
        console.log(e);
      }
    }, this.heart_time);
  },
};