En primer lugar, si el proyecto necesita interactuar con el cliente y el servidor durante mucho tiempo, sin tiempo y sin interrupción, utilizar la conexión WebSocket es la mejor opción, como el proyecto de intercambio.
Entonces, ¿qué funciones se necesitan para crear un WebSocket y cómo puede ser un sistema ws robusto?
Primero necesitamos varias funciones básicas:
1. onopen (devolución de llamada exitosa de la conexión ws)
2. onmessage (ws devuelve la devolución de llamada de datos)
3. onclose (ws cierre de devolución de llamada)
4. onerror (devolución de llamada de error ws)
Cuando nuestro ws tiene estos métodos, necesitamos agregar algunos métodos para interactuar con el servidor
1. suscríbete
2. darse de baja (darse de baja)
3. solicitud (enviar datos al servidor)
Con estos métodos, necesitamos considerar algunos puntos anormales
1. reconectar (reconexión de ws, cuando ws es anormal y está desconectado, reconectar ws)
2. reSubscribe (resuscribe, cuando ws se vuelve a conectar, vuelve a suscribir los eventos suscritos previamente)
3. Heartbeat (ws heartbeat, siempre debemos verificar si el servidor aún está vivo)
4. pollingRollback (devolución de llamada de respaldo de sondeo, cuando ws está desconectado, los datos aún deben actualizarse continuamente)
Ahora que sabemos esto, necesitamos crear algunos métodos para implementarlo. No hablaré sobre la lógica de la implementación específica. Puedes mirar directamente el código.
Primero, creamos una carpeta de socket y creamos tres archivos como este, para que podamos mantenerlo fácilmente
índice.js
/**
* websocket
*/
import Heartbeat from './heartbeat'
import PollingRollback from './pollingRollback'
export default class Socket {
constructor(url) {
this.ws = null
this.url = url
this.subscriptionMap = {}
this.pollingRollback = null
this.createPollingCallback() // 创建轮询
this.start()
}
start() {
if (!this.url) return console.error('url is required')
this.ws = new WebSocket(this.url + "?lang=" + window.localStorage.lang);
this.ws.addEventListener("open", this.onOpen);
this.ws.addEventListener("message", this.onMessage);
this.ws.addEventListener("close", this.onClose);
this.ws.addEventListener("error", this.onError);
}
request(payload) { // 单纯地给服务器发送数据
if (this.isConnected()) {
this.ws.send(JSON.stringify({ ...payload, event: 'req' }));
}
}
subscribe({ payload, rollback, callback }, isReSubscribe) {
if (!isReSubscribe && this.subscriptionMap[payload.id]) return
this.subscriptionMap[payload.id] = { payload, rollback, callback }
this.pollingRollback.set(payload.id, rollback)
if (this.isConnected()) {
this.ws.send(JSON.stringify({ ...payload, event: 'sub' }));
}
}
unSubscribe(id) {
if (!id) return
if (this.isConnected()) {
if (this.subscriptionMap[id]) {
const payload = this.subscriptionMap[id].payload
this.ws.send(JSON.stringify({ ...payload, event: 'cancel' }));
this.pollingRollback.remove(id)
delete this.subscriptionMap[id];
}
}
}
isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN
}
onOpen = () => {
clearInterval(this.reConnectTimer)
this.createHeartbeat() // 创建 socket 心脏
this.reSubscribe() // 重新订阅已有的sub
this.pollingRollback.close() // ws 连接之后,关闭轮询
}
onMessage = (result) => {
const data = result.data
if (/ping|pong/i.test(data)) return
const normalizedData = JSON.parse(data || "{}");
this.handleCallback(normalizedData)
}
handleCallback = (data) => {
const id = data.id;
if (!id) return;
if (this.subscriptionMap[id]) {
this.subscriptionMap[id]["callback"] && this.subscriptionMap[id]["callback"](data);
}
}
onClose = () => {
console.warn(`【Websocket is closed】`)
this.ws.removeEventListener("open", this.onOpen);
this.ws.removeEventListener("message", this.onMessage);
this.ws.removeEventListener("close", this.onClose);
this.ws.removeEventListener("error", this.onError);
this.ws = null;
}
onError = (error) => {
if (error && error.message) {
console.error(`【Websocket error】 ${error.message}`)
}
this.ws.close()
this.reConnect()
}
reConnect() { // 开启重连
this.pollingRollback.open() // ws连接之前,开启轮询
if (this.reConnectTimer) return
this.reConnectTimer = setInterval(() => {
this.start()
}, 3000)
}
reSubscribe() {
Object.values(this.subscriptionMap).forEach(subscription => this.subscribe(subscription, true))
}
createHeartbeat() {
this.heartbeat = new Heartbeat(this.ws)
this.heartbeat.addEventListener('die', () => {
this.ws.close()
this.ws.reConnect()
})
}
createPollingCallback() {
this.pollingRollback = new PollingRollback()
}
}
latido del corazón.js
/**
* 心跳
*/
const INTERVAL = 5000 // ping 的间隔
const TIMEOUT = INTERVAL * 2 // 超时时间(只能是INTERVAL的整数倍数,超过这个时间会触发心跳死亡事件) 默认为 ping 两次没有响应则超时
const DIE_EVENT = new CustomEvent("die") // 心跳死亡事件 => 超时时触发
export default class Heartbeat extends EventTarget {
constructor(ws, interval, timeout) {
super()
if (!ws) return
this.ws = ws
this.interval = interval || INTERVAL
this.timeout = timeout || TIMEOUT
this.counter = 0
this.ws.addEventListener("message", this.onMessage)
this.ws.addEventListener("close", this.onClose)
this.start()
}
ping() {
this.counter += 1
if (this.counter > (this.timeout / this.interval)) { // ping 没有响应 pong
this.dispatchEvent(DIE_EVENT)
return
}
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
const data = JSON.stringify({ ping: new Date().getTime() })
this.ws.send(data);
}
}
pong(data) {
this.ws.send(data.replace("ping", "pong"));
}
onMessage = (result) => {
const data = result.data;
if (/pong/i.test(data)) { // 服务器响应重新计数
return this.counter = 0
}
if (/ping/.test(data)) { // 服务器 ping 我们
return this.pong(data)
}
}
onClose = () => {
this.ws.removeEventListener("message", this.onMessage);
this.ws.removeEventListener("close", this.onClose);
this.ws = null;
clearInterval(this.keepAliveTimer)
}
start() {
this.keepAliveTimer = setInterval(this.ping, this.interval)
}
}
encuestaRollback.js
/**
* 轮询
*/
export default class PollingRollback {
constructor(interval) {
this.rollbackMap = {}
this.rollbackTimer = null
this.interval = interval || 3000
}
set(id, rollback) {
this.rollbackMap[id] = rollback
}
remove(id) {
delete this.rollbackMap[id]
}
open() {
this.rollbackTimer = setInterval(() => {
Object.values(this.rollbackMap).forEach(rollback => rollback && rollback())
}, this.interval)
}
close() {
clearInterval(this.rollbackTimer)
}
}
De esta manera, se encapsula un Socket completo, entonces, ¿cómo debemos usarlo?
Podemos crear un archivo ws.js y encapsular el método que necesitamos en una clase nuevamente.
En cuanto a algunas personas que preguntan por qué necesitamos encapsular una clase sobre la base original, porque el método expuesto debe ser breve y claro, y la clase original También se puede usar directamente.Si solo dos personas lo desarrollan, ¡no será fácil para la otra persona comenzar!
ws.js
import Socket from './socket'
class CommonWs {
constructor(url) {
this.ws = null
this.url = url
}
connect() {
this.ws = new Socket(this.url)
}
request(payload) {
if (!this.ws) this.connect()
this.ws.request(payload)
}
subscribe(payload, rollback, callback) {
if (!this.ws) this.connect()
this.ws.subscribe({ payload, rollback, callback })
}
unSubscribe(id) {
if (!this.ws) this.connect()
this.ws.unSubscribe(id)
}
close() {
if (!this.ws) return
this.ws.close()
}
isConnected() {
return this.ws && this.ws.isConnected()
}
}
// ws
export const ws = new CommonWs(<wsUrl>)
// ws2
export const ws2 = new CommonWs(<ws2Url>)
Bien, en este punto puede crear ws directamente para usar métodos como suscripción, cancelación de suscripción, cierre de conexión ws, etc. En cuanto a las cosas detrás del Socket, no necesitamos saberlo, siempre que nuestro código esté libre de errores. .
Aquí agrego el significado del parámetro subscribe
* carga útil Contenido requerido de suscripción, por ejemplo, si queremos suscribirnos a la información del saldo de la billetera, el fondo necesita que enviemos una identificación, luego pasaremos {id: 'saldo'} y similares
* retroceder Durante la desconexión anormal de la conexión ws, los datos en la página aún deben actualizarse, por lo que usamos la función de retroceder para sondear y actualizar los datos
* devolución de llamada Como sugiere el nombre, se llamará al método correspondiente después de que ws devuelva los datos