最近需要开发一款产品,需要实现前后端双端实时通讯,且为了便于后期更新,前端采用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 storesetting.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
}
}




发表评论