背景
- 产品通信方式改为
websocket
- 后端使用原生写
- 目前业务只接收信息
拿到需求的时候马上想到了socket.io
库,因此没想太多就测试使用了,但是测试发现一直连接失败,debugger一天一直没解决,后来才从socket.io
文档上发现了问题,害,以后得好好看文档。
根据socket.io
推荐的客户端封装robust-websocket已经好久没有更新了,同时该库使用ES5
写法,不符合当前要求,因此决定自己封装一个。
封装
目前的需求其实就是前端接收信息即可,因此就做了简单的封装,以下是封装时考虑到的几点:
- 实现发布订阅功能
- 支持单例
- 连接时存在超时时间
- 支持断开重连并且支持配置重连次数
其中部分API参考了socket.io
基础模板
询问chatGPT
得到了初始模板
发布订阅功能
根据需求点一步步完成
interface ClientOptions {
autoConnect: boolean;
protocols?: string[];
}
type EventFunc = (event: any) => void;
const enum WebSocketEventEnum {
open = 'open',
close = 'close',
error = 'error',
message = 'message'
}
const getDefaultOptions = (): ClientOptions => ({
autoConnect: true
});
const getEmptyEventsMap = (): Record<WebSocketEventEnum, EventFunc[]> => ({
[WebSocketEventEnum.open]: [],
[WebSocketEventEnum.close]: [],
[WebSocketEventEnum.error]: [],
[WebSocketEventEnum.message]: [],
});
class WebsocketClient {
private url: string;
private options: ClientOptions;
private websocket: WebSocket | null;
private events = getEmptyEventsMap();
constructor(url: string, options: Partial<ClientOptions>) {
this.url = url;
this.options = {
...getDefaultOptions(),
...options
};
if (this.options.autoConnect) {
this.connect();
}
}
public connect() {
this.websocket = new WebSocket(this.url, this.options.protocols);
this.websocket.onopen = (event) => {
this.emit(WebSocketEventEnum.open, event);
}
this.websocket.onmessage = (event) => {
this.emit(WebSocketEventEnum.message, event);
}
this.websocket.onerror = (event) => {
this.emit(WebSocketEventEnum.error, event);
}
this.websocket.onclose = (event) => {
this.emit(WebSocketEventEnum.close, event);
}
}
public on(name: WebSocketEventEnum, listener: EventFunc) {
this.events[name].push(listener);
}
public off(name?: WebSocketEventEnum, listener?: EventFunc) {
if (!name) {
this.events = getEmptyEventsMap();
return;
}
if (!listener) {
this.events[name] = [];
return;
}
const index = this.events[name].findIndex(fn => fn === listener);
if (index > -1) {
this.events[name].splice(index, 1);
}
}
public send (data: string | ArrayBuffer | SharedArrayBuffer | Blob | ArrayBufferView) {
if (this.websocket?.readyState === WebSocket.OPEN) {
this.websocket.send(data);
}
}
private emit(name: WebSocketEventEnum, event: any) {
this.events[name].forEach(listener => listener(event));
}
}
支持单例
考虑到如果某一个websocket状态如果与某一个文件强关联那么可以把事件直接注册到那个文件中,为了避免创建多个实例,所以考虑加一个单例功能
class WebSocketClient {
private static instance: WebsocketClient | null = null;
public static getInstance(url: string, options: Partial<ClientOptions> = {}) {
if (!WebsocketClient.instance) {
WebsocketClient.instance = new WebsocketClient(url, options);
}
return WebsocketClient.instance;
}
}
设置超时时间
interface ClientOptions {
// ...
timeout: number;
}
const getDefaultOptions = (): ClientOptions => ({
// ...
timeout: 20_000,
});
class WebSocketClient {
public connect() {
this.websocket = new WebSocket(this.url, this.options.protocols);
this.setTimer();
this.websocket.onopen = (event) => {
this.clearTimer();
this.emit(WebSocketEventEnum.open, event);
}
// ...
}
private setTimer() {
this.clearTimer();
this.timer = setTimeout(() => {
// 疑问1
this.websocket?.close();
}, this.options.timeout);
}
private clearTimer() {
this.timer !== null && clearTimeout(this.timer);
}
}
断开重连
interface ClientOptions {
// ...
reconnectionAttempts: number;
reconnectionDelay: number;
}
const enum WebSocketEventEnum {
// ...
reconnectAttempt = 'reconnectAttempt',
reconnectFailed = 'reconnectFailed'
}
const getDefaultOptions = (): ClientOptions => ({
// ...
reconnectionAttempts: Infinity,
reconnectionDelay: 5_000,
});
class WebSocketClient {
private reconnectionAttempts = 0;
public connect(resetReconnectionAttempts = true) {
// 手动调用默认重置重连,但是内部调用不需要清空
if (resetReconnectionAttempts) {
this.reconnectionAttempts = 0;
}
// ...
this.websocket.onerror = (event) => {
this.emit(WebSocketEventEnum.error, event);
this.reconnect(event);
}
this.websocket.onclose = (event) => {
this.emit(WebSocketEventEnum.close, event);
this.reconnect(event);
}
}
public disconnect() {
this.reconnectionAttempts = -1;
this.websocket?.close(1_000, 'Normal Closure');
}
private reconnect (event: Event) {
// -1时不需要重连
if (this.reconnectionAttempts === -1) {
this.websocket = null;
return;
}
// 疑问2
if (this.websocket?.readyState !== WebSocket.CLOSED) {
return;
}
this.websocket = null;
this.reconnectionAttempts++;
this.emit(WebSocketEventEnum.reconnectAttempt, this.reconnectionAttempts);
if (!Number.isFinite(this.options.reconnectionAttempts) || this.reconnectionAttempts <= this.options.reconnectionAttempts) {
setTimeout(() => {
this.connect(false);
}, this.options.reconnectionDelay);
return;
}
this.emit(WebSocketEventEnum.reconnectFailed, event);
}
}
其他
其实还可以增加once
功能,只触发一次,这里的逻辑和on类似,因此不再列举
问题
在上面的代码中引申出两个问题
疑问1
在超时时间中我直接使用了this.websocket?.close()
,根据MDN中的语法来说存在code
和reason
- 默认打印出的
code
和reason
是什么 - 如果我在代码中手动赋值,那么打印出的
code
和reason
是什么,是默认值还是手动赋的值
疑问2
- 如果我直接设置
this.websocket = null
不加前面的判断会出现什么结果