websocket服务端与uni-app客户端、H5网页端三端交互

原创  郑建华   2021-03-27   254人阅读  2 条评论

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

image.png

    3、H5端,使用vue搭建项目

image.png

    本项目采用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)

四、效果

image.png

image.png


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
  }
}


本文地址:https://www.zjh336.cn/?id=2030
版权声明:本文为原创文章,版权归 郑建华 所有,欢迎分享本文,转载请保留出处!

发表评论


表情

 评论列表

  1. erhn
    erhn 【实习生】  @回复

    京东专用快递网站 快递单号 空包代发www.5adanhao.cn

  2. 招投标
    招投标 【助理】  @回复

    文章写的很棒,赞一个