Skip to content

问诊室 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 区分患者卡片、通用通知、温馨提示等。

易错点汇总 ​

  1. 连接地址用服务端地址,问诊室项目里复用 baseURL,不要重复写死。
  2. connect、disconnect、error 是基础事件,建议都绑定,方便调试。
  3. 发消息用 socket.emit('事件名', 内容),收消息用 socket.on('事件名', 回调)。
  4. 离开页面必须 socket.close(),否则连接可能残留。
  5. 默认消息处理注意二维转一维、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 前缀;离开页面不关闭连接会造成无用连接。

常见错误 ​

  1. 现象:io() 一调用就报 CORS 错误,控制台红字 blocked by CORS policy,连接事件不触发。 原因:客户端跑在 5173,socket 服务在 3000,属于跨域;服务端创建 Server 时没开跨域。 修法:在服务端 new Server(httpServer, { cors: true }) 打开跨域后重启服务;不要试图在客户端"绕过",跨域必须服务端放行。

  2. 现象:连接地址写成 ws://localhost:3000,连接一直失败或握手报错。 原因:混淆了原生 WebSocket 与 socket.io。原生构造用 ws/wss,而 socket.io 挂在 HTTP 服务上,用 http/https 地址。 修法:io() 传 baseURL(http 协议);只有用浏览器内置 new WebSocket() 时才写 ws://。项目里统一复用 request.ts 导出的 baseURL,别手写地址。

  3. 现象:离开问诊室再进来,控制台打印多次"连接成功",消息重复出现。 原因:onMounted 里创建连接,但 onUnmounted 没有 socket.close(),旧连接残留。 修法:onUnmounted 中关闭连接;同时确认 socket 变量声明在 setup 顶层(let socket: Socket),而不是在 onMounted 回调内部,否则卸载钩子里拿不到它、也没有 close 的类型提示。

  4. 现象:患者卡片里性别/年龄正常,但"患病多久""是否就诊过"显示成 2、0。 原因:直接把枚举数字渲染到模板,没有做数字→文案的映射。 修法:把 timeOptions / flagOptions 抽到 services/constants.ts 统一导出,组件内写 getIllnessTimeText() / getConsultFlagText() 用 find 取 label;consult 页面同步改为引用常量,避免两处维护。

  5. 现象:渲染默认消息时页面错乱、时间分组丢失,或模板里 item.message.consultRecord 报"可能为 undefined"。 原因:后端 data 是按时间段的二维数组,items 里没有时间段起始时间;且不同类型消息的 message 字段结构不同,TS 推断出可选。 修法:遍历时先把 createTime 拼成一条 MessageType.notify 的通用消息 push 进去,再展开 items;渲染前用 v-if 做类型收窄(如 item.message.consultRecord 存在再取内部字段)。

  6. 现象:加载更多历史聊天记录后,新加载的消息跑到了列表最底部,顺序颠倒。 原因:用 list.value.push(...) 往后追加,而聊天记录是"越早越靠上"。 修法:用 list.value.unshift(...arr) 往头部追加;首次默认消息同样走 unshift,保持一条路径。

  7. 现象:点击"查看"预览病情图片,控制台报参数类型不符,或点了没反应。 原因:pictures 可能为 undefined,showImagePreview 要求必传有值的数组;且 pictures 元素是对象,需要先 map 成 URL 字符串数组。 修法:函数参数声明为可选,内部先判空+判长度,有值才 images.map(i => i.url) 后预览,否则 showToast('暂无图片')。

  8. 现象:连接显示成功,但收不到默认聊天记录,也收不到医生消息。 原因:一是没带 auth.token 或 query.orderId(没进对房间/未认证);二是监听的事件名与接口文档不一致(如写成 chatMessageList 而文档是 chat message list)。 修法:对照接口文档逐字核对事件名;orderId 从 route.query.orderId 取并断言为 string,token 从 useUserStore 取,二者都随 io() 一起传入。

练习 ​

  1. 连接生命周期改造:把 room/index.vue 的连接逻辑抽成一个 useChatSocket() 组合式函数,要求:返回 socket、connected 状态、close();组件卸载自动断开;连续进出页面 5 次,控制台"连接成功/连接关闭"必须严格成对出现。

  2. 事件名常量表:新建 constants/socket-events.ts,把 chat message list、send chat message、receive chat message、update message status、get chat message、status change 全部定义为常量,替换现有硬编码字符串,并写注释标注每个事件的方向(客户端监听 / 客户端发送)。

  3. 消息格式化纯函数:把"二维时间段数组 → 一维消息列表"的逻辑抽成 formatTimeMessages(data: TimeMessages[]): Message[],要求:不改动入参、createTime 生成的消息 id 不与真实消息 id 冲突、写 2 条单测覆盖"空数组"和"多时间段"。

  4. 消息组件条件渲染:给 RoomMessage 补齐 MessageType 中 image(4)、prescription(22)、evaluate(23/24) 分支的占位结构(只渲染 v-if 分支 + 简单文本),确保传入任意类型消息时组件不报错、不白屏。

  5. 状态联动自测:用超级医生后台抢单把订单状态从 2 改为 3,观察 status change 事件是否触发、consult 数据是否刷新;再手动在代码里注释掉 status change 的监听,对比现象并说明为什么"只靠 onMounted 拉一次"不够。

课件图示 ​

课件第3页:问诊室-websocket介绍  什么是 websocket ?  网络通信

课件第5页:问诊室-socket.io-client 使用  socket.io 是什么?

课件第7页:问诊室-建立连接 初始化Socket 携带token 参数订单ID 绑定常规事件

课件第15页:问诊室-接诊状态-订单数据 订单状态枚举 订单详情类型 订单详情API 初始化&

课件第11页:问诊室-默认消息-处理数据 分析数据结构 添加数据类型 转换消息列表 10.00

课件第13页:问诊室-默认消息-渲染消息 传递数据给组件 展示患者卡片 预览图片 通用消息和提

原视频对照 ​

#视频时长
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,方向搞反比样式错乱更难被用户容忍。