背景:周末有个紧急需求,需要实时监控车辆位置,使用百度地图规划路线,项目已有公共封装好的websocket工具类。每条线路有多辆公交车,每辆公交车均有终端实时向服务器推送位置。
项目技术背景:vue 3 + vite 3.2 + element-plus
于是,吭哧吭哧开始编码,应用代码:
<template>
<div id="bMap" class="map-box"></div>
</template>
<script setup lang="ts">
import { onMounted, watch } from 'vue'
import { configMapboxCenterLngLat, bMapStyleJson } from '@/views/bigScreen/config/setting.js'
import { socket } from '@/utils/common/websocketCreate'
import { apiBSGetRouteLineStationList, apiBSGetRouteLineBuslist } from '../api/bigScreen'
const props = defineProps({
// 公交车列表,父组件传入
busList: {
type: Array,
default: () => {
return []
},
},
routeId: undefined,
})
const bMapObj = window.BMapGL
let bMap = null
let driving = null
const init = () => {
bMap = new bMapObj.Map('bMap')
bMap.centerAndZoom(
new bMapObj.Point(configMapboxCenterLngLat[0], configMapboxCenterLngLat[1]),
16
) // 初始化地图,设置中心点坐标和地图级别
bMap.enableScrollWheelZoom(true) // 开启鼠标滚轮缩放
bMap.setHeading(60)
bMap.setTilt(60)
bMap.setMapStyleV2({ styleJson: bMapStyleJson })
}
// 标注车辆位置
const addBusPos = (pos) => {
const icon = new bMapObj.Icon(
'/images/bigScreen/map-icon/marker-bus.png',
new bMapObj.Size(31, 50)
)
const convertor = new bMapObj.Convertor()
const pt = new bMapObj.Point(pos.longitude, pos.latitude)
convertor.translate([pt], 1, 5, (data) => {
const point = data.points[0]
const marker = new bMapObj.Marker(point, { icon })
const opts = { width: 100, height: 50, title: pos.plateNo }
const infoWin = new bMapObj.InfoWindow('', opts)
let choseMarker = null
marker.id = pos.vin
// 获取所有已经存在的标注,如果有ID相同的标注,则删除之后重新添加
const markeres = bMap.getOverlays()
for (let i = 0; i < markeres.length; i++) {
if (markeres[i].id === pos.vin) {
choseMarker = markeres[i]
}
}
if (choseMarker) {
bMap?.removeOverlay(choseMarker)
}
bMap?.addOverlay(marker)
// 无车牌不展示信息框
if (pos.plateNo) {
marker.addEventListener('click', function () {
bMap.openInfoWindow(infoWin, point) //开启信息窗口
})
}
})
}
watch(
() => props.busList,
(busList) => {
bMap.clearOverlays()
busList.map((item) => {
let socket = socketCreater()
addBusPos(item)
socket.init(`/gb32960/${item.vin}`, item.vin, (message) => {
try {
const data = JSON.parse(message.data)
addBusPos({ ...data.data, plateNo: item.plateNo })
} catch (e) {
console.log('心跳检测')
}
})
})
}
)
</script>
websocket 封装代码
/*
* @Descripttion: 封装socket方法
*/
const socket = {
websocket: null,
connectURL: `${import.meta.env.VITE_SOCEKT_URL}${import.meta.env.VITE_BASE_API}`,
// 开启标识
socket_open: false,
// 心跳timer
hearbeat_timer: null,
// 心跳发送频率
hearbeat_interval: 10000,
// 是否自动重连
is_reonnect: true,
// 重连次数
reconnect_count: 3,
// 已发起重连次数
reconnect_current: 1,
// 网络错误提示此时
ronnect_number: 0,
// 重连timer
reconnect_timer: null,
// 重连频率
reconnect_interval: 5000,
// 设备id
clientId: '',
context: '',
receiveMessage: null,
init: (context = '', clientId = '', receiveMessage = null) => {
if (!('WebSocket' in window)) {
$message.warning('浏览器不支持WebSocket')
return null
}
// 已经创建过连接不再重复创建
// if (socket.websocket) {
// return socket.websocket
// }
socket.clientId = clientId
socket.context = context
socket.receiveMessage = receiveMessage
socket.websocket = new WebSocket(socket.connectURL + context)
socket.websocket.onmessage = (e) => {
if (receiveMessage) {
receiveMessage(e)
}
}
socket.websocket.onclose = () => {
clearInterval(socket.hearbeat_interval)
socket.socket_open = false
// 需要重新连接
if (socket.is_reonnect) {
socket.reconnect_timer = setTimeout(() => {
// 超过重连次数
if (socket.reconnect_current > socket.reconnect_count) {
clearTimeout(socket.reconnect_timer)
socket.is_reonnect = false
return
}
// 记录重连次数
socket.reconnect_current++
socket.reconnect()
}, socket.reconnect_interval)
}
}
// 连接成功
socket.websocket.onopen = function () {
console.log('ws建立链接')
socket.socket_open = true
socket.is_reonnect = true
socket.send(socket.clientId)
// 开启心跳
socket.heartbeat()
}
// 连接发生错误
socket.websocket.onerror = function () {}
},
send: (data, callback = null) => {
// 开启状态直接发送
if (socket.websocket.readyState === socket.websocket.OPEN) {
socket.websocket.send(socket.clientId)
if (callback) {
callback()
}
// 正在开启状态,则等待1s后重新调用
} else {
clearInterval(socket.hearbeat_timer)
socket.ronnect_number++
}
},
receive: (message) => {
let params = JSON.parse(message.data).data
params = JSON.parse(params)
return params
},
heartbeat: () => {
if (socket.hearbeat_timer) {
clearInterval(socket.hearbeat_timer)
}
socket.hearbeat_timer = setInterval(() => {
console.log('心跳ping~')
socket.send(socket.clientId)
}, socket.hearbeat_interval)
},
close: () => {
clearInterval(socket.hearbeat_interval)
socket.is_reonnect = false
if (socket.websocket) {
socket.websocket.close()
}
},
/**
* 重新连接
*/
reconnect: () => {
if (socket.websocket && !socket.is_reonnect) {
socket.close()
}
socket.init(socket.context, socket.clientId, socket.receiveMessage)
},
}
export default socket
问题来了,在使用代码中,只有最后一个websocket有效,百思不得其解。
busList.map((item) => {
let socket = socketCreater()
addBusPos(item)
socket.init(`/gb32960/${item.vin}`, item.vin, (message) => { // 此处代码只有最后一个init的websocket能收取消息
try {
const data = JSON.parse(message.data)
addBusPos({ ...data.data, plateNo: item.plateNo })
} catch (e) {
console.log('心跳检测')
}
})
})
一直以为,是使用方面的问题,尝试了换成for循环,foreach,等其他循环方式来初始化websocket。然而还是没有什么卵用,照样还是只有最后一个websocket能正常收发消息,直到检查到websocket 公共封装才明白问题所在。直接使用export default socket,导出的变量是全局变量,且封装的是Object,在内存中是引用类型,每次执行,生成的websocket对象都会更新,实际上,每次执行init的方法,都实例化了一个websocket链接,但是由于是引用类型,每次都只是更新了引用,传入的匿名函数去处理onmessage事件,都只是处理了最后一次更新的websocket的实例。按照如下封装,将websocket实例用方法包起来,将websocket实例返回给外部引用对象,就解决了这个问题。
/*
* @Descripttion: 封装socket方法--
*/
export default function socketCreater() {
let socket = {
websocket: null,
connectURL: `${import.meta.env.VITE_SOCEKT_URL}${import.meta.env.VITE_BASE_API}`,
// 开启标识
socket_open: false,
// 心跳timer
hearbeat_timer: null,
// 心跳发送频率
hearbeat_interval: 10000,
// 是否自动重连
is_reonnect: true,
// 重连次数
reconnect_count: 3,
// 已发起重连次数
reconnect_current: 1,
// 网络错误提示此时
ronnect_number: 0,
// 重连timer
reconnect_timer: null,
// 重连频率
reconnect_interval: 5000,
// 设备id
clientId: '',
context: '',
receiveMessage: null,
init: (context = '', clientId = '', receiveMessage = null) => {
if (!('WebSocket' in window)) {
$message.warning('浏览器不支持WebSocket')
return null
}
// 已经创建过连接不再重复创建
// if (socket.websocket) {
// return socket.websocket
// }
socket.clientId = clientId
socket.context = context
socket.receiveMessage = receiveMessage
socket.websocket = new WebSocket(socket.connectURL + context)
socket.websocket.onmessage = (e) => {
if (receiveMessage) {
receiveMessage(e)
}
}
socket.websocket.onclose = () => {
clearInterval(socket.hearbeat_interval)
socket.socket_open = false
// 需要重新连接
if (socket.is_reonnect) {
socket.reconnect_timer = setTimeout(() => {
// 超过重连次数
if (socket.reconnect_current > socket.reconnect_count) {
clearTimeout(socket.reconnect_timer)
socket.is_reonnect = false
return
}
// 记录重连次数
socket.reconnect_current++
socket.reconnect()
}, socket.reconnect_interval)
}
}
// 连接成功
socket.websocket.onopen = function () {
console.log('ws建立链接')
socket.socket_open = true
socket.is_reonnect = true
socket.send(socket.clientId)
// 开启心跳
socket.heartbeat()
}
// 连接发生错误
socket.websocket.onerror = function () {}
},
send: (data, callback = null) => {
// 开启状态直接发送
if (socket.websocket.readyState === socket.websocket.OPEN) {
socket.websocket.send(socket.clientId)
if (callback) {
callback()
}
// 正在开启状态,则等待1s后重新调用
} else {
clearInterval(socket.hearbeat_timer)
// if (socket.ronnect_number < 1) {
// ElMessage({
// type: 'error',
// message: i18n.global.t('chat.unopen'),
// duration: 0,
// })
// }
socket.ronnect_number++
}
},
receive: (message) => {
let params = JSON.parse(message.data).data
params = JSON.parse(params)
return params
},
heartbeat: () => {
if (socket.hearbeat_timer) {
clearInterval(socket.hearbeat_timer)
}
socket.hearbeat_timer = setInterval(() => {
console.log('心跳ping~')
socket.send(socket.clientId)
}, socket.hearbeat_interval)
},
close: () => {
clearInterval(socket.hearbeat_interval)
socket.is_reonnect = false
if (socket.websocket) {
socket.websocket.close()
}
},
/**
* 重新连接
*/
reconnect: () => {
if (socket.websocket && !socket.is_reonnect) {
socket.close()
}
socket.init(socket.context, socket.clientId, socket.receiveMessage)
},
}
return socket
}
使用方法
watch(
() => props.busList,
(busList) => {
bMap.clearOverlays()
busList.map((item) => {
let socket = socketCreater()
addBusPos(item)
socket.init(`/gb32960/${item.vin}`, item.vin, (message) => {
try {
const data = JSON.parse(message.data)
addBusPos({ ...data.data, plateNo: item.plateNo })
} catch (e) {
console.log('心跳检测')
}
})
})
}
)