node+vue3的websocket前后端消息推送
前期写web项目时,前端获取数据的方式一般是向后端发起数据请求,然后后端向前端发送数据,然后对数据进行渲染,这是最常规的一种数据通讯方式,适用于绝大部分前后端分离的项目
实际上还有一种场景,需要后端向前端主动发送数据,然后前端更新视图,这种一般出现在数据大屏显示项目中。后端每间隔一段时间向前端发送数据,然后驱动前端更新页面,使用的技术是websocket,研究了一段时间,算搞明白是怎么回事了
先看看最终前端的展示效果
一、后端使用websocket
这里我用的后端是node,服务框架用的是express
首先安装express和ws,我的安装包信息如下:
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.19.2",
"ws": "^8.16.0"
}
}
1、websocket服务
新建service/websocket_service.js文件,代码如下
const WebSocket = require("ws");
const { getProvinceData, getBarData } = require("../utils/getData");
const wss = new WebSocket.Server({ port: 9998 });
module.exports.listen = () => {
wss.on("connection", (ws, req) => {
console.log("WebSocket client connected");
ws.on("close", () => {
console.log("WebSocket client disconnected");
});
ws.on("message", (data) => {
console.log(`Received message from client: ${data}`);
if (data == "map") {
ws.send(
JSON.stringify({
data: getProvinceData(),
})
);
}
if (data == "bar") {
ws.send(
JSON.stringify({
data: getBarData(),
})
);
}
});
});
};
这个文件是websocket的关键,后面会在后端入口文件app.js中引用。websocket连接成功后,会监听连接事件、关闭事件、接收消息事件,接收消息事件中,根据前端发送过来的字段,向前端推送不同的数据。正常情况,这里应该是从数据库取数据,而非随机数据,数据库取数据的逻辑写过无数遍,换一下代码就行了。
此外,还引用了一个随机获取数据的文件getData.js,
代码如下:
const province = [
{ name: "山东", value: 2706 },
{ name: "江苏", value: 2321 },
{ name: "广东", value: 2108 },
{ name: "河南", value: 2081 },
{ name: "四川", value: 1878 },
{ name: "湖南", value: 1760 },
{ name: "河北", value: 1659 },
{ name: "湖北", value: 1641 },
{ name: "安徽", value: 1411 },
{ name: "江西", value: 1340 },
{ name: "云南", value: 1195 },
{ name: "广西", value: 1189 },
{ name: "贵州", value: 1135 },
{ name: "辽宁", value: 1068 },
{ name: "浙江", value: 1030 },
{
name: "陕西",
value: 727,
},
{
name: "山西",
value: 720,
},
{
name: "福建",
value: 714,
},
{
name: "重庆",
value: 589,
},
{
name: "甘肃",
value: 522,
},
{
name: "内蒙古",
value: 485,
},
{
name: "吉林",
value: 458,
},
{
name: "黑龙江",
value: 452,
},
{
name: "新疆",
value: 445,
},
{
name: "上海",
value: 267,
},
{
name: "海南",
value: 216,
},
{
name: "北京",
value: 213,
},
{
name: "天津",
value: 204,
},
{
name: "宁夏",
value: 171,
},
{
name: "青海",
value: 121,
},
{ name: "西藏", value: 85 },
];
exports.getProvinceData = () => {
const data = [];
province.forEach((item) => {
data.push({
name: item.name,
value: Math.floor(Math.random() * (1000 - 100 + 1)) + 100,
});
});
return data
};
exports.getBarData = () => {
const data = []
for (let i = 0; i < 7; i++){
data.push(Math.floor(Math.random() * (1000 - 100 + 1)) + 100)
}
return data
}
2、项目入口
也就是app.js,代码如下
const express = require("express");
const WebSocketService = require("./service/websocket_service");
const app = express();
app.listen(3019, () => {
console.log("Server is running on port 3087");
});
// 开启服务端监听
// 当客户端连接后,就会监听
WebSocketService.listen()
除了常规启动express服务外,还要引入上面写好的websocket文件,并调用方法监听
后端就这些了
二、前端接收数据并展示
我用vite创建了一个基础的vue3项目,在App.vue组件上写大屏的代码
App.vue
<script setup>
import ChinaMap from '@/components/ChinaMap.vue'
import BarChart from '@/components/BarChart.vue'
</script>
<template>
<div class="charts">
<div class="chart first">
<BarChart />
</div>
<div class="chart second">
<ChinaMap />
</div>
<div class="chart third">
<BarChart />
</div>
<div class="chart fourth">
<BarChart />
</div>
<div class="chart fifth">
<BarChart />
</div>
<div class="chart sixth">
<BarChart />
</div>
<div class="chart seventh">
<BarChart />
</div>
<div class="chart eighth">
<BarChart />
</div>
<div class="chart ninth">
<BarChart />
</div>
</div>
</template>
<style lang="scss" scoped>
.charts {
height: 100vh;
width: 100%;
display: grid;
gap: 10px;
padding: 10px;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr);
.chart {
box-shadow: rgba(6, 24, 44, 0.4) 0px 0px 0px 2px, rgba(6, 24, 44, 0.65) 0px 4px 6px -1px, rgba(255, 255, 255, 0.08) 0px 1px 0px inset;
}
.first {
grid-row: 1/2;
grid-column: 1/2;
}
.second {
grid-row: 1/3;
grid-column: 2/4;
}
.third {
grid-row: 1/2;
grid-column: 4/5;
}
.fourth {
grid-row: 2/3;
grid-column: 1/2;
}
.fifth {
grid-row: 2/3;
grid-column: 4/5;
}
.sixth {
grid-row: 3/4;
grid-column: 1/2;
}
.seventh {
grid-row: 3/4;
grid-column: 2/3;
}
.eighth {
grid-row: 3/4;
grid-column: 3/4;
}
.ninth {
grid-row: 3/4;
grid-column: 4/5;
}
}
</style>
使用了grid布局,有个点需要注意一下
写外面包裹的容器时,一定要算好横向和竖向的子容器个数,也就是要写下面两行代码
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr);
表明行3行4列,不写的话,容器的宽高一直会随意变化,grid布局参考我的另一篇博客
根组件中还引入了俩画图的组件,这里我直接上代码了
/components/ChinaMap.vue
<template>
<div class="chart" ref="chartRef">
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from "echarts";
import "@/assets/json/china";
let ws = new WebSocket('ws://localhost:9998');
const chartData = ref([
{ name: "山东", value: 2706 },
])
let interval = null
const handleOpen = () => {
console.log('WebSocket connection established');
interval = setInterval(sendMessage, 5000)
}
const handleClose = () => {
console.log('WebSocket connection closed');
reLink()
}
const handleMessage = (event) => {
const { data } = JSON.parse(event.data);
chartData.value = data
drawChart()
}
const handleError = () => {
console.log('WebSocket connection error');
}
ws.addEventListener('open', handleOpen)
ws.addEventListener('close', handleClose)
ws.addEventListener('message', handleMessage)
ws.addEventListener('error', handleError)
// 重连
let timer = null
const reLink = () => {
console.log('connecting failed, preparing relink...')
const relinkTime = 1000
timer = setInterval(() => {
console.log('relinking....')
ws = new WebSocket('ws://localhost:9998')
if (ws.readyState === 0) {
clearInterval(timer)
timer = null
ws.addEventListener('open', handleOpen)
ws.addEventListener('close', handleClose)
ws.addEventListener('message', handleMessage)
ws.addEventListener('error', handleError)
} else {
relinkTime = relinkTime + 5000
}
}, relinkTime)
}
const sendMessage = () => {
ws.send('map')
}
const chartRef = ref(null)
let initChart
const drawChart = () => {
if (initChart != null && initChart != "" && initChart != undefined) {
initChart.dispose(); //销毁
}
initChart = echarts.init(chartRef.value)
initChart.setOption({
backgroundColor: "transparent", // 设置背景色透明
// 必须设置
tooltip: {
show: false,
},
visualMap: {
text: ["", ""],
showLabel: true,
left: "100",
bottom: "100",
min: 0,
max: 1000,
inRange: {
color: ["#edfbfb", "#b7d6f3", "#40a9ed", "#3598c1", "#215096"],
},
// splitNumber: 5,
seriesIndex: "0",
},
series: [
// 地图配置
{
name: "数值",
type: "map",
map: "china",
zoom: 1,
tooltip: {
show: true,
formatter: function (param) {
return param.data.name + ": " + param.data.value
},
},
label: {
show: true, // 显示省份名称
color: "#000",
align: "center",
},
top: "10%",
left: "center",
aspectScale: 0.75,
roam: true, // 地图缩放和平移
itemStyle: {
borderColor: "#3ad6ff", // 省分界线颜色 阴影效果的
borderWidth: 1,
areaColor: "#F5F5F5",
opacity: 1,
},
// 去除选中状态
select: {
disabled: true,
},
// 控制鼠标悬浮上去的效果
emphasis: {
// 聚焦后颜色
disabled: false, // 开启高亮
label: {
align: "center",
color: "#ffffff",
},
itemStyle: {
color: "#ffffff",
areaColor: "#0075f4", // 阴影效果 鼠标移动上去的颜色
},
},
z: 2,
data: chartData.value,
},
],
})
}
onMounted(() => {
setTimeout(() => {
drawChart()
}, 200)
});
onUnmounted(() => {
ws.close();
clearInterval(interval)
});
</script>
<style lang="scss" scoped>
</style>
画图的代码不解释了,主要是websocket的使用
现在WebSocket已经是window的内置类了,所以不需要安装啥,直接上来生成示例就行了
websocket需要监听open/close/message/error事件,核心的就是message事件了,此外,还做了断开后重连的操作,逻辑是设置定时器重新连接服务端
注意这里的message事件是指接收服务端传递过来的message事件
**有一点需要注意,websocket只能传递字符串类型数据,不能传递对象,**所以后端在使用ws.send时,需要使用JSON.stringify转换格式,前端在接收时也需要使用JSON.parse解码
BarChart.vue中的代码
<template>
<div class="chart" ref="chartRef">
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from "echarts";
const chartData = ref([120, 200, 150, 80, 70, 110, 130])
let ws = new WebSocket('ws://localhost:9998');
let interval = null
const handleOpen = () => {
console.log('WebSocket connection established');
interval = setInterval(sendMessage, 5000)
}
const handleClose = () => {
console.log('WebSocket connection closed');
reLink()
}
const handleMessage = (event) => {
const { data } = JSON.parse(event.data);
// console.log(data)
chartData.value = data
drawChart()
}
const handleError = () => {
console.log('WebSocket connection error');
}
ws.addEventListener('open', handleOpen)
ws.addEventListener('close', handleClose)
ws.addEventListener('message', handleMessage)
ws.addEventListener('error', handleError)
// 重连
let timer = null
const reLink = () => {
console.log('connecting failed, preparing relink...')
const relinkTime = 1000
timer = setInterval(() => {
console.log('relinking....')
ws = new WebSocket('ws://localhost:9998')
if (ws.readyState === 0) {
clearInterval(timer)
timer = null
ws.addEventListener('open', handleOpen)
ws.addEventListener('close', handleClose)
ws.addEventListener('message', handleMessage)
ws.addEventListener('error', handleError)
} else {
relinkTime = relinkTime + 5000
}
}, relinkTime)
}
const sendMessage = () => {
ws.send('bar')
}
const chartRef = ref(null)
let initChart
const drawChart = () => {
if (initChart != null && initChart != "" && initChart != undefined) {
initChart.dispose(); //销毁
}
initChart = echarts.init(chartRef.value)
initChart.setOption({
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: chartData.value,
type: 'bar'
}
]
})
}
onMounted(() => {
setTimeout(() => {
drawChart()
}, 200)
});
onUnmounted(() => {
ws.close();
clearInterval(interval)
});
</script>
<style lang="scss" scoped></style>
逻辑差不多,不再解释了,最好的做法是把重复的代码放到hooks中去