最近需要开发一款产品,需要实现前后端双端实时通讯,且为了便于后期更新,前端采用APP+H5的混合模式开发,变化少的内容放到APP端,变化大的内容放到H5端。实时通讯通过websocket来实现,连接APP端和服务端。然后在实现APP端与H5端的双端通讯,以APP端为桥梁,实现三端互通。
一、技术选型
1、后端 采用netty-websocket
/** * 客服端平台webSocket服务终端 */ @ServerEndpoint(value = "/wisdomWs/wisdomScreen/{deviceNo}", port = "12349") @Slf4j public class WisdomScreenWebSocketEndPointer { private final static ConcurrentHashMap<String, Session> webSocketMap = new ConcurrentHashMap<>(); //等待死亡的连接 private final static ConcurrentHashMap<String, Session> waitingWebSocket = new ConcurrentHashMap<>(); /** * 初始化 */ @PostConstruct public void init() { } @BeforeHandshake public void handshake(Session session, HttpHeaders headers, @RequestParam Map reqMap, @PathVariable String deviceNo, @PathVariable Map pathMap) { session.setSubprotocols("stomp"); String type = StrHelper.getObjectValue(reqMap.get("type")); } @OnOpen public void onOpen(Session session, HttpHeaders headers, @RequestParam Map reqMap, @PathVariable String deviceNo, @PathVariable Map pathMap) { session.setDeviceNo(deviceNo); // 时间戳 session.setDateKey(LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli()); // 新连接时及消息处理 webSocketMap.put(deviceNo, session); } @OnClose public void onClose(Session session) throws IOException { closeConnectionHandler(session); } @OnError public void onError(Session session, Throwable throwable) { closeConnectionHandler(session); } @OnMessage public void onMessage(Session session, String message) { // 接收消息处理 receiveMessage(session, message); } @OnBinary public void onBinary(Session session, byte[] bytes) { for (byte b : bytes) { System.out.println(b); } session.sendBinary(bytes); } @OnEvent public void onEvent(Session session, Object evt) { if (evt instanceof IdleStateEvent) { IdleStateEvent idleStateEvent = (IdleStateEvent) evt; switch (idleStateEvent.state()) { case READER_IDLE: System.out.println("read idle"); break; case WRITER_IDLE: System.out.println("write idle"); break; case ALL_IDLE: System.out.println("all idle"); break; default: break; } } } /** * 断开连接处理 * * @param session */ private void closeConnectionHandler(Session session) { session.close(); } /** * 接收到消息并处理 * * @param session * @param message */ private void receiveMessage(Session session, String message) { // 获取设备号 String deviceNo = session.getDeviceNo(); // 处理 心跳 接收到0 回复1 if (StringUtils.equals(message, WebSocketFixedMessage.PING.getBody())) { // 接收心跳包的时间 if (Objects.nonNull(session.getDeviceNo())) { exceptionConnectHandler(session); } session.sendText(WebSocketFixedMessage.PONG.getBody()); return; } JSONObject returnMsg = new JSONObject(); returnMsg.put("message","我是客户端发送返回的消息:"+message); returnMsg.put("action","showMessage"); // 发送消息 SendMsg(returnMsg.toJSONString(),deviceNo); } /** * 异常连接处理 * * @param session */ private void exceptionConnectHandler(Session session) { String deviceNo = session.getDeviceNo(); Session conn = webSocketMap.get(deviceNo); if (Objects.isNull(conn.getLastHeartTime())) { conn.setLastHeartTime(LocalDateTime.now()); } else { if (conn.getLastHeartTime().plusHours(1).isBefore(LocalDateTime.now())) { webSocketMap.remove(deviceNo); } } } /** * 发送消息 * @param message * @param deviceNo * @return */ public boolean SendMsg(String message, String deviceNo) { ChannelFuture channelFuture = null; if (webSocketMap.size() > 0) { for (Map.Entry<String, Session> entry : webSocketMap.entrySet()) { boolean flag = false; if (deviceNo.equals(entry.getKey())) { flag = true; } if (flag) { channelFuture = entry.getValue().sendText(message); break; } } } if(channelFuture!=null){ channelFuture.awaitUninterruptibly(); } return (channelFuture == null || !channelFuture.isSuccess()) ? false : true; } }
2、APP端,采用uni-app的纯nvue模式
3、H5端,使用vue搭建项目
本项目采用wwvue-cli脚手架搭建
参考开源地址:https://github.com/vannvan/wwvue-cli
开箱即用命令:1、安装 npm i wwvue-cli -g 使用 2、wwvue init project-name
二、核心代码展示
1、APP端与服务端交互相关内容
websocketUtils.js
import { wsBaseUrl, fixedMessageEnum } from './config' import * as db from './db.js' import { ArrayQueue, navigateTo } from '@/config/common' import store from '@/store' import * as common from './common.js' import AppVersionManager from './AppVersionManager.js' let __assign = (this && this.__assign) || function() { __assign = Object.assign || function(t) { for (let s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (let p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; let heartTimer = null; // 心跳句柄 let reConnectTimer = null; // 重连句柄 let isClose = true; let config = null; let socketTask = null; let messageQueue = new ArrayQueue(); let connectOK = false // 连接是否成功 export default class WebSocketHandler { constructor(param) { config = __assign({ token: '', isReconnect: true, // 是否断线重连 isHeartData: true, // 是否开启心跳 heartTime: 5000, // 心跳时间 reConnectTime: 5000, // 重连时间间隔 initConnected: function(data) { // 接收服务器连接反馈消息 } }, param); let _this = this; // 初始化 this.initWebSocket = function(success, fail) { if (!db.get('deviceNo')) { common.getDeviceNo() } let baseUrl = '' if (wsBaseUrl) { baseUrl = wsBaseUrl } else { baseUrl = db.get('wsBaseUrl') } let url = baseUrl + db.get('deviceNo'); socketTask = uni.connectSocket({ url: url, method: 'GET', header: { 'content-type': 'application/json' }, // protocols: ['protocol1'], success: function() { console.log('success') typeof success == "function" && success(_this); }, fail: function(err) { console.log('fail') config.initFailConnected(connectOK) typeof fail == "function" && fail(err, _this); }, complete: function() { // 连接完成 _this.initComplete() } }); // 监听socket是否打开成功 socketTask.onOpen(function(res) { isClose = false; connectOK = true if (config.isHeartData) { // console.log("%c [uni-socket-promise] %c 开始心跳", 'color:red;', 'color:#000;'); _this.clearHeart(); _this.startHeart(); } clearInterval(reConnectTimer) reConnectTimer = null }) // 监听socket关闭 socketTask.onClose(function() { connectOK = false if (config.isHeartData && heartTimer != null) { // console.log("%c [uni-socket-promise] %c 关闭心跳", 'color:red;', 'color:#000;'); _this.clearHeart(); } // 判断是否为异常关闭 if (reConnectTimer == null && !isClose && config.isReconnect) { // 执行重连操作 _this.reConnectSocket(); } }); // 监听到错误异常 socketTask.onError(function() { console.log(2) connectOK = false if (config.isHeartData && heartTimer != null) { _this.clearHeart(); } if (reConnectTimer == null && config.isReconnect) { // 执行重连操作 _this.reConnectSocket(); } }); // 接收到消息 socketTask.onMessage(function(data) { const message = JSON.parse(data.data) if (message instanceof Object) { // 写具体的业务操作 _this.objectMessageHandler(_this, message); //必须 } else if (!isNaN(message)) { // 是数字 //固定格式消息处理 _this.fiexedMessagehandler(message) messageQueue.push(message) } else { console.log('非法数据,无法解析') } }) } this.initWebSocket() this.initComplete = function() {} // 心跳 this.startHeart = function() { heartTimer = setInterval(function() { // 发送心跳 uni.sendSocketMessage({ data: fixedMessageEnum['ping'].toString() }) }, config.heartTime); } // 清除心跳 this.clearHeart = function() { clearInterval(heartTimer); heartTimer = null; } this.refreshWebSocket = function(token) { config.token = token _this.initWebSocket() } //重连 this.reConnectSocket = function() { // 网络断开时,不需要再次去重连 if (store.getters.netWorkStatus) { reConnectTimer = setInterval(function() { if (!connectOK) { _this.initWebSocket(function(e) { // 比较奇怪,前端已经显示初始化完成,但实际上没有连接,所以取消重连的逻辑不能在这一步进行 config.reConnectTime += config.reConnectTime; }, function(err, e) { // 如果重新连接失败,则增加 重连时间 config.reConnectTime += config.reConnectTime; }); } else { clearInterval(reConnectTimer) } }, config.reConnectTime); } else { clearInterval(reConnectTimer) } } //全局固定格式消息处理 this.fiexedMessagehandler = function(message) { switch (message) { case fixedMessageEnum['pong']: break; case fixedMessageEnum['exit']: common.modelShow('提示', '您的账号在其他设备上登陆,如果这不是您的操作,请及时修改您的登陆密码。', () => { store.dispatch('logout') }, false) break; case fixedMessageEnum['update']: // 接受到更新指令 db.set('appMustUpdate', true) common.modelShow('提示', 'app发布了新版本,是否需要更新', () => { uni.reLaunch({ url: '/pages/my/update/index' }) }, true, '取消', '确定') store.state.isFirst = 1; break; case fixedMessageEnum['asking_exit']: // 接受到重复登陆指令,服务器询问是否踢人 config.initConnected(message) // 确认 // uni.sendSocketMessage({ // data: fixedMessageEnum['confirm_exit'].toString() // }) break; case fixedMessageEnum['connect_complete']: config.initConnected(message) default: break; } } // 业务消息处理 this.bizMessagehandler = function() { } //对象消息处理 this.objectMessageHandler = function(socket, message) { let messages = []; //1.接收消息并向后台发送消息 代表以经收到消息 if (Array.isArray(message)) { // 是数组 /* message.forEach(function(msg){ readMessage(socket,messages,msg); }) */ } else { //判断是否是messageType == 2 设备更新 if (message.type === 2 && message.messageType === 2) { common.modelShow('提示', 'app发布了新版本,是否需要更新', () => { const appVersionManager = new AppVersionManager(); appVersionManager.checkUpdate(store.getters.curSystemVersion, data => { // 需要更新 跳转到更新页面去执行更新 if (data) { uni.reLaunch({ url: `/pages/my/update/index?data=${JSON.stringify(data)}` }); } }); }, true, '取消', '确定') } } if (messages.length == 0) { return; //消息无效 } } } // 发送消息 sendMessage(message) { return new Promise(function(resolve, reject) { uni.sendSocketMessage({ data: message, success: function(msg) { return resolve(msg); }, fail: function(msg) { return reject(res); } }) }) } //关闭 close() { isClose = true; if (config.isHeartData) { this.clearHeart(); } // 关闭socket uni.closeSocket(); } /** * 获取所有后端发送消息队列 */ getMessageQueue() { return messageQueue; } /** * 是否连接成功 */ isConnection() { return !isClose } }
store.js
import Vue from 'vue' import Vuex from 'vuex' import * as db from '@/config/db' //引入db import * as common from '@/config/common' import { fixedMessageEnum } from '@/config/config' import WebSocketHandler from '@/config/webSocketUtils' import { getToday } from "@/config/date-utils.js" Vue.use(Vuex) const store = new Vuex.Store({ state: { searchStyle: '', userInfo: db.get('userInfo'), //登录用户信息 webSocketCon: null, // webSocket对象 netWorkOk: true, // 网络连接状态 curSystemVersion: '', // 当前系统的版本号 isScan: false, scanLoginFlag: false, loadHomeFlag: true }, mutations: { SEARCH_STYLE(state, style) { state.searchStyle = style }, SET_TOKEN: (state, token) => { state.token = token db.set('userToken', token) }, SET_USERINFO: (state, userinfo) => { state.userInfo = userinfo db.set('userInfo', userinfo) }, SET_SYSTEM_VERSION: (state, version) => { state.curSystemVersion = version }, INIT_WEBSOCKET: (state, payload) => { //重置websocket state.webSocketCon = new WebSocketHandler({ token: payload.token, isReconnect: true, isHeartData: true, heartTime: 15000, initConnected: function(data) {} }) }, SET_NETWORK_STATUS: (state, status) => { state.netWorkOk = status } }, actions: {}, getters: { token: state => state.token, userInfo: state => state.userInfo, netWorkStatus: state => state.netWorkOk, // 网络状态 webSocketHandler: state => state.webSocketCon, // websocket连接对象 curSystemVersion: state => state.curSystemVersion } }) export default store
setting.nvue
<template> <div> <div style="margin-top: 20rpx;"> <text class="title">设备号:</text><input class="content" v-model="deviceNo" placeholder="设备号" /> </div> <div> <text class="title">服务端地址:</text><input class="content" v-model="serverUrl" placeholder="请输入服务端地址" /> </div> <div> <text class="title">h5端地址:</text><input class="content" v-model="h5Url" placeholder="请输入h5端地址" /> </div> <div> <text class="title">消息内容:</text><input class="content" v-model="message" placeholder="请输入要发送的消息" /> </div> <div> <text class="title">服务端消息回执:</text><input class="content" v-model="receiveMessage" placeholder="服务端返回的消息" /> </div> <div> <text class="title">h5端消息回执:</text><input class="content" v-model="h5ReceiveMessage" placeholder="h5端返回的消息" /> </div> <div> <button type="default" @click="connnectServer">创建连接</button> <button type="default" @click="sendMessage">发送消息</button> <button type="default" @click="skipPage">跳转网页</button> </div> </div> </template> <script> import * as db from '@/config/db.js' import * as common from '@/config/common.js' export default { data() { return { deviceNo: '', serverUrl: 'ws://192.168.2.125:12349/wisdomWs/wisdomScreen/', //h5Url: 'http://daohang.zjh336.cn/demo2/index.html', h5Url: 'http://192.168.2.125:8082/home', message: '消息!消息!消息!消息!', receiveMessage: '', h5ReceiveMessage: '' } }, onLoad(option) { /* h5方式获取设备号 const that_ = this plus.device.getInfo({ success(res){ that_.deviceNo = res.uuid } }) */ // 内置方法获取设备号 if (!db.get('deviceNo')) { common.getDeviceNo() } this.deviceNo = db.get('deviceNo') if (option.message) { this.h5ReceiveMessage = option.message } // 预载页面 uni.preloadPage({ url: "/pages/index/index" }); }, methods: { // 连接服务端 connnectServer() { // 设置websocket服务端地址 db.set('wsBaseUrl', this.serverUrl) // 初始化websocket连接 this.$store.commit('INIT_WEBSOCKET', ''); // 重写创建连接成功回调方法 this.$store.getters.webSocketHandler.initComplete = () => { this.receiveMessage = '创建连接成功!' } // 重写接收到消息回调方法 this.$store.getters.webSocketHandler.objectMessageHandler = (socket, message) => { this.receiveMessage = message.msg // 判断类型为sendH5 则直接发送到h5 if (message.action === 'sendH5') { // 编码 const paramStr = encodeURIComponent(JSON.stringify(message)) const vw = plus.webview.getWebviewById('evol-costom-medical-webview') vw.evalJS("setParams('"+paramStr+"')") } } }, // 发送消息到服务端 sendMessage() { // 调用消息发送方法 this.$store.getters.webSocketHandler.sendMessage(this.message) }, // 跳转页面 skipPage() { // 跳转到h5页面 uni.navigateTo({ url: '/pages/index/index?url=' + this.h5Url }) } } } </script> <style> .title { font-size: 15rpx; } .content { font-size: 15rpx; height: 18rpx; } </style>
2、APP端与H5端交互相关内容
index.nvue
<template> <view></view> </template> <script> import * as config from '@/config/config.js' import * as common from '@/config/common.js' export default { data() { return { url: '', webviewIsReady: false } }, onLoad(option) { // 获取传入url参数 if (option.url) { //拼接时间戳 this.url = option.url + `?t=` + new Date().getTime() //预加载h5页面 plus.webview.prefetchURL(this.url) //创建webview const wv = plus.webview.create(this.url, config.webMedicalViewIdEnum, { top: 0, //放置在titleNView下方。如果还想在webview上方加个地址栏的什么的,可以继续降低TOP值 bottom: 0 }) // 监听标题修改事件 common.monitorTitleUpdate(wv, this.onPostMessage) setTimeout(() => { if (this.webviewIsReady) { // 创建消息对象 const messageObj = { message: '123456', action: 'showMessage' } // 调用h5端setParams方法 common.webViewSetParams(messageObj, config.webMedicalViewIdEnum) } else { // TODO 如果需要 网页未加载完成之前就发送消息 则此处需要做一个消息队列 待onPostmessage接收到加载完成的消息后,重新发送队列中的消息给网页 } }, 2000) } }, methods: { // 处理h5发送过来的消息 onPostMessage(res) { // 接收到的消息内容 console.log(res) // 加载完成 if (res.action === 'loaded' && !this.webviewIsReady) { // 接收到的消息内容 const wv = plus.webview.getWebviewById(config.webMedicalViewIdEnum) //添加webview到当前窗口 plus.webview.currentWebview().append(wv) // 设置 this.webviewIsReady = true } else if (res.action === 'showMessage') { // 解析message 并且传入到设置页面,展示接收到的消息 uni.redirectTo({ url: '/pages/setting/setting?message=' + res.message }) } } } } </script> <style> </style>
3、H5端与APP交互相关内容
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="<%= BASE_URL %>_favicon2.ico"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> <script> // 发送消息给app window.sendMessageApp = (action, message) => { const messageData = { action:action, message:message, timestamp:new Date().getTime() } document.title = JSON.stringify(messageData) } // 提供给app调用 window.setParams = (params) => { // 解码并转js对象 const messageData = JSON.parse(decodeURIComponent(params)) // 调用挂载的layOut组件vue对象的receiveMessage方法 广播来自app的消息 window.HMWS_VUE && window.HMWS_VUE.receiveMessage(messageData) } </script> </html>
layout.vue
<template> <div> <router-view /> </div> </template> <script> export default { mounted() { // 挂载当前vue对象到window中 window.HMWS_VUE = this // 发送消息到app 加载完成 window.sendMessageApp('loaded', '加载完成') }, methods: { // 定义receiveMessage方法 广播消息内容 receiveMessage(messageData) { // 触发消息总线方法 this.$EventBus.$emit('receiveMessage', messageData) } } } </script> <style> </style>
home.vue
<template> <div> <div><span>当前信息:</span>{{ message }}</div> <div> <span>需要发送的消息:</span><el-input v-model="curMessage" /> <el-button @click="sendMessage">发送消息</el-button> <el-button @click="skipHome">跳转页面</el-button> </div> </div> </template> <script> export default { data() { return { // message: '当前页面消息', curMessage: '' } }, created() { // 监听收到消息事件 this.$EventBus.$on('receiveMessage', (messageData) => { // 接收到的消息 console.log(messageData) // 获取消息内容 this.message = messageData.message }) }, methods: { sendMessage() { // 向app发送消息 window.sendMessageApp('showMessage', this.curMessage) }, skipHome() { // 跳转页面 this.$router.push('./HelloWorld') } } } </script> <style> </style>
三、交互说明
一、APP端与websocket服务端交互说明 1、涉及核心文件: @/config/webSocketUtils.js @/store/index.js 2、代码说明 2.1 webSocketUtils定义了WebSocketHandler对象 创建该对象后可以直接使用的方法: sendMessage发送消息方法、close关闭、getMessageQueue获取消息队列方法、isConnection是否连接方法 构造方法中包含:initWebSocket初始化方法、startHeart开始心跳、clearHeart清除心跳、refreshWebSocket刷新、reConnectSocket重连、 fiexedMessagehandler固定消息处理、bizMessagehandler业务消息处理、objectMessageHandler对象消息处理、initComplete初始化完成方法 可以通过重写方法的方式来进行自定义回调 构造方法入参:token、isReconnect是否断线重连、isHeartData是否开启心跳、heartTime心跳时间、reCoonectionTime重连时间间隔、initConnected接收服务器连接反馈消息 2.2 store的mutations中挂载了INIT_WEBSOCKET方法,创建了一个WebSocketHandler对象,并且挂载到state中,可以通过getters方法获取到WebSocketHandler 3、使用方式 3.1 设置websocket服务端地址 db.set('wsBaseUrl', 'ws://localhost:12349') 3.2 初始化websocket连接 this.$store.commit('INIT_WEBSOCKET', '') 3.3 重写创建连接成功回调方法 this.$store.getters.webSocketHandler.initComplete = () => { this.receiveMessage = '创建连接成功!' } 3.4 重写接收到消息的回调方法 this.$store.getters.webSocketHandler.objectMessageHandler = (socket, message) => { this.receiveMessage = message.message } 3.5 发送消息到服务端 this.$store.getters.webSocketHandler.sendMessage(this.message) 4、消息体说明 { action:'', // 指令 showMessage 显示消息,loaded 加载完成,sendH5 直接发送到H5 message:'', // 消息内容 timestamp:'' // 时间戳 } 二、APP端与H5端交互说明 H5端通过webView的方式嵌入到APP中,以@/pages/index/index.nvue为例,在onLoad中,预载H5端页面,创建webview对象,并且监听标题修改事件。 在标题修改事件中,如果接收到h5端返回的加载完成消息,则将webview对象添加到当前窗口。 1、消息体说明 { action:'', // 指令 showMessage 显示消息,loaded 加载完成,sendH5 直接发送到H5 message:'', // 消息内容 timestamp:'' // 时间戳 } 2、APP端发送消息到H5端 2.1 创建消息对象 const messageObj = { message: '123456', action: 'showMessage', timestamp: new Date().getTime() } 2.2 编码 防止消息中存在特殊字符 const paramStr = encodeURIComponent(JSON.stringify(messageObj)) 2.3 调用方法 其中getWebviewById的参数为创建webView对应的id plus.webview.getWebviewById('evol-costom-medical-webview').evalJS("setParams('" +paramStr + "')") 2.4 已封装方法在common中 webViewSetParams(messageData, webviewId) 示例: // 创建消息对象 const messageObj = { message: '123456', action: 'showMessage' } // 调用h5端setParams方法 common.webViewSetParams(messageObj, config.webMedicalViewIdEnum) 3、APP端接收H5端的消息 3.1 在创建webView时监听标题修改事件 wv.addEventListener('titleUpdate', (e) => { try { if (e.title) { //解析json const res = JSON.parse(e.title) // 处理h5发送过来的消息 this.onPostMessage(res) } } catch (e) {} }) 已封装方法在common中 monitorTitleUpdate(webview,onPostMessage) 示例: common.monitorTitleUpdate(wv, this.onPostMessage) 3.2 处理消息 onPostMessage(res) { // 加载完成 if (res.action === 'loaded' && !this.webviewIsReady) { // 接收到的消息内容 const wv = plus.webview.getWebviewById("evol-costom-medical-webview") //添加webview到当前窗口 plus.webview.currentWebview().append(wv) // 设置 this.webviewIsReady = true } else if (res.action === 'showMessage') { // 解析message 并且传入到设置页面,展示接收到的消息 uni.redirectTo({ url: '/pages/setting/setting?message=' + res.message }) } }
三、H5端与APP端交互说明 1、H5端接收APP端消息 1.1 在index.html中 给window挂载setParams方法 window.setParams = (params) => { // 解码并转js对象 const messageData = JSON.parse(decodeURIComponent(params)) // 调用挂载的layOut组件vue对象的receiveMessage方法 广播来自app的消息 window.HMWS_VUE && window.HMWS_VUE.receiveMessage(messageData) } 1.2 在@/layout/Layout.vue中 将this挂载到window中,对象名为HMWS_VUE mounted() { // 挂载当前vue对象到window中 window.HMWS_VUE = this // 发送消息到app 加载完成 window.sendMessageApp('loaded', '加载完成') } 1.3 在main.js中挂载EventBus Vue.prototype.$EventBus = new Vue() 1.4 在@/layout/layout.vue中 定义receiveMessage方法 并且触发消息总线方法 // 定义receiveMessage方法 广播消息内容 receiveMessage(messageData) { // 触发消息总线方法 this.$EventBus.$emit('receiveMessage', messageData) } 1.5 在需要接收消息的页面的created钩子中添加监听方法 this.$EventBus.$on('receiveMessage', (messageData) => { // 接收到的消息 console.log(messageData) // 获取消息内容 this.message = messageData.message }) 2、H5端发送消息到APP端 2.1 在index.html中 给window挂载sendMessageApp方法 window.sendMessageApp = (action, message) => { const messageData = { action:action, message:message, timestamp:new Date().getTime() } document.title = JSON.stringify(messageData) } 2.2 页面使用 window.sendMessageApp('showMessage', this.curMessage)
四、效果
2021.04.24更新
非常不凑巧,根据实际情况来看,产品既需要兼容APP也需要支持windows版本,也就是APP和浏览器访问都需要支持,所以就推翻了原来的设计。在原来的设计中,webSocket服务端主要是和APP端进行交互,再由APP和H5端进行交互,主要的逻辑集中在APP中。如果需要支持windows版本,则需要将主要逻辑迁移到H5端,由H5端与webSocket进行交互。这样一来,只需要在H5端设计一个统一的入口页面,后续的交互逻辑都在这个页面中进行处理。APP端则仅需要配置一个H5端入口页面的地址即可。
H5端与webSocket交互核心内容如下(相当于套用之前的模板,只是将其中的uni方法替换掉了):
import { fixedMessageEnum } from './systemConfig' import * as common from './common' import store from '@/store' let __assign = (this && this.__assign) || function() { __assign = Object.assign || function(t) { for (let s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i] for (const p in s) { if (Object.prototype.hasOwnProperty.call(s, p)) { t[p] = s[p] } } } return t } return __assign.apply(this, arguments) } let heartTimer = null // 心跳句柄 let reConnectTimer = null // 重连句柄 let isClose = true let config = null let socketTask = null let connectOK = false // 连接是否成功 export default class WebSocketHandler { constructor(param) { config = __assign({ isReconnect: true, // 是否断线重连 isHeartData: true, // 是否开启心跳 heartTime: 5000, // 心跳时间 reConnectTime: 5000, // 重连时间间隔 initConnected: function(data) { // 接收服务器连接反馈消息 } }, param) const _this = this // 初始化 this.initWebSocket = function(success, fail) { const deviceUUID = window.localStorage.getItem('deviceUUID') if (!deviceUUID) { common.getDeviceUUID() } const wsBaseUrl = window.localStorage.getItem('wsBaseUrl') const url = wsBaseUrl + deviceUUID // 创建websocket链接 socketTask = new WebSocket(url) // 监听socket是否打开成功 socketTask.onopen = function(res) { isClose = false connectOK = true if (config.isHeartData) { _this.clearHeart() _this.startHeart() } clearInterval(reConnectTimer) reConnectTimer = null // 获取?后面的参数 const search = window.location.search // 定义显示模式参数 let showModel = '' if (search) { // 按照&拆分参数 const paramArray = search.split('&') // 过滤其中的showModel参数 const showModelParam = paramArray.filter(item => item ? item.indexOf('showModel=') !== -1 : false) // 参数不为空 if (showModelParam && showModelParam.length) { // 获取参数值 showModel = showModelParam[0].split('=')[1] } } // 从缓存中获取显示模式 const localShowModel = window.localStorage.getItem('showModel') // 如果显示模式参数为空 则取缓存参数 showModel = showModel || localShowModel // 如果显示模式 还是空的 if (!showModel) { // 则默认设置为windows模式 showModel = 'windows' } // 将显示模式添加到缓存中 window.localStorage.setItem('showModel', showModel) // 路由跳转页面 window.HMWS_VUE.$router.push({ path: '/noContent/noContent?showModel=' + showModel }) } // 监听socket关闭 socketTask.onclose = function() { connectOK = false if (config.isHeartData && heartTimer != null) { _this.clearHeart() } // 判断是否为异常关闭 if (reConnectTimer == null && !isClose && config.isReconnect) { // 执行重连操作 _this.reConnectSocket() } } // 监听到错误异常 socketTask.onerror = function() { // websocket连接异常 connectOK = false if (config.isHeartData && heartTimer != null) { _this.clearHeart() } if (reConnectTimer == null && config.isReconnect) { // 执行重连操作 _this.reConnectSocket() } } // 接收到消息 socketTask.onmessage = function(data) { const message = JSON.parse(data.data) if (message instanceof Object) { // 写具体的业务操作 _this.objectMessageHandler(_this, message) // 必须 } else if (!isNaN(message)) { // 是数字 // 固定格式消息处理 _this.fixedMessageHandler(message) } else { console.log('非法数据,无法解析') } } } this.initWebSocket() this.initComplete = function() {} // 心跳 this.startHeart = function() { heartTimer = setInterval(function() { // 发送心跳 socketTask.send(fixedMessageEnum['ping'].toString()) }, config.heartTime) } // 清除心跳 this.clearHeart = function() { clearInterval(heartTimer) heartTimer = null } this.refreshWebSocket = function(token) { config.token = token _this.initWebSocket() } // 重连 this.reConnectSocket = function() { // 网络断开时,不需要再次去重连 if (store.getters.netWorkStatus) { reConnectTimer = setInterval(function() { if (!connectOK) { _this.initWebSocket() } else { clearInterval(reConnectTimer) } }, config.reConnectTime) } else { clearInterval(reConnectTimer) } } // 全局固定格式消息处理 this.fixedMessageHandler = function(message) { switch (message) { case fixedMessageEnum['pong']: break case fixedMessageEnum['exit']: break case fixedMessageEnum['update']: break case fixedMessageEnum['asking_exit']: // 接受到重复登陆指令,服务器询问是否踢人 config.initConnected(message) // 确认 break case fixedMessageEnum['connect_complete']: config.initConnected(message) } } // 业务消息处理 this.bizMessageHandler = function() { } // 对象消息处理 this.objectMessageHandler = function(socket, messageData) { // 如果接收到了服务加载完成的指令 if (messageData.action === 'serverLoaded') { // console.log('开始获取H5端地址') // 调用消息发送方法获取H5Ur store.getters.webSocketHandler.sendMessage('getH5Url', '') } // 修改设备状态 if (messageData.action === 'changeDeviceStatus') { // 触发 修改设备状态方法 this.$EventBus.$emit('changeDeviceStatus', messageData.message) } } } // 发送消息 sendMessage(action, message) { // 创建消息对象 const messageData = { action: action, message: message, timestamp: new Date().getTime() } return new Promise(function(resolve, reject) { try { socketTask.send(JSON.stringify(messageData)) return resolve() } catch (e) { return reject(e) } }) } // 关闭 close() { isClose = true if (config.isHeartData) { this.clearHeart() } // 关闭socket socketTask.close() } /** * 是否连接成功 */ isConnection() { return !isClose } }
发表评论