Wenn das Projekt über einen längeren Zeitraum und ohne Zeit und Unterbrechung mit dem Client und dem Server interagieren muss, ist die Verwendung einer WebSocket-Verbindung die beste Wahl, z. B. ein Austauschprojekt
Welche Funktionen sind also erforderlich, um einen WebSocket zu erstellen, und wie kann daraus ein robustes WS-System werden?
Zunächst benötigen wir einige Grundfunktionen:
1. onopen (WS-Verbindung erfolgreicher Rückruf)
2. onmessage (ws gibt Datenrückruf zurück)
3. onclose (WS-Close-Rückruf)
4. onerror (WS-Fehlerrückruf)
Wenn unser WS über diese Methoden verfügt, müssen wir einige Methoden hinzufügen, um mit dem Server zu interagieren
1. Abonnieren
2. unSubscibe (abmelden)
3. Anfrage (Daten an den Server senden)
Bei diesen Methoden müssen wir einige ungewöhnliche Punkte berücksichtigen
1. reConnect (WS-Wiederverbindung, wenn WS abnormal ist und die Verbindung getrennt ist, WS erneut verbinden)
2. reSubscribe (erneut abonnieren, wenn ws wieder eine Verbindung herstellt, abonnieren Sie die zuvor abonnierten Ereignisse erneut)
3. Heartbeat (WS-Heartbeat, wir müssen immer prüfen, ob der Server noch aktiv ist)
4. pollingRollback (Abfrage-Backup-Rückruf, wenn ws getrennt wird, müssen die Daten weiterhin kontinuierlich aktualisiert werden)
Da wir das nun wissen, müssen wir einige Methoden erstellen, um es zu implementieren. Ich werde nicht auf die Logik der spezifischen Implementierung eingehen. Sie können sich den Code direkt ansehen.
Zuerst erstellen wir einen Socket-Ordner und drei Dateien wie diese, damit wir ihn einfach verwalten können
index.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()
}
}
heartbeat.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)
}
}
pollingRollback.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)
}
}
Auf diese Weise wird ein vollständiger Socket gekapselt. Wie sollten wir ihn also verwenden?
Wir können eine ws.js-Datei erstellen und die Methode, die wir benötigen, erneut in einer Klasse kapseln.
Einige Leute fragen, warum wir eine Klasse auf der ursprünglichen Basis kapseln müssen, da die offengelegte Methode kurz und klar sein muss und die ursprüngliche Klasse dies auch kann direkt verwendet werden. Wenn nur zwei Personen es entwickeln, wird es für die andere Person nicht einfach sein, damit anzufangen!
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>)
Okay, an dieser Stelle können Sie WS direkt erstellen, um Methoden wie Abonnement, Abmeldung, Schließen der WS-Verbindung usw. zu verwenden. Was die Dinge hinter dem Socket betrifft, müssen wir nichts wissen, solange unser Code fehlerfrei ist .
Hier füge ich die Bedeutung des Subscribe-Parameters hinzu
* Payload-Abonnement erforderlicher Inhalt. Wenn wir beispielsweise Informationen zum Wallet-Kontostand abonnieren möchten, müssen wir im Hintergrund eine ID senden, dann übergeben wir { id: 'balance' } und dergleichen
* Rollback Während der abnormalen Trennung der WS-Verbindung müssen die Daten auf der Seite noch aktualisiert werden, daher verwenden wir die Rollback-Funktion, um die Daten abzufragen und zu aktualisieren
* Rückruf Wie der Name schon sagt, wird die entsprechende Methode aufgerufen, nachdem ws die Daten zurückgegeben hat