需求
最近在用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);
},
};