背景
解决http协议通讯延迟的问题,后端数据有变化无法主动向前端发送数据,实现多个客户端数据的实时联动响应。该项目使用技术为 vue + echarts + nodejs
知识点
WebSocket是一种协议,设计用于提供低延迟、全双工和长期运行的连接
-
全双工:是一种通信方式,通信的两个参与方可以同时发送和接收数据,不需要等待对方的响应或传输完成
-
实时通信:即时消息传递、音视频通话、在线会议和实时数据传输等,可以实现即时的数据传输和交流,不需要用户主动请求或刷新来获取更新数据
之前解决通讯延时的方法:
-
轮询:客户端定期向服务器发送请求
-
长轮询:客户端发出请求后,保持连接打开,等待新数据响应后再关闭连接
-
Comet:保持长连接,在返回请求后继续保持连接打开,基于HTTP的模型
-
缺点:造成不必要的网络开销和延迟
基本使用
后端
-
安装 :npm i ws -S
- 使用:
// 创建对象 const WebSocket = require('ws') const wss = new WebSocket({ port: 9998 }) // 监听连接事件 wss.on('connection',client => { console.log('有客户端连接成功') // 监听接收数据事件 wss.on('message',msg => { console.log('客户端发来了数据' + msg) }) // 发送数据 client.send('hello socket from backend') })
前端
// WebSocket 是 window 对象提供的,不需要安装额外的包
const ws = new WebSocket('ws://localhost:9998')
// 连接成功事件
ws.onopen = () => {
console.log('连接服务端成功了....')
}
// 接收数据事件
ws.onmessage = msg => {
console.log('接收到服务端发送过来的数据了'+msg.data)
}
// 关闭连接事件
ws.onclose = () => {
console.log('连接服务器失败')
}
// 发送数据
send.onclick = function() {
ws.send('hello socket from frontend')
}
项目实战
基于已完成的之前使用http请求的 echart 项目修改
后端
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 9998 })
const path = require('path')
const fileUtil = require('../utils/file_util')
module.exports.listen = () => {
wss.on('connection', (client) => {
console.log('客户端连接成功')
client.on('message', async (msg) => {
const payload = JSON.parse(msg)
const action = payload.action
if (action === 'getData') {
let file_path = '../data/' + payload.chartName + '.json'
file_path = path.join(__dirname, file_path)
const ret = await fileUtil.getFileJsonData(file_path)
// 将前端需要的数据存储在 data 字段下,和前端传入的数据一起再以字符串的形式返回给前端
payload.data = ret
client.send(JSON.stringify(payload))
} else {
// 如果不需要读取数据,则直接将前端传入的字符串数据传给其他所有客户端
wss.clients.send(msg)
}
})
})
}
// app.js
const WebSocket = require('./service/web_socket_service')
WebSocket.listen()
前端
export default class SocketService {
static instance = null
static get Instance() {
if (!this.instance) {
// 这里一定要 return 返回
return (this.instance = new SocketService())
} else {
return this.instance
}
}
callBackMapping = {}
ws = null
connect() {
if (!window.WebSocket) {
return console.log('您的浏览器不支持WebSocket')
}
this.ws = new WebSocket('ws://localhost:9998')
this.ws.onopen = () => {
console.log('服务器连接成功')
}
this.ws.onclose = () => {
console.log('服务器关闭')
}
this.ws.onmessage = (msg) => {
// msg 返回的是一个消息事件,里面的 data 才是需要的数据,然后通过 JSON.parse 将字符串解析为js对象
const ret = JSON.parse(msg.data)
console.log(ret.data)
// 实际需要的数据
const realData = ret.data
if (ret.action === 'getData') {
if (this.callBackMapping[ret.socketType]) {
// 根据socketType调用对应组件注册的函数,将返回的数据作为参数传入
this.callBackMapping[ret.socketType].call(this, realData)
} else {
console.log('没有找到函数')
}
} else if (ret.action === 'fullScreen') {
// console.log(ret)
} else if (ret.action === 'themeChange') {
// console.log(ret)
}
}
}
registerCallBack(socketType, callback) {
this.callBackMapping[socketType] = callback
}
unRegisterCallBack(socketType) {
this.callBackMapping[socketType] = null
}
send(data) {
this.ws.send(JSON.stringify(data))
}
}
笔记思路
-
创建 utils/socket_service.js 文件,里面默认导出 SocketService 类
-
在类里面用单例模式,使项目中只有一个 socketservice 实例对象
原理是调用静态成员,判断当前是否有实例对象,如果没有则创建一个,如果有则返回已有的实例对象
-
里面有4个方法,和后端连接的 connect 方法,组件用来注册回调函数的 registerCallBack 方法,组件销毁的时候注销回调函数的 unRegisterCallBack 方法,组件向服务器发送数据的 send 方法
-
connect 方法,首先判断浏览器是否支持 WebSocket,然后创建 ws 对象,监听开启、数据传输和服务器关闭的事件,由于其他方法会调用 ws 对象的方法,所有将 ws 提前声明
-
registerCallBack 方法,需要传入2个参数,用来标注是哪个组件的回调函数的 socketType 和 回调函数 callback,提前声明存储所有回调函数的对象 callBackMapping
-
unRegisterCallBack 方法,传入1个参数,需要清除的回调函数的标注 socketType
-
send 方法,传入1个参数,执行向服务器传送数据的行为
-
-
绑定原型,为了方便组件使用注册函数和发送数据,将 WebSocket 实例对象 $socket 绑定到 vue 的原型上
-
组件改造,在 created 周期里将当前组件的获取数据后执行的函数注册在 SocketService 类里的 callBackMapping 属性上;在 mounted 周期里,执行 $socket 里的发送数据到服务器;将获取数据后执行的函数修改为,接收参数 res,通过 JSON.stringify 将 res 解析为 js 对象后使用;destroyed 周期里,注销存储在 SocketService 类里的执行函数
-
执行逻辑:SocketService 作为一个和后端连接的中转站,开启和后端的连接;项目中所有需要和后端有交互的,都需要先将拿到数据后的执行函数,注册在这个中转站里面;中转站将自己的所有方法都放在一个公共区域,所有需要发送数据到后端的组件,都可以从公共区域(即绑定在Vue原型上的$socket)调用发送数据的方法;组件销毁后要注销放在中转站里面的执行函数;中转站通过监听后端开启、发送数据、关闭来执行操作;后端传输数据首先被中转站接收,中转站对接收的数据字段与存储的执行函数进行比对后调用