Pinia 不管理 WebSocket 实例,仅负责状态响应式;应封装独立的 WebSocket 生命周期逻辑,store 中用 shallowRef 存实例、ref 存状态,并由组件级 composable 统一处理连接、心跳与清理。
pinia 本身不管理 websocket 实例,它只管理状态;websocket 连接、重连、心跳等逻辑必须由你手动控制,pinia 只负责把连接状态、消息、错误等存成响应式数据。
为什么不能直接在 Pinia store 里 new WebSocket()
因为 WebSocket 是浏览器全局对象,创建后会持有事件监听器和网络句柄。如果 store 被多次调用(比如多个组件 useStore),又没做单例控制,就会反复新建连接,导致:
- 同一页面出现多个
WebSocket实例,互相干扰 -
onmessage回调被重复绑定,一条消息触发多次处理 - 组件卸载时未清理监听器,引发内存泄漏
正确做法是:用一个独立的 websocket.ts 或 useWebSocket.ts 封装连接生命周期,Pinia store 只暴露状态和操作函数(如 connect()、send())。
Pinia store 中如何安全暴露 WebSocket 状态
store 不应持有 WebSocket 实例本身(避免响应式代理污染),而应通过 ref 存储其关键属性,并用普通变量保存实例引用:
import { defineStore } from 'pinia'
import { ref, shallowRef } from 'vue'
export const useWebSocketStore = defineStore('ws', () => {
// ✅ 安全:用 shallowRef 避免对 WebSocket 实例做响应式代理
const socket = shallowRef<WebSocket | null>(null)
// ✅ 响应式状态:这些会被 Vue 监听,用于 UI 更新
const status = ref<'idle' | 'connecting' | 'open' | 'closing' | 'closed'>('idle')
const messages = ref<string[]>([])
const error = ref<string | null>(null)
// ✅ 操作函数:封装发送逻辑,内部检查 readyState
const send = (data: string) => {
if (socket.value?.readyState === WebSocket.OPEN) {
socket.value.send(data)
}
}
// ✅ 连接函数:实际创建实例并绑定事件,但不 return socket
const connect = (url: string) => {
if (socket.value) return
socket.value = new WebSocket(url)
socket.value.onopen = () => {
status.value = 'open'
error.value = null
}
socket.value.onmessage = (e) => {
messages.value.push(e.data)
}
socket.value.onerror = (e) => {
error.value = e.toString()
status.value = 'closed'
}
socket.value.onclose = () => {
status.value = 'closed'
socket.value = null // ✅ 主动清空引用,防止残留
}
}
return { status, messages, error, connect, send }
})
注意:shallowRef 是关键——它让 socket 不被 Vue 的响应式系统劫持,避免因 Proxy 导致 WebSocket 内部方法失效或报错。
立即学习“前端免费学习笔记(深入)”;
递归分析 Vue 项目组件依赖,从入口文件生成组件层级图,支持 Vue 2/3,输出组件名、文件路径和属性。适用于分析组件结构、排查依赖或了解项目架构。
心跳与重连必须脱离 Pinia 控制流
心跳检测(setInterval)和断线重连(setTimeout)属于副作用逻辑,Pinia store 里不应直接写定时器。否则:
- store 被销毁时,定时器不会自动清除,造成内存泄漏
- 多个 store 实例可能启动多套心跳,服务器收到重复 ping
- 重连逻辑若依赖 store 状态,容易陷入“状态还没更新,重连已触发”的竞态
推荐方案:在封装好的 useWebSocket() composable 中统一管理,例如:
// composables/useWebSocket.ts
import { onUnmounted, onActivated, onDeactivated } from 'vue'
import { useWebSocketStore } from '@/stores/websocket'
export function useWebSocket(url: string) {
const store = useWebSocketStore()
// ✅ 启动连接
store.connect(url)
// ✅ 心跳:每 30s 发送一次
const heartTimer = setInterval(() => {
if (store.status === 'open' && store.socket) {
store.socket.send(JSON.stringify({ type: 'ping' }))
}
}, 30000)
// ✅ 组件卸载时清理
onUnmounted(() => {
clearInterval(heartTimer)
if (store.socket) store.socket.close()
})
return store
}
这样,心跳和生命周期绑定在组件作用域,Pinia store 只专注数据同步。
多组件共享同一个 WebSocket 连接的要点
Vue 3 + Pinia 下,只要所有组件都调用同一个 store(useWebSocketStore()),就能天然共享状态。但要注意:
- 不要在每个组件里调用
connect()—— 应由根组件或布局组件首次触发 - 使用
watch监听status变化,而不是在onMounted里反复检查readyState - 发送消息前务必校验
store.status === 'open',不能只看socket?.readyState(因为实例可能已被置为 null) - 服务端返回的二进制数据(如 ArrayBuffer)需用
e.data.arrayBuffer()显式转换,Pinia store 中建议存为 base64 字符串或 Blob URL 供组件消费
最易忽略的一点:WebSocket 连接是**跨路由、跨组件存活**的,但它的事件回调(onmessage)是全局的。如果你在多个组件中都往 messages 里 push,又没做去重或分发路由,消息会混在一起。这时候该用消息类型字段(如 { type: 'chat', data: 'xxx' })配合 Pinia 的 action 分发,而不是把原始数据一股脑塞进数组。

















