Skip to content

Commit cb27b5e

Browse files
authored
feat: use-web-socket (#47)
1 parent 1d69304 commit cb27b5e

File tree

5 files changed

+316
-0
lines changed

5 files changed

+316
-0
lines changed

packages/hooks/docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ function getHooksSidebar() {
206206
{ text: 'useInfiniteScroll', link: '/useInfiniteScroll/' },
207207
{ text: 'useNetwork', link: '/useNetwork/' },
208208
{ text: 'useVirtualList', link: '/useVirtualList/' },
209+
{ text: 'useWebSocket', link: '/useWebSocket/' },
209210
],
210211
},
211212
{

packages/hooks/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import useUrlState from './useUrlState'
4444
import useVirtualList from './useVirtualList'
4545
import useWhyDidYouUpdate from './useWhyDidYouUpdate'
4646
import useWinResize from './useWinResize'
47+
import useWebSocket from './useWebSocket'
4748

4849
export {
4950
useAsyncOrder,
@@ -92,4 +93,5 @@ export {
9293
useVirtualList,
9394
useWhyDidYouUpdate,
9495
useWinResize,
96+
useWebSocket,
9597
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<template>
2+
<div class="container">
3+
<vhp-button
4+
v-if="readyState === ReadyState.Open"
5+
@click="() => sendMessage && sendMessage(`${Date.now()}`)"
6+
>✉️ send</vhp-button
7+
>
8+
<vhp-button
9+
style="margin-left: 8px;"
10+
v-if="readyState === ReadyState.Open"
11+
@click="() => disconnect && disconnect()"
12+
>
13+
❌ disconnect</vhp-button
14+
>
15+
<vhp-button
16+
style="margin-left: 8px;"
17+
v-if="readyState !== ReadyState.Open"
18+
@click="() => connect && connect()"
19+
>{{ readyState === ReadyState.Connecting ? 'connecting' : '📞 connect' }}</vhp-button
20+
>
21+
<div style="margin-top: 8px;">readyState: {{ readyState }}</div>
22+
<div style="margin-top: 8px;"> message count: {{ messageList.length }}</div>
23+
<div style="margin-top: 8px;">
24+
<p>received message: </p>
25+
<p v-for="(item, index) in messageList" :key="index">{{ item }}</p>
26+
</div>
27+
</div>
28+
</template>
29+
30+
<script lang="ts" setup>
31+
import { computed, ref, watchEffect } from 'vue'
32+
import { useWebSocket } from 'vue-hooks-plus'
33+
34+
enum ReadyState {
35+
Connecting = 0,
36+
Open = 1,
37+
Closing = 2,
38+
Closed = 3,
39+
}
40+
41+
const { readyState, sendMessage, latestMessage, disconnect, connect } = useWebSocket(
42+
'wss://demo.piesocket.com/v3/channel_1?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV&notify_self',
43+
)
44+
const messageHistory = computed(() => latestMessage?.value)
45+
46+
const messageList = ref<any[]>([])
47+
48+
watchEffect(() => {
49+
if (messageHistory.value?.data) messageList.value.push(messageHistory.value?.data)
50+
})
51+
</script>
52+
53+
<style scoped lang="less">
54+
.container {
55+
height: 400px;
56+
57+
overflow: scroll;
58+
}
59+
</style>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
map:
3+
# 映射到docs的路径
4+
path: /useWebSocket
5+
---
6+
7+
# useWebSocket
8+
9+
用于处理 WebSocket 的 Hook。
10+
11+
## 代码演示
12+
13+
### 基础用法
14+
15+
<demo src="./demo/demo.vue"
16+
language="vue"
17+
title="基本用法"
18+
desc=""> </demo>
19+
20+
## API
21+
22+
```typescript
23+
enum ReadyState {
24+
Connecting = 0,
25+
Open = 1,
26+
Closing = 2,
27+
Closed = 3,
28+
}
29+
30+
interface Options {
31+
reconnectLimit?: number;
32+
reconnectInterval?: number;
33+
onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void;
34+
onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void;
35+
onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void;
36+
onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void;
37+
protocols?: string | string[];
38+
}
39+
40+
interface Result {
41+
latestMessage?: WebSocketEventMap['message'];
42+
sendMessage?: WebSocket['send'];
43+
disconnect?: () => void;
44+
connect?: () => void;
45+
readyState: ReadyState;
46+
webSocketIns?: WebSocket;
47+
}
48+
49+
useWebSocket(socketUrl: string, options?: Options): Result;
50+
```
51+
52+
### Params
53+
54+
| 参数 | 说明 | 类型 | 默认值 |
55+
| --------- | -------------------- | --------- | ------ |
56+
| socketUrl | 必填,webSocket 地址 | `string` | - |
57+
| options | 可选,连接配置项 | `Options` | - |
58+
59+
#### Options
60+
61+
| 参数 | 说明 | 类型 | 默认值 |
62+
| --- | --- | --- | --- |
63+
| onOpen | webSocket 连接成功回调 | `(event: WebSocketEventMap['open'], instance: WebSocket) => void` | - |
64+
| onClose | webSocket 关闭回调 | `(event: WebSocketEventMap['close'], instance: WebSocket) => void` | - |
65+
| onMessage | webSocket 收到消息回调 | `(message: WebSocketEventMap['message'], instance: WebSocket) => void` | - |
66+
| onError | webSocket 错误回调 | `(event: WebSocketEventMap['error'], instance: WebSocket) => void` | - |
67+
| reconnectLimit | 重试次数 | `number` | `3` |
68+
| reconnectInterval | 重试时间间隔(ms) | `number` | `3000` |
69+
| manual | 手动启动连接 | `boolean` | `false` |
70+
| protocols | 子协议 | `string` \| `string[]` | - |
71+
72+
### Result
73+
74+
| 参数 | 说明 | 类型 |
75+
| --- | --- | --- |
76+
| latestMessage | 最新消息 | `Ref<WebSocketEventMap['message']>` |
77+
| sendMessage | 发送消息函数 | `WebSocket['send']` |
78+
| disconnect | 手动断开 webSocket 连接 | `() => void` |
79+
| connect | 手动连接 webSocket,如果当前已有连接,则关闭后重新连接 | `() => void` |
80+
| readyState | 当前 webSocket 连接状态 | `Ref<ReadyState>` |
81+
| webSocketIns | webSocket 实例 | `WebSocket` |
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/* eslint-disable no-unused-vars */
2+
import { watch, ref, Ref, onUnmounted, unref } from 'vue'
3+
4+
export enum ReadyState {
5+
Connecting = 0,
6+
Open = 1,
7+
Closing = 2,
8+
Closed = 3,
9+
}
10+
11+
export interface Options {
12+
reconnectLimit?: number
13+
reconnectInterval?: number
14+
manual?: Ref<boolean> | boolean
15+
onOpen?: (event: WebSocketEventMap['open'], instance: WebSocket) => void
16+
onClose?: (event: WebSocketEventMap['close'], instance: WebSocket) => void
17+
onMessage?: (message: WebSocketEventMap['message'], instance: WebSocket) => void
18+
onError?: (event: WebSocketEventMap['error'], instance: WebSocket) => void
19+
20+
protocols?: string | string[]
21+
}
22+
23+
export interface Result {
24+
latestMessage?: Ref<WebSocketEventMap['message']>
25+
sendMessage?: WebSocket['send']
26+
disconnect?: () => void
27+
connect?: () => void
28+
readyState: Ref<ReadyState>
29+
webSocketIns?: WebSocket
30+
}
31+
32+
/**
33+
* @param socketUrl socketUrl地址
34+
* @param options 配置
35+
* @return readyState(Connecting = 0,Open = 1,Closing = 2,Closed = 3)
36+
*/
37+
export default function useWebSocket(
38+
socketUrl: Ref<string> | string,
39+
options: Options = {},
40+
): Result {
41+
const {
42+
reconnectLimit = 3,
43+
reconnectInterval = 3 * 1000,
44+
manual = ref(false),
45+
onOpen,
46+
onClose,
47+
onMessage,
48+
onError,
49+
protocols,
50+
} = options
51+
52+
const reconnectTimesRef = ref(0)
53+
const reconnectTimerRef = ref<ReturnType<typeof setTimeout>>()
54+
const websocketRef = ref<WebSocket>()
55+
56+
const unmountedRef = ref(false)
57+
58+
const latestMessage = ref<WebSocketEventMap['message']>()
59+
const readyState = ref<ReadyState>(ReadyState.Closed)
60+
61+
const reconnect = () => {
62+
if (
63+
reconnectTimesRef.value < reconnectLimit &&
64+
websocketRef.value?.readyState !== ReadyState.Open
65+
) {
66+
if (reconnectTimerRef.value) {
67+
clearTimeout(reconnectTimerRef.value)
68+
}
69+
70+
reconnectTimerRef.value = setTimeout(() => {
71+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
72+
connectWs()
73+
reconnectTimesRef.value++
74+
}, reconnectInterval)
75+
}
76+
}
77+
78+
const connectWs = () => {
79+
if (reconnectTimerRef.value) {
80+
clearTimeout(reconnectTimerRef.value)
81+
}
82+
83+
if (websocketRef.value) {
84+
websocketRef.value.close()
85+
}
86+
87+
const ws = new WebSocket(unref(socketUrl), protocols)
88+
readyState.value = ReadyState.Connecting
89+
90+
ws.onerror = event => {
91+
if (unmountedRef.value) {
92+
return
93+
}
94+
reconnect()
95+
onError?.(event, ws)
96+
readyState.value = ws.readyState || ReadyState.Closed
97+
}
98+
ws.onopen = event => {
99+
if (unmountedRef.value) {
100+
return
101+
}
102+
onOpen?.(event, ws)
103+
reconnectTimesRef.value = 0
104+
readyState.value = ws.readyState || ReadyState.Open
105+
}
106+
ws.onmessage = (message: WebSocketEventMap['message']) => {
107+
if (unmountedRef.value) {
108+
return
109+
}
110+
onMessage?.(message, ws)
111+
latestMessage.value = message
112+
}
113+
ws.onclose = event => {
114+
if (unmountedRef.value) {
115+
return
116+
}
117+
reconnect()
118+
onClose?.(event, ws)
119+
readyState.value = ws.readyState || ReadyState.Closed
120+
}
121+
122+
websocketRef.value = ws
123+
}
124+
125+
const sendMessage: WebSocket['send'] = message => {
126+
if (readyState.value === ReadyState.Open) {
127+
websocketRef.value?.send(message)
128+
} else {
129+
throw new Error('WebSocket disconnected')
130+
}
131+
}
132+
133+
const connect = () => {
134+
reconnectTimesRef.value = 0
135+
connectWs()
136+
}
137+
138+
const disconnect = () => {
139+
if (reconnectTimerRef.value) {
140+
clearTimeout(reconnectTimerRef.value)
141+
}
142+
143+
reconnectTimesRef.value = reconnectLimit
144+
websocketRef.value?.close()
145+
}
146+
147+
watch(
148+
[socketUrl, manual],
149+
c => {
150+
const [_, manualWatch] = c
151+
if (!manualWatch) {
152+
connect()
153+
}
154+
},
155+
{
156+
immediate: true,
157+
},
158+
)
159+
160+
onUnmounted(() => {
161+
unmountedRef.value = true
162+
disconnect()
163+
})
164+
165+
return {
166+
latestMessage: latestMessage as Result['latestMessage'],
167+
sendMessage,
168+
connect,
169+
disconnect,
170+
readyState,
171+
webSocketIns: websocketRef.value,
172+
}
173+
}

0 commit comments

Comments
 (0)