主题
问诊室 WebSocket 消息(一)
本页对应 问诊室 WebSocket 消息,共 7 段视频,约 1 小时 38 分。
学习目标
- 能说明 WebSocket 与 HTTP 的通讯差异,并使用原生 WebSocket 完成建立连接、收发消息、监听关闭和主动关闭连接。
- 能使用 socket.io-client 完成连接、发送消息、接收消息、关闭连接,并正确处理跨域配置。
- 能在问诊室中携带 token 与 orderId 初始化 Socket.IO 连接,绑定 connect、disconnect、error 事件,并在组件卸载时关闭连接。
- 能依据通信规则监听 chat message list 获取默认消息,并将二维时间段消息转换为页面所需的一维消息列表。
- 能按 messageType 渲染患者卡片、通用通知和温馨提示,完成选项文字转换及病情描述图片预览。
- 能定义订单状态枚举和订单详情类型,调用接口获取订单数据,并监听 state change 事件更新问诊订单状态。
知识点
从 HTTP 到 WebSocket:为什么需要它
(原笔记图:HTTP 与 WebSocket 通讯对比图)用于说明:HTTP 通常只能由客户端发起请求,服务器再响应;WebSocket 则在客户端发起连接后,双方都能主动发消息。
为什么需要:问诊室要聊天,患者和医生都要能主动收消息,HTTP/Ajax 的“请求—响应”不够用。怎么想:先把 WebSocket 理解成一种通讯协议,和 HTTP 一样用于客户端与服务端通讯,但支持双向。怎么做:在 JS 中用内置 WebSocket 建立连接,监听 open、message、close,用 send 发消息,用 close 关闭。易错点:协议地址是 ws:// 或加密的 wss://,不是 HTTP;连接成功要监听 onopen,不能 new 完就认为已连上。
js
const ws = new WebSocket('wss://服务端地址')
ws.onopen = () => console.log('连接成功')
btn.onclick = () => ws.send('你好,服务器')
ws.onmessage = (event) => console.log('服务端消息', event.data)
ws.onclose = () => console.log('连接关闭')
btn2.onclick = () => ws.close()调试时可看到:刷新后打印“连接成功”;点击发送后服务端回 hello from server,说明双向通讯成立;发消息几秒后服务端主动关闭连接,触发 onclose。
socket.io-client 的基本使用
为什么需要:原生 WebSocket 适合理解过程,项目中一般用封装度更高的 Socket.IO。怎么想:Socket.IO 是即时通讯解决方案,提供后端服务和前端 JS 库;本节只关注客户端如何连接、发消息、收消息、断开。怎么做:安装 socket.io-client,用 io(地址) 创建实例,监听 connect,用 emit 发消息,用 on 收消息,用 close 关闭。易错点:客户端包名要带 -client;服务端与前端端口不同会跨域,需在服务端开启 CORS,转写中错误就是 CORS,服务端配置 cors: true 后重启即可。
js
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000')
socket.on('connect', () => console.log('连接成功'))
socket.emit('chat message', '你好')
socket.on('chat message', (message) => console.log(message))
socket.close()
socket.on('disconnect', () => console.log('连接关闭'))问诊室建立连接:token 与订单 ID
为什么需要:问诊室不是匿名聊天,必须认证身份,并把患者和医生放进同一订单聊天室。怎么想:连接地址复用接口的 baseURL,认证参数放 auth.token,订单参数放 query.orderId。怎么做:在 onMounted 初始化 socket,绑定 connect、disconnect、error,在 onUnmounted 关闭连接。易错点:socket 若只在 onMounted 内声明,onUnmounted 用不到,要提取到外层并加 Socket 类型;orderId 从路由 query 取,类型断言为字符串。
ts
import { io, type Socket } from 'socket.io-client'
import { baseURL } from '@/utils/request'
let socket: Socket
onMounted(() => {
socket = io(baseURL, {
auth: { token: userStore.user?.token },
query: { orderId: route.query.orderId as string }
})
socket.on('connect', () => console.log('连接成功'))
socket.on('disconnect', () => console.log('连接关闭'))
socket.on('error', (err) => console.log('通讯错误', err))
})
onUnmounted(() => socket?.close())通信规则与默认消息处理
为什么需要:聊天、历史记录、已读、订单状态变化都靠约定事件名通讯。怎么想:接口文档中的事件名相当于“地址”;建立连接后服务端主动发 chatMessageList,客户端监听即可拿默认记录。怎么做:收到 res.data 后把二维时间段数组转成一维消息列表,并把每段 createTime 也拼成一条通用通知消息;列表向上追加用 unshift。易错点:data 不是消息数组而是时间段数组;items 才是具体消息;createTime 要补成消息;消息类型数字要用枚举增强语义。
ts
socket.on('chatMessageList', (res) => {
const data = res.data as TimeMessages[]
const list: Message[] = []
data.forEach((item) => {
list.push({
id: item.createTime,
messageType: MessageType.Notify,
message: { content: item.createTime },
createTime: item.createTime
} as Message)
list.push(...item.items)
})
messageList.value.unshift(...list)
})渲染默认消息与订单状态
为什么需要:聊天记录要按类型渲染患者卡片、普通通知、温馨提示;接诊状态又依赖订单数据。怎么想:v-for 遍历列表传给 RoomMessage,组件内按 messageType 条件渲染;订单状态用枚举,初始化取详情,状态变更事件触发后重新取详情。怎么做:患者卡片展示姓名、性别、年龄、病情描述;患病时间、是否就诊用常量 options 的 label 转中文;图片点击用 showImagePreview,先 map 出 url;无图给轻提示。订单用 getConsultOrderDetail(orderId),并监听状态变更事件重新 loadConsult。易错点:consultRecord 可能为空,要用条件判断做类型守卫;预览参数可能为空,要判断长度;事件名以接口文档为准;医生接诊后状态会从 2 变 3,需刷新订单数据。
ts
const loadConsult = async () => {
const res = await getConsultOrderDetail(route.query.orderId as string)
consult.value = res.data
}
onMounted(loadConsult)
socket.on('stateChange', loadConsult) // 事件名以接口文档为准至此,问诊室从 WebSocket 原理、Socket.IO 客户端、带认证连接、默认消息处理到订单状态驱动,形成了完整闭环。
从原生 WebSocket 到 socket.io-client
为什么需要:原生 WebSocket 能实现客户端与服务端双向通信,但实际项目里还要处理重连、事件命名、房间、认证等细节。转写稿里先用原生 WebSocket 演示了 onopen、send、onmessage、close、onclose,目的是理解“建立连接—收发消息—关闭连接”的流程;真正做问诊室聊天时,改用封装度更高的 socket.io-client。
怎么想:把 socket.io-client 看成“带事件系统的双向通信客户端”。连接成功后,用 emit 发消息,用 on 收消息,用 close 断开。
怎么做:先安装客户端包,再初始化连接并绑定事件。
易错点:不要把 socket.io-client 当成普通 Ajax;它的“地址”是服务端地址,“事件名”由前后端约定,不是接口 URL。
(图:HTTP与WebSocket通信对比;图:socket.io-client连接、收发、关闭流程)
安装与最小连接示例
为什么需要:先跑通最小示例,才能确认地址、协议和跨域没有问题。
怎么想:视频2中先克隆官方示例服务端,启动 localhost:3000,再用客户端连接。
怎么做:
ts
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000')
socket.on('connect', () => {
console.log('连接成功')
})
socket.emit('chat message', '你好 socket.io')
socket.on('chat message', (message) => {
console.log('服务端消息', message)
})
socket.on('disconnect', () => {
console.log('连接关闭')
})
// socket.close()调试过程:浏览器刷新后报 CORS,因为客户端在 5173,服务端在 3000。到服务端 index.js 的 io 配置中开启 cors: true,重启服务端后连接成功。
易错点:改完服务端配置要重启;connect 事件触发才代表连接成功。
问诊室中的连接:认证与订单ID
为什么需要:问诊室不是匿名聊天,必须带 token 做身份认证,还要带 orderId 把患者和医生放进同一聊天室。
怎么想:连接时通过 auth 传 token,通过 query 传 orderId。
怎么做:
ts
import { io, Socket } from 'socket.io-client'
import { onMounted, onUnmounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { useRoute } from 'vue-router'
import { baseURL } from '@/utils/request'
let socket: Socket
const store = useUserStore()
const route = useRoute()
onMounted(() => {
socket = io(baseURL, {
auth: { token: store.user?.token },
query: { orderId: route.query.orderId }
})
socket.on('connect', () => console.log('连接成功'))
socket.on('disconnect', () => console.log('连接关闭'))
socket.on('error', (err) => console.log('通信错误', err))
})
onUnmounted(() => {
socket?.close()
})易错点:socket 要提到 onMounted 外,否则 onUnmounted 拿不到;加 Socket 类型才有 close 提示;orderId 从地址栏取,注意类型断言。
通信规则与默认消息获取
为什么需要:聊天室有固定事件规则,不能自己乱起事件名。
怎么想:接口文档中约定:chat message list 获取历史记录,send chat message 发消息,receive chat message 收医生消息,status change 监听订单状态。
怎么做:
ts
socket.on('chat message list', (res) => {
console.log('默认聊天记录', res.data)
})易错点:事件名不是 URL;服务端主动推送 chat message list 后,客户端只负责监听并处理 res.data。默认数据是按时间段返回的二维数组,需要把 createTime 也拼成一条 messageType.notify 消息,再与 items 合并成一维列表,并用 unshift 往前追加历史消息。渲染时按 messageType 区分患者卡片、通用通知、温馨提示等。
易错点汇总
- 连接地址用服务端地址,问诊室项目里复用
baseURL,不要重复写死。 connect、disconnect、error是基础事件,建议都绑定,方便调试。- 发消息用
socket.emit('事件名', 内容),收消息用socket.on('事件名', 回调)。 - 离开页面必须
socket.close(),否则连接可能残留。 - 默认消息处理注意二维转一维、
createTime转消息、messageType用枚举、图片预览用showImagePreview并处理空数组。
从 HTTP 到 WebSocket:为什么聊天不能只靠 Ajax
本节笔记中的协议对比图(alt:HTTP 与 WebSocket 通讯方式对比)是理解后续代码的入口。
为什么需要:HTTP/Ajax 的通讯通常只能由客户端先发请求,服务端再响应;问诊室聊天需要医生端、患者端和服务端之间主动推送消息,HTTP 这种“一问一答”不够用。
怎么想:WebSocket 在握手建立连接后,服务端可以主动给客户端发消息,客户端也可以给服务端发消息,直到任意一端关闭连接。
怎么做:原生写法先 new WebSocket(url),再监听 onopen、onmessage,发送用 send,关闭用 close,并监听 onclose。
易错点:WebSocket 地址协议是 ws:// 或加密的 wss://;onmessage 里服务端消息通常在 event.data 中;原生 API 只适合理解原理,项目里一般用封装度更高的库。
js
const ws = new WebSocket('ws://localhost:3000')
ws.onopen = () => console.log('连接成功')
ws.onmessage = (event) => console.log('服务端消息', event.data)
ws.send('你好,服务端')
ws.close()
ws.onclose = () => console.log('连接关闭')项目里为什么改用 socket.io-client
为什么需要:原生 WebSocket 需要自己处理重连、事件约定、跨域调试等细节,实际即时通讯项目更常用 Socket.IO。
怎么想:Socket.IO 同时提供服务端和客户端库,客户端通过 io() 建立连接,通过自定义事件名收发消息,事件名相当于前后端约定的“通讯规则”。
怎么做:安装 socket.io-client,在组件中导入 io 并创建实例;服务端地址虽然写成 http://localhost:3000,但它本质连接的是挂在 HTTP 服务上的 Socket.IO 服务。
易错点:客户端端口与服务端端口不同会触发 CORS,调试时服务端需开启跨域配置,例如 cors: true;不要误以为地址是 HTTP 就只是在发 Ajax。
js
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000')
socket.on('connect', () => console.log('连接成功'))建立连接时携带 token 和 orderId
为什么需要:问诊室必须确认“你是谁”以及“你进的是哪个问诊订单的聊天室”,否则不能安全聊天。
怎么想:身份信息放 auth.token,订单 ID 放 query.orderId;服务端地址与接口基础地址一致,应抽成 baseURL 统一维护。
怎么做:在 onMounted 中初始化 Socket.IO,从用户 store 取 token,从路由 route.query.orderId 取订单号;把 socket 提到外层并加 Socket 类型,方便 onUnmounted 关闭。
易错点:route.query.orderId 可能是数组或 undefined,按项目约定可断言为 string;不要把 socket 只声明在 onMounted 内部,否则卸载时拿不到。
ts
import { io, type Socket } from 'socket.io-client'
let socket: Socket
onMounted(() => {
socket = io(baseURL, {
auth: { token: userStore.user?.token },
query: { orderId: route.query.orderId }
})
socket.on('connect', () => console.log('连接成功'))
socket.on('disconnect', () => console.log('连接关闭'))
socket.on('error', (err) => console.log('通讯错误', err))
})
onUnmounted(() => socket?.close())通信规则:先拿默认消息,再按事件收发
为什么需要:聊天不只“发一条收一条”,还要拉历史记录、标记已读、监听订单状态变化。
怎么想:把接口文档中的事件名当成规则:chat message list 获取聊天记录,send chat message 发消息,receive chat message 收消息,update message status 改已读,status change 监听订单状态。
怎么做:连接成功后立即监听 chat message list,服务端会主动推送默认聊天记录;医生发消息时监听接收事件。
易错点:这些是事件名,不是请求 URL;服务端主动推送的消息要在连接后尽早绑定监听。
ts
socket.on('chat message list', (res) => {
console.log('默认聊天记录', res)
})默认消息格式处理:二维时间段转一维消息列表
为什么需要:服务端 data 是按时间段组织的二维数组,每个时间段有 createTime 和 items,而页面渲染需要一维消息列表。
怎么想:遍历每个时间段,先把 createTime 拼成一条通用通知消息,再把该时间段 items 中的具体消息追加进去。
怎么做:准备 list 保存消息,遍历 data 生成 arr,最后用 unshift 追加到 list 前面,因为历史消息应向上追加。
易错点:时间消息的类型用 MessageType.notify;时间消息的 id 可用 createTime;不要用 push 把历史记录加到列表尾部。
ts
const list = ref<Message[]>([])
res.data.forEach((item: TimeMessages) => {
arr.push({
id: item.createTime,
msgType: MessageType.notify,
content: item.createTime,
createTime: item.createTime
} as Message)
arr.push(...item.items)
})
list.value.unshift(...arr)渲染默认消息:按 messageType 分支
为什么需要:同一聊天室里有患者卡片、普通通知、温馨提示、图片等不同结构。
怎么想:父组件 v-for 遍历 list,把单条 item 传给 RoomMessage;子组件根据 item.msgType 决定渲染哪块结构。
怎么做:患者卡片渲染姓名、性别、年龄、病情描述;患病时间、是否就诊过要把 0/1/2 等值通过常量 options 转成中文;图片预览用 showImagePreview(images.map(i => i.url))。
易错点:images 可能为 undefined,预览前要判断长度,否则类型和运行都可能出错;通用消息和温馨提示分别判断 notify、notifyTip。
ts
const onPreviewImage = (images?: Image[]) => {
if (images?.length) showImagePreview(images.map((i) => i.url))
else showToast('暂无图片')
}订单数据与接诊状态变化
为什么需要:问诊室是否允许输入文字、上传图片,要由订单接诊状态控制。
怎么想:先定义订单状态枚举,再定义订单详情类型;初始化时获取订单详情,医生接诊或结束时服务端会推送 status change,收到后重新获取订单详情。
怎么做:封装 getConsultOrderDetail(orderId),组件 onMounted 调 loadConsult(),并监听 socket.on('status change', loadConsult)。
易错点:订单状态 2 是待接诊,3 是问诊中;订单详情类型可复用已有 consult 并扩展字段;从地址栏取到的 orderId 要按 string 处理。
ts
const consult = ref<ConsultOrderItem>()
const loadConsult = async () => {
const res = await getConsultOrderDetail(route.query.orderId as string)
consult.value = res.data
}
onMounted(() => {
loadConsult()
socket.on('status change', loadConsult)
})代码示例
示例一:原生 WebSocket 完成前后端双向通讯
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>原生 WebSocket 通讯</title>
</head>
<body>
<button id="btn">给服务端发消息</button>
<button id="btn2">关闭连接</button>
<script>
// 服务端地址替换为笔记中准备好的测试地址(ws 相当于 http,wss 相当于 https)
const WS_URL = 'ws://笔记中提供的服务端地址'
// 1. 建立连接
const ws = new WebSocket(WS_URL)
// 2. 服务端应答后触发 open,代表连接成功
ws.onopen = () => {
console.log('连接成功')
}
// 3. 客户端给服务端发消息
document.querySelector('#btn').addEventListener('click', () => {
ws.send('你好,服务器')
})
// 4. 接收服务端发来的消息
ws.onmessage = (event) => {
// 服务端发的消息在 event.data 里
console.log('服务端发的消息:', event.data)
}
// 5. 监听连接关闭(服务端几秒后也会主动断开)
ws.onclose = () => {
console.log('连接关闭')
}
// 6. 客户端主动关闭连接
document.querySelector('#btn2').addEventListener('click', () => {
ws.close()
})
</script>
</body>
</html>- 关键行:
new WebSocket(url)建立连接;onopen表示服务端应答、连接成功;ws.send()发消息;onmessage收消息;ws.close()主动断开;onclose监听断开。 - 易错点:HTTP 只能由客户端发起、服务端无法主动推送,聊天必须用 WebSocket 这种双向协议;收到的数据在
event.data里,不是event本身;连接关闭后不能再send。
示例二:socket.io-client 基本使用(连接 / 发消息 / 收消息 / 关闭)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>socket.io-client 基本使用</title>
</head>
<body>
<script src="https://cdn.socket.io/4/socket.io.min.js"></script>
<script>
// socket.io 服务挂在 3000 端口的 web 服务上,所以可以用 http 协议连接
const socket = io('http://localhost:3000')
// 1. 连接成功
socket.on('connect', () => {
console.log('连接成功')
// 2. 发消息:事件名前后端约定为 chat message
socket.emit('chat message', '你好 socket.io')
})
// 3. 接收消息(示例服务端会把消息原样回给我)
socket.on('chat message', (message) => {
console.log('收到消息:', message)
// 4. 收到回复后关闭连接
socket.close()
})
// 5. 监听连接关闭
socket.on('disconnect', () => {
console.log('连接关闭')
})
</script>
</body>
</html>- 关键行:
io(url)拿到 socket 实例并建立连接;connect事件代表连接成功;socket.emit('事件名', 内容)发消息;socket.on('事件名', cb)收消息,回调参数就是消息内容;socket.close()关闭连接;disconnect事件监听断开。 - 易错点:
socket.io-client的包名必须带-client(服务端是socket.io);跨域时需在服务端开启 CORS(io(server, { cors: true }))后重启服务,否则控制台报 CORS 错误;事件名是前后端约定好的自定义事件,相当于以前的接口地址。
示例三:问诊室用 socket.io-client 建立连接
vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useRoute } from 'vue-router'
import { io, type Socket } from 'socket.io-client'
import { baseURL } from '@/utils/request'
import { useUserStore } from '@/stores'
const route = useRoute()
const store = useUserStore()
// 提取到 onMounted / onUnmounted 之外,卸载时才能拿到;加 Socket 类型才有 close 提示
let socket: Socket
onMounted(() => {
// 初始化 socket 连接:地址复用接口的基础地址
socket = io(baseURL, {
// 身份认证,携带 token
auth: {
token: `Bearer ${store.user?.token}`
},
// query 传订单 ID,把自己和医生放进同一个聊天室
query: {
orderId: route.query.orderId
}
})
// 常规事件绑定
socket.on('connect', () => console.log('连接成功'))
socket.on('disconnect', () => console.log('连接关闭'))
socket.on('error', (err) => console.log('发生错误', err))
})
// 离开页面时关闭连接
onUnmounted(() => {
socket.close()
})
</script>- 关键行:
baseURL从request.ts中导出,与接口地址维护在同一个变量里;auth.token做身份认证;query.orderId取自路由地址栏,决定进入哪个聊天室;onUnmounted里socket.close()断开。 - 易错点:
socket若在onMounted里用const声明,onUnmounted中拿不到,必须提到外层;不加Socket类型时close没有类型提示;token 记得拼Bearer前缀;离开页面不关闭连接会造成无用连接。
常见错误
现象:
io()一调用就报 CORS 错误,控制台红字blocked by CORS policy,连接事件不触发。 原因:客户端跑在 5173,socket 服务在 3000,属于跨域;服务端创建Server时没开跨域。 修法:在服务端new Server(httpServer, { cors: true })打开跨域后重启服务;不要试图在客户端"绕过",跨域必须服务端放行。现象:连接地址写成
ws://localhost:3000,连接一直失败或握手报错。 原因:混淆了原生WebSocket与socket.io。原生构造用ws/wss,而socket.io挂在 HTTP 服务上,用http/https地址。 修法:io()传baseURL(http 协议);只有用浏览器内置new WebSocket()时才写ws://。项目里统一复用request.ts导出的baseURL,别手写地址。现象:离开问诊室再进来,控制台打印多次"连接成功",消息重复出现。 原因:
onMounted里创建连接,但onUnmounted没有socket.close(),旧连接残留。 修法:onUnmounted中关闭连接;同时确认socket变量声明在 setup 顶层(let socket: Socket),而不是在onMounted回调内部,否则卸载钩子里拿不到它、也没有close的类型提示。现象:患者卡片里性别/年龄正常,但"患病多久""是否就诊过"显示成
2、0。 原因:直接把枚举数字渲染到模板,没有做数字→文案的映射。 修法:把timeOptions / flagOptions抽到services/constants.ts统一导出,组件内写getIllnessTimeText()/getConsultFlagText()用find取label;consult页面同步改为引用常量,避免两处维护。现象:渲染默认消息时页面错乱、时间分组丢失,或模板里
item.message.consultRecord报"可能为 undefined"。 原因:后端data是按时间段的二维数组,items里没有时间段起始时间;且不同类型消息的message字段结构不同,TS 推断出可选。 修法:遍历时先把createTime拼成一条MessageType.notify的通用消息push进去,再展开items;渲染前用v-if做类型收窄(如item.message.consultRecord存在再取内部字段)。现象:加载更多历史聊天记录后,新加载的消息跑到了列表最底部,顺序颠倒。 原因:用
list.value.push(...)往后追加,而聊天记录是"越早越靠上"。 修法:用list.value.unshift(...arr)往头部追加;首次默认消息同样走unshift,保持一条路径。现象:点击"查看"预览病情图片,控制台报参数类型不符,或点了没反应。 原因:
pictures可能为undefined,showImagePreview要求必传有值的数组;且pictures元素是对象,需要先map成 URL 字符串数组。 修法:函数参数声明为可选,内部先判空+判长度,有值才images.map(i => i.url)后预览,否则showToast('暂无图片')。现象:连接显示成功,但收不到默认聊天记录,也收不到医生消息。 原因:一是没带
auth.token或query.orderId(没进对房间/未认证);二是监听的事件名与接口文档不一致(如写成chatMessageList而文档是chat message list)。 修法:对照接口文档逐字核对事件名;orderId从route.query.orderId取并断言为string,token从useUserStore取,二者都随io()一起传入。
练习
连接生命周期改造:把
room/index.vue的连接逻辑抽成一个useChatSocket()组合式函数,要求:返回socket、connected状态、close();组件卸载自动断开;连续进出页面 5 次,控制台"连接成功/连接关闭"必须严格成对出现。事件名常量表:新建
constants/socket-events.ts,把chat message list、send chat message、receive chat message、update message status、get chat message、status change全部定义为常量,替换现有硬编码字符串,并写注释标注每个事件的方向(客户端监听 / 客户端发送)。消息格式化纯函数:把"二维时间段数组 → 一维消息列表"的逻辑抽成
formatTimeMessages(data: TimeMessages[]): Message[],要求:不改动入参、createTime生成的消息id不与真实消息 id 冲突、写 2 条单测覆盖"空数组"和"多时间段"。消息组件条件渲染:给
RoomMessage补齐MessageType中image(4)、prescription(22)、evaluate(23/24)分支的占位结构(只渲染v-if分支 + 简单文本),确保传入任意类型消息时组件不报错、不白屏。状态联动自测:用超级医生后台抢单把订单状态从 2 改为 3,观察
status change事件是否触发、consult数据是否刷新;再手动在代码里注释掉status change的监听,对比现象并说明为什么"只靠onMounted拉一次"不够。
课件图示






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 问诊室-websocket介绍 | 14:19 |
| 2 | 问诊室-socket.io-client使用 | 14:45 |
| 3 | 问诊室-建立连接 | 11:50 |
| 4 | 问诊室-通信规则和默认消息获取 | 07:37 |
| 5 | 问诊室-默认消息-处理格式 | 16:10 |
| 6 | 问诊室-默认消息-完成渲染 | 19:44 |
| 7 | 问诊室-接诊状态-订单数据 | 14:14 |
小结
- 原生
WebSocket只需掌握onopen / onmessage / send / close / onclose五个点,它的价值在于理解双向通信的时序,业务里不要直接用它。 socket.io的连接必须携带身份与房间信息(auth.token+query.orderId),否则"连上了"也只是个空房间,后续所有消息都会对不上。- 跨域、协议、事件名这三类问题占了连接阶段报错的绝大多数,排查顺序建议:看 Network/Console 报错 → 核对协议与地址 → 核对事件名拼写。
- 连接的创建放在
onMounted、销毁放在onUnmounted,socket变量必须声明在两者共同的作用域并标注Socket类型,否则既拿不到也写不出提示。 - 后端返回的聊天数据结构是为"分时间段"设计的,前端必须做一次二维到一维的转换,并把时间段起始时间补成一条通用消息,否则时间轴会丢。
- 消息渲染的本质是"按
messageType分支渲染",数字枚举必须先映射成中文文案,这类映射表应抽到公共常量文件,避免多组件重复维护。 - 订单状态是问诊室 UI 的开关:初始化拉一次 +
status change事件里再拉一次,二者缺一不可。 - 聊天列表追加方向决定体验:历史消息用
unshift,实时新消息用push,方向搞反比样式错乱更难被用户容忍。