WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许客户端与服务端实时交换数据,无需频繁轮询,适合聊天室、股票行情、大屏数据等场景。它相比 HTTP 延迟低、效率高,可实现推送与订阅模式。
安装依赖
pnpm add @vueuse/core@14.1.0
<script setup lang="ts">
import { onUnmounted, ref } from 'vue'
import { useWebSocket } from '@vueuse/core'
const WS_URL = 'wss://ws.postman-echo.com/raw'
const message = ref('')
const messages = ref<string[]>([])
const {
status,
send,
open,
close,
} = useWebSocket(WS_URL, {
autoReconnect: {
retries: -1,
delay: 1000,
},
heartbeat: {
message: 'ping',
interval: 10000,
},
onMessage(_, event) {
handleServerMessage(event.data)
},
})
/**
* 单独处理服务端消息的业务函数
*
* @param msg 服务端的消息
*/
function handleServerMessage(msg: string) {
messages.value.push(`服务端:${msg}`)
}
/**
* 发送消息
*/
const sendMessage = () => {
if (!message.value) return
send(message.value)
messages.value.push(`我:${message.value}`)
message.value = ''
}
/**
* 主动断开
*/
const handleDisconnect = () => {
close(1000, 'manual close')
}
onUnmounted(() => {
close()
})
</script>
<template>
<div class="container">
<h1>useWebSocket(WebSocket 响应式通信)</h1>
<div class="card">
<h2>连接状态</h2>
<p>
当前状态:
<span :class="{ open: status === 'OPEN', close: status !== 'OPEN' }">
{{ status }}
</span>
</p>
<button @click="open">手动连接</button>
<button @click="handleDisconnect">断开连接</button>
</div>
<div class="card">
<h2>发送消息</h2>
<input
v-model="message"
placeholder="输入要发送的内容"
class="input"
@keyup.enter="sendMessage"
/>
<button @click="sendMessage">发送</button>
</div>
<div class="card">
<h2>消息记录</h2>
<div class="log">
<div v-for="(item, index) in messages" :key="index" class="log-item">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.container {
padding: 24px;
font-family: Arial, Helvetica, sans-serif;
}
.card {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.open {
color: #67c23a;
font-weight: bold;
}
.close {
color: #f56c6c;
font-weight: bold;
}
.input {
width: 70%;
padding: 6px 10px;
border: 1px solid #dcdcdc;
border-radius: 4px;
margin-right: 8px;
}
button {
padding: 6px 14px;
border: none;
border-radius: 4px;
background-color: #409eff;
color: #ffffff;
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
.log {
max-height: 200px;
overflow-y: auto;
background-color: #f8f8f8;
padding: 10px;
border-radius: 4px;
}
.log-item {
font-size: 14px;
margin-bottom: 4px;
}
.tip {
background-color: #f8f8f8;
}
</style><script setup lang="ts">
import { ref, computed, onUnmounted } from 'vue'
import { useWebSocket } from '@vueuse/core'
const WS_URL = 'wss://ws.postman-echo.com/raw'
const message = ref('')
const logs = ref<string[]>([])
const {
status,
send,
open,
close,
} = useWebSocket(WS_URL, {
// 初始化自动连接(默认 true)
immediate: true,
// 指定 WebSocket 子协议(常用于 JWT 或消息订阅协议)
// protocols: ['token-xxxx', 'v1'],
protocols: [],
// 断线自动重连策略
autoReconnect: {
retries: -1, // 最大重连次数
delay: 1000, // 固定延迟
// 以下可实现指数退避策略:
// delay: retryCount => Math.min(2000 * retryCount, 10000),
onFailed() {
appendLog('重连失败:已超过最大次数')
},
},
// 心跳机制(Ping-Pong)
heartbeat: {
message: 'ping',
interval: 8000,
},
// 打开连接回调
onConnected(_) {
appendLog('WebSocket 连接成功!')
},
// 关闭连接回调
onDisconnected(_, event) {
appendLog(`连接断开:code=${event.code} reason=${event.reason}`)
},
// 错误回调
onError(_, event) {
appendLog(`WebSocket 错误:${event}`)
},
// 收到消息
onMessage(_, event) {
handleServerMessage(event.data)
},
})
/** 处理服务端消息 */
function handleServerMessage(msg: string) {
if (msg === 'pong') {
appendLog('收到心跳响应:pong')
return
}
appendLog(`服务端:${msg}`)
}
/** 追加日志 */
function appendLog(text: string) {
const time = new Date().toLocaleTimeString()
logs.value.push(`[${time}] ${text}`)
// 自动滚动到底部(可选)
requestAnimationFrame(() => {
const el = document.querySelector('.log')
el && (el.scrollTop = el.scrollHeight)
})
}
/** 手动发送 */
const sendMessage = () => {
if (!message.value) return
send(message.value)
appendLog(`我:${message.value}`)
message.value = ''
}
/** 手动断开 */
const handleDisconnect = () => {
close(1000, 'manual close')
}
/** 连接状态友好提示 */
const statusText = computed(() => {
switch (status.value) {
case 'OPEN': return '🟢 已连接'
case 'CONNECTING': return '🟡 连接中...'
case 'CLOSED': return '🔴 已关闭'
default: return status.value
}
})
onUnmounted(() => {
close()
})
</script>
<template>
<div class="container">
<h1>useWebSocket(高级配置版)</h1>
<div class="card">
<h2>连接状态</h2>
<p>{{ statusText }}</p>
<button @click="open">手动连接</button>
<button @click="handleDisconnect">断开连接</button>
</div>
<div class="card">
<h2>发送消息</h2>
<input v-model="message" placeholder="输入内容 回车发送" @keyup.enter="sendMessage" class="input" />
<button @click="sendMessage">发送</button>
</div>
<div class="card">
<h2>消息日志</h2>
<div class="log">
<div v-for="(item, index) in logs" :key="index" class="log-item">
{{ item }}
</div>
</div>
</div>
</div>
</template>
<style scoped>
.container {
padding: 24px;
font-family: Arial, Helvetica, sans-serif;
}
.card {
border: 1px solid #dcdcdc;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.input {
width: 70%;
padding: 6px 10px;
border: 1px solid #dcdcdc;
margin-right: 8px;
}
button {
padding: 6px 14px;
border: none;
border-radius: 4px;
background-color: #409eff;
color: #fff;
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
.log {
height: 220px;
overflow-y: auto;
background-color: #f6f6f6;
padding: 8px;
}
.log-item {
font-size: 13px;
margin-bottom: 4px;
font-family: monospace;
}
</style>src/composables/useWebSocketProvider.ts
import { provide, inject, ref } from 'vue'
import { useWebSocket, type UseWebSocketReturn } from '@vueuse/core'
const WS_KEY = Symbol('GLOBAL_WS')
/**
* 服务端数据最大缓存长度
* 超过该长度后会自动丢弃最旧的数据,防止内存无限增长
*/
const MAX_DATA_LENGTH = 100
/**
* 扩展全局 WebSocket 类型
* 在原有 UseWebSocketReturn 的基础上增加:
* - latest:最新一条服务端数据
* - dataList:最近 MAX_DATA_LENGTH 条服务端数据
*/
export interface GlobalWebSocket extends UseWebSocketReturn<any> {
/**
* 最新一条服务端推送的数据
*/
latest: ReturnType<typeof ref<string | null>>
/**
* 最近 MAX_DATA_LENGTH 条服务端推送的数据
*/
dataList: ReturnType<typeof ref<string[]>>
}
/**
* 在 App.vue 中调用
* 创建并提供全局 WebSocket 实例
*
* @param url WebSocket 服务端地址
*/
export function provideWebSocket(url: string) {
console.info('[WebSocket] 开始初始化连接,地址:', url)
/**
* 最新一条数据
*/
const latest = ref<string | null>(null)
/**
* 最近 MAX_DATA_LENGTH 条数据
*/
const dataList = ref<string[]>([])
const ws = useWebSocket(url, {
autoReconnect: {
retries: -1,
delay: 1000,
},
heartbeat: {
message: 'ping',
interval: 8000,
},
onConnected() {
console.info('[WebSocket] 🟢 连接成功')
},
onDisconnected(_, e) {
console.warn(
`[WebSocket] 🔴 连接已断开,状态码:${e.code},原因:${e.reason || '无'}`
)
},
onError(_, e) {
console.error('[WebSocket] ❌ 发生错误:', e)
},
/**
* 只处理服务端推送的数据
* 不记录连接日志,不记录自己发送的数据
*/
onMessage(_, e) {
const data = e.data as string
console.info('[WebSocket] 📩 收到服务端数据:', data)
// 更新最新数据
latest.value = data
// 维护一个固定长度的滑动窗口数据队列
dataList.value.push(data)
if (dataList.value.length > MAX_DATA_LENGTH) {
dataList.value.shift()
}
},
})
provide(WS_KEY, {
...ws,
latest,
dataList,
})
console.info('[WebSocket] 已通过 provide 注入为全局实例')
}
/**
* 在任意页面中调用
* 获取全局 WebSocket 实例
*/
export function useGlobalWebSocket(): GlobalWebSocket {
const ws = inject<GlobalWebSocket>(WS_KEY)
if (!ws) {
console.error(
'[WebSocket] ❌ 获取失败:未找到全局 WebSocket 实例,请确认已在 App.vue 中调用 provideWebSocket'
)
throw new Error(
'未找到全局 WebSocket 实例,请先在 App.vue 中调用 provideWebSocket() 进行初始化'
)
}
console.info('[WebSocket] 成功获取全局 WebSocket 实例')
return ws
}App.vue
<script setup lang="ts">
import { provideWebSocket } from '@/composables/useWebSocketProvider'
/**
* 从环境变量中获取 WebSocket 基础地址
*/
const WS_URL = import.meta.env.VITE_WS_URL as string
/**
* 从 localStorage 中读取 token
* 建议统一约定 key,例如:ACCESS_TOKEN
*/
const TOKEN_KEY = 'ACCESS_TOKEN'
const token = localStorage.getItem(TOKEN_KEY)
/**
* 拼接最终 WebSocket 连接地址
*/
let wsUrl = WS_URL
if (token) {
wsUrl = `${WS_URL}?token=${encodeURIComponent(token)}`
console.info('[WebSocket] 已从 localStorage 中读取到 Token,已拼接到连接地址')
} else {
console.warn(
`[WebSocket] 未在 localStorage 中找到 Token(key=${TOKEN_KEY}),将以匿名方式建立连接`
)
}
console.info('[WebSocket] 最终连接地址:', wsUrl)
/**
* 初始化并注入全局 WebSocket
*/
provideWebSocket(wsUrl)
</script>
<template>
<router-view />
</template>环境变量
.env.development
VITE_WS_URL=wss://ws.postman-echo.com/raw
.env.production
VITE_WS_URL=wss://your-prod-ws-server/ws
views/Demo.vue
<script setup lang="ts">
import { ref } from 'vue'
import { useGlobalWebSocket } from '@/composables/useWebSocketProvider'
/**
* 从全局 WebSocket 中获取:
* - status:连接状态
* - send / open / close:连接控制
* - latest:最新一条服务端数据
* - dataList:最近 100 条服务端数据
*/
const { status, send, open, close, latest, dataList } = useGlobalWebSocket()
const input = ref('')
/**
* 只负责发送数据
* 不在前端本地记录任何发送日志
* 所有展示数据全部来自服务端 onMessage
*/
const sendMsg = () => {
if (!input.value) return
send(input.value)
input.value = ''
}
</script>
<template>
<div style="padding:20px">
<h2>全局 WebSocket</h2>
<p>
当前状态:
<b :style="{ color: status === 'OPEN' ? 'green' : 'red' }">
{{ status }}
</b>
</p>
<div>
<button @click="open">连接</button>
<button @click="close()">断开</button>
</div>
<div style="margin-top: 12px">
<input v-model="input" placeholder="输入消息" />
<button @click="sendMsg">发送</button>
</div>
<hr />
<h3>最新一条服务端数据</h3>
<div
style="padding:8px;background:#e6f7ff;border-radius:4px;word-break:break-all"
>
{{ latest }}
</div>
<h3 style="margin-top: 16px">最近 100 条服务端数据</h3>
<div
style="margin-top:8px;background:#f6f6f6;max-height:200px;overflow:auto"
>
<div v-for="(item, i) in dataList" :key="i">
{{ item }}
</div>
</div>
</div>
</template>统一地址:
const WS_URL = 'wss://ws.postman-echo.com/raw'适合做:全局连接指示灯
<script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
const { status } = useWebSocket(WS_URL)
</script>
<template>
<div>
WebSocket 状态:{{ status }}
</div>
</template>适合:页面级 WebSocket
<script setup lang="ts">
import { onUnmounted } from 'vue'
import { useWebSocket } from '@vueuse/core'
const { close } = useWebSocket(WS_URL)
onUnmounted(() => {
close()
})
</script>适合:日志上报、埋点上报
<script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
const { send } = useWebSocket(WS_URL)
send(JSON.stringify({
type: 'report',
data: 'something happened',
}))
</script>适合:行情推送、告警推送、大屏数据
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '@vueuse/core'
const lastMessage = ref('')
useWebSocket(WS_URL, {
onMessage(_, e) {
lastMessage.value = e.data
},
})
</script>
<template>
<div>最新数据:{{ lastMessage }}</div>
</template>真实项目最常用
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '@vueuse/core'
const data = ref<any>(null)
useWebSocket(WS_URL, {
onMessage(_, e) {
try {
data.value = JSON.parse(e.data)
} catch {
console.warn('非 JSON 消息')
}
},
})
</script>真实后端几乎必用
<script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
const token = 'Bearer xxx.yyy.zzz'
useWebSocket(`${WS_URL}?token=${token}`)
</script>或协议方式:
useWebSocket(WS_URL, {
protocols: [token],
})<script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
useWebSocket(WS_URL, {
heartbeat: {
message: 'ping',
interval: 5000,
},
})
</script><script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
useWebSocket(WS_URL, {
autoReconnect: {
retries: -1,
delay: 1000,
},
})
</script><script setup lang="ts">
import { useWebSocket } from '@vueuse/core'
const { open, close } = useWebSocket(WS_URL, {
immediate: false,
})
</script>
<template>
<button @click="open">连接</button>
<button @click="close()">断开</button>
</template>例如:行情 + 通知
<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '@vueuse/core'
const price = ref('')
const notice = ref('')
useWebSocket('wss://example.com/price', {
onMessage(_, e) {
price.value = e.data
},
})
useWebSocket('wss://example.com/notice', {
onMessage(_, e) {
notice.value = e.data
},
})
</script>这是实际项目非常标准的形态:
// stores/ws.ts
import { defineStore } from 'pinia'
import { useWebSocket } from '@vueuse/core'
import { ref } from 'vue'
export const useWsStore = defineStore('ws', () => {
const messages = ref<string[]>([])
const { send } = useWebSocket(WS_URL, {
onMessage(_, e) {
messages.value.push(e.data)
},
})
return { messages, send }
})<script setup lang="ts">
import { ref } from 'vue'
import { useWebSocket } from '@vueuse/core'
const value = ref(0)
useWebSocket(WS_URL, {
onMessage(_, e) {
value.value = Number(e.data)
},
})
</script>
<template>
<h1>{{ value }}</h1>
</template>