Vue中封装并使用WebSocket

WebSocket介绍

WebSocket是一种在Web浏览器和服务器之间建立持久性连接的协议,它允许双向通信,而不需要像HTTP协议那样每次请求都需要重新建立连接。
WebSocket是全双工通信协议,可以在客户端和服务器之间实现实时数据传输。

WebSocket通常在以下场景下使用:

  1. 实时数据传输:例如股票市场、体育比赛、即时消息等需要实时更新数据的应用场景。
  2. 在线游戏:需要实时传输游戏状态、位置、动作等数据。
  3. 视频会议:需要实时传输音视频数据,WebSocket可以用于传输控制信息。
  4. 实时协作:例如团队协作、在线编辑等场景,需要实时传输文本、图像、音频等数据。
  5. 实时通知:例如推送通知、即时聊天等场景,需要实时传输消息。

WebSocket适用于需要实时双向通信的应用场景,它可以提供更好的用户体验和更高的交互性。

WebSocket状态码

状态码 含义
101 HTTP协议切换为WebSocket协议。连接成功。
1000 正常断开连接。
1001 服务器断开连接。
1002 websocket协议错误。
1003 客户端接受了不支持数据格式(只允许接受文本消息。是客户端限制不接受二进制数据,而不是websocket协议不支持二进制数据)。
1006 异常关闭。
1007 客户端接受了无效数据格式(文本消息编码不是utf-8)。
1009 传输数据量过大。
1010 客户端终止连接。
1011 服务器终止连接。
1012 服务端正在重新启动。
1013 服务端临时终止。
1014 通过网关或代理请求服务器,服务器无法及时响应。
1015 TLS握手失败。

WebSocket Web API

WebSocket Web API: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

WebSocket 对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。

构造函数

  • WebSocket(url[, protocols]) : 使用 WebSocket() 构造函数来构造一个 WebSocket 对象。

属性

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/binaryType

  • WebSocket.binaryType : 使用二进制的数据类型连接。
  • WebSocket.bufferedAmount : 未发送至服务器的字节数。
  • WebSocket.extensions : 服务器选择的扩展。
  • WebSocket.onclose : 用于指定连接关闭后的回调函数。
  • WebSocket.onerror : 用于指定连接失败后的回调函数。
  • WebSocket.onmessage : 用于指定当从服务器接受到信息时的回调函数。
  • WebSocket.onopen : 用于指定连接成功后的回调函数。
  • WebSocket.protocol : 服务器选择的下属协议。
  • WebSocket.readyState : 当前的链接状态。
  • WebSocket.url : WebSocket 的绝对路径。

方法

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/close

  • WebSocket.close([code[, reason]]) : 关闭当前链接。
  • WebSocket.send(data) : 对要传输的数据进行排队。

事件

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/close_event

使用 addEventListener() 或将一个事件监听器赋值给本接口的 oneventname 属性,来监听下面的事件。

  • close : 当一个 WebSocket 连接被关闭时触发。 也可以通过 onclose 属性来设置。
  • error : 当一个 WebSocket 连接因错误而关闭时触发,例如无法发送数据时。 也可以通过 onerror 属性来设置。
  • message : 当通过 WebSocket 收到数据时触发。 也可以通过 onmessage 属性来设置。
  • open : 当一个 WebSocket 连接成功时触发。 也可以通过 onopen 属性来设置。

JS使用WebSocket示例

以下是使用WebSocket的示例代码:

在前端JavaScript代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
const socket = new WebSocket('ws://localhost:8080');

socket.addEventListener('open', function (event) {
socket.send('Hello Server!');
});

socket.addEventListener('message', function (event) {
console.log('Message from server ', event.data);
});

socket.addEventListener('close', function (event) {
console.log('WebSocket connection closed');
});

在后端Node.js代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
console.log('WebSocket connection established');

ws.on('message', function incoming(message) {
console.log('received: %s', message);
ws.send('Server received: ' + message);
});

ws.on('close', function () {
console.log('WebSocket connection closed');
});
});

在这个示例中,前端JavaScript代码创建了一个WebSocket对象,并连接到服务器的端口8080。当连接建立时,它会向服务器发送一条消息,当从服务器收到消息时,它会打印消息内容。当WebSocket连接关闭时,它会打印一条消息。

后端Node.js代码使用WebSocket模块创建了一个WebSocket服务器,并在端口8080上监听连接请求。当客户端连接到服务器时,它会打印一条消息。当从客户端收到消息时,它会打印消息内容并将消息发送回客户端。当WebSocket连接关闭时,它会打印一条消息。

Vue3中封装WebSocket

封装websocket

/utils/websocket.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { ElMessage, ElNotification as message } from 'element-plus';
import { getEnvName } from '~/utils';
import { backend_url } from '~/constvars';
const defaultUrl = 'wss://api-backend-stg.com';

export class WlWebsocket {
private websocket: any = null;

private wsBaseUrl = defaultUrl;
// 开启标识
private socket_open = false;
// 心跳timer
private hearbeat_timer: any = null;
// 心跳发送频率
private hearbeat_interval = 8 * 1000;
// 是否自动重连
private is_reconnect = false;
// 重连次数
private reconnect_count = 3;
// 已发起重连次数
private reconnect_current = 1;
// 重连timer
private reconnect_timer: any = null;
// 重连频率
private reconnect_interval = 5 * 1000;

private url = '';
private query = '';

constructor(url: string, query: string = '') {
this.url = url;
this.query = query;
let env: any = getEnvName();
if (!!env) {
this.wsBaseUrl = 'wss://' + backend_url[env];
}
console.log('getEnvName() env', env, this.wsBaseUrl);
}

init = (receiveMessage: Function | null, onSocketClose: Function | null) => {
if (!('WebSocket' in window)) {
message.warning('浏览器不支持WebSocket');
return null;
}
const wsUrl = this.wsBaseUrl + this.url + this.query;

console.log('==wsUrl==', wsUrl);

this.websocket = new WebSocket(wsUrl);

this.websocket.onmessage = (data: any) => {
if (!!receiveMessage) {
// console.log('==data===', data);
const res = JSON.parse(data?.data);
if (res.err_code != 0) {
ElMessage.error(res?.message);
}
receiveMessage(JSON.parse(data?.data));
}
};

this.websocket.onclose = (e: any) => {
console.log('WebSocket连接关闭', e);
if (!!onSocketClose) {
onSocketClose();
}
this.socket_open = false;
// 需要重新连接
if (!!this.is_reconnect) {
console.log(
!!this.is_reconnect,
'WebSocket连接关闭 重新连接------',
this.reconnect_current
);
this.reconnect_timer = setTimeout(() => {
// 超过重连次数
if (this.reconnect_current > this.reconnect_count) {
clearTimeout(this.reconnect_timer);
this.is_reconnect = false;
return;
}
// 记录重连次数
this.reconnect_current++;
this.reconnect();
}, this.reconnect_interval);
}
};

// 连接成功
this.websocket.onopen = () => {
console.log('WebSocket连接成功');
this.socket_open = true;
// this.is_reconnect = true;
// 开启心跳
// this.heartbeat();
};

// 连接发生错误
this.websocket.onerror = (e: any) => {
console.log('WebSocket连接出错', e);
ElMessage.error(e.code + ' 连接出错');
};
};

heartbeat = () => {
console.log('== this.heartbeat 开启心跳');
this.hearbeat_timer && clearInterval(this.hearbeat_timer);

this.hearbeat_timer = setInterval(() => {
console.log('WebSocket setInterval(()) sen data');
// let data = {};
// this.send(data);
}, this.hearbeat_interval);
};

send = (data: string, callback = null) => {
console.log(
'WebSocket=====send=====',
this.websocket.readyState,
this.websocket.OPEN,
this.websocket
);

// 开启状态直接发送
if (this.websocket.readyState === this.websocket.OPEN) {
console.log('send if', this.websocket);
this.websocket.send(JSON.stringify(data));
// @ts-ignore
callback && callback();
} else {
console.log('send else');
clearInterval(this.hearbeat_timer);
ElMessage.warning('无法发送消息,socket链接已断开');
}
};

close = () => {
console.log('WebSocket 关闭关闭关闭===');
this.is_reconnect = false;
this.websocket.close();
this.websocket = null;
};

/**
* 重新连接
*/
reconnect = () => {
if (this.websocket && !this.is_reconnect) {
console.log('重新连接 this.close();');
this.close();
}
// console.log('重新连接 this.init');
// this.init(null);
};
}

封装websocket的接口API

/apis/test.ts

1
2
3
4
5
6
7
import { toQueryString } from '~/utils';
import { WlWebsocket } from '~/utils/websocket';

// 获取符合条件的所有Test列表
export const apiGetTestList = (query: any) => {
return new WlWebsocket(`/api/test/${query}/list`);
};

页面中调用接口API

/views/test/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<template>
<div>
<div>
<el-button class="search-button" @click="filter()" :disabled="loading">
筛选
</el-button>

<el-row v-if="tableList.length > 0">

<el-table :data="tableList" v-loading="loading">
<el-table-column prop="test_id" label="test" width="240">
</el-table-column>
</el-table>

<div>
<el-pagination
v-model:currentPage="searchForm.page"
v-model:page-size="searchForm.size"
:page-sizes="[5, 10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalNumber"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>

</el-row>
</div>
</div>
</template>

<script setup lang="ts">
import { ref, reactive, onBeforeUnmount, onBeforeMount } from 'vue';
import { Icon } from '@iconify/vue/dist/iconify';
import { iconMap } from '~/auth';
import { apiGetTestList } from '~/apis/order-image';

const loading = ref(false);

const tableList = ref([] as any);
const totalNumber = ref(0);

const searchForm = reactive({
page: 1,
size: 10,
});

const webSocket = ref();

// 重新渲染table数据
const refreshListData = (res: any) => {
const date = new Date();
console.log(
'重新渲染table数据 res 时间',
date.getMinutes() + ':' + date.getSeconds(),
res
);
loading.value = false;

if (res.data !== null) {
tableList.value = res.data;
totalNumber.value = res.total;
} else {
tableList.value = [];
}
};

const sendSocketMessage = () => {
const msg = {
page: searchForm.page,
size: searchForm.size,
update: true,
};
webSocket.value.send(msg);
};

const closeSocket = () => {
if (!!webSocket.value && webSocket.value.websocket != null) {
webSocket.value.close();
}
};

// 搜索事件
const publicSearch = () => {
webSocket.value = apiGetTestList({ query: '1111' });
webSocket.value.init(
(res: any) => {
refreshListData(res);
},
() => {
loading.value = false;
}
);
};

// 每页显示条目个数
const handleSizeChange = (val: any) => {
loading.value = true;
searchForm.size = val;
searchForm.page = 1;
sendSocketMessage();
};

// 当前页数
const handleCurrentChange = (val: any) => {
loading.value = true;
searchForm.page = val;
sendSocketMessage();
};

// 点击筛选按钮
const filter = async () => {
console.log(' filterEvent ');
loading.value = true;
searchForm.size = 10;
searchForm.page = 1;
publicSearch();
};

onBeforeUnmount(() => {
closeSocket();
});
</script>

<style lang="scss" scoped></style>