跳转到内容

WebSocket 配置详解

本章节详细介绍如何使用 vite-plugin-mock-dev-server 配置和开发 WebSocket Mock 服务。

概述

WebSocket 提供了全双工通信通道,适用于实时聊天、实时数据推送、在线协作等场景。插件通过 ws 选项支持 WebSocket Mock。

基础配置

启用 WebSocket 支持

在插件配置中指定 WebSocket 路径前缀:

vite.config.ts
ts
import { defineConfig } from 'vite'
import { mockDevServerPlugin } from 'vite-plugin-mock-dev-server'

export default defineConfig({
  plugins: [
    mockDevServerPlugin({
      // HTTP Mock 路径前缀
      prefix: ['/api'],
      // WebSocket Mock 路径前缀
      wsPrefix: ['/ws', '/socket.io']
    })
  ]
})

重要

wsPrefix 中的路径不应该同时配置在 vite.config.tsserver.proxy 中,这会导致 WebSocket 冲突。

基本 WebSocket Mock

创建 WebSocket Mock 文件:

mock/chat.mock.ts
ts
import { defineMock } from 'vite-plugin-mock-dev-server'

export default defineMock({
  url: '/ws/chat',
  ws: true,
  setup(wss) {
    // 监听连接事件
    wss.on('connection', (ws, req) => {
      console.log('Client connected:', req.url)

      // 发送欢迎消息
      ws.send(JSON.stringify({
        type: 'system',
        message: 'Welcome to chat!'
      }))

      // 监听消息
      ws.on('message', (data) => {
        const message = JSON.parse(data.toString())
        console.log('Received:', message)

        // 广播给所有客户端
        wss.clients.forEach((client) => {
          if (client.readyState === 1) {
            client.send(JSON.stringify({
              type: 'message',
              data: message
            }))
          }
        })
      })

      // 监听关闭
      ws.on('close', () => {
        console.log('Client disconnected')
      })
    })
  }
})

配置选项详解

ws 选项

启用 WebSocket 支持:

ts
export default defineMock({
  url: '/ws/endpoint',
  ws: true, // 启用 WebSocket
  setup(wss, options) {
    // WebSocket 服务器逻辑
  }
})

setup 函数

WebSocket 服务器的核心配置函数:

ts
function setup(wss, options): void

参数说明:

  • wss: WebSocketServer - WebSocket 服务器实例
  • options: 包含 onCleanup 清理回调注册函数

wss (WebSocketServer)

WebSocket 服务器实例,提供以下功能:

属性/方法类型说明
clientsSet<WebSocket>所有连接的客户端
on(event, callback)Function监听事件
emit(event, data)Function触发事件

options.onCleanup

注册清理回调,在热更新时执行:

ts
export default defineMock({
  url: '/ws/chat',
  ws: true,
  setup(wss, { onCleanup }) {
    const interval = setInterval(() => {
      // 定期广播心跳
      wss.clients.forEach((client) => {
        if (client.readyState === 1) {
          client.send(JSON.stringify({ type: 'heartbeat' }))
        }
      })
    }, 30000)

    // 注册清理函数
    onCleanup(() => {
      clearInterval(interval)
      console.log('Cleanup WebSocket resources')
    })
  }
})

WebSocket 事件

服务器事件

connection 事件

新客户端连接时触发:

ts
wss.on('connection', (ws, req) => {
  // ws: WebSocket 实例
  // req: HTTP 请求对象

  console.log('New connection from:', req.socket.remoteAddress)
  console.log('URL:', req.url)
  console.log('Headers:', req.headers)
})

客户端事件

message 事件

收到客户端消息时触发:

ts
ws.on('message', (data, isBinary) => {
  if (isBinary) {
    // 处理二进制数据
    console.log('Binary data received:', data)
  }
  else {
    // 处理文本数据
    const text = data.toString()
    console.log('Text message:', text)
  }
})

close 事件

客户端断开连接时触发:

ts
ws.on('close', (code, reason) => {
  console.log(`Connection closed: ${code} - ${reason}`)
})

error 事件

发生错误时触发:

ts
ws.on('error', (error) => {
  console.error('WebSocket error:', error)
})

实际应用场景

场景 1:实时聊天室

mock/chat-room.mock.ts
ts
import { defineMock, defineMockData } from 'vite-plugin-mock-dev-server'

interface ChatMessage {
  id: string
  userId: string
  username: string
  content: string
  timestamp: number
}

// 存储聊天记录
const chatHistory = defineMockData<ChatMessage[]>('chat-history', [])

// 存储在线用户
const onlineUsers = defineMockData<Map<string, { username: string, joinTime: number }>>
('online-users', new Map())

export default defineMock({
  url: '/ws/chat-room',
  ws: true,
  setup(wss, { onCleanup }) {
    // 广播消息给所有客户端
    const broadcast = (message: any, excludeWs?: WebSocket) => {
      const data = JSON.stringify(message)
      wss.clients.forEach((client) => {
        if (client !== excludeWs && client.readyState === 1) {
          client.send(data)
        }
      })
    }

    wss.on('connection', (ws, req) => {
      let userId: string | null = null

      ws.on('message', (data) => {
        try {
          const message = JSON.parse(data.toString())

          switch (message.type) {
            case 'join':
              userId = message.userId
              onlineUsers.value.set(userId, {
                username: message.username,
                joinTime: Date.now()
              })

              // 发送历史记录
              ws.send(JSON.stringify({
                type: 'history',
                data: chatHistory.value.slice(-50) // 最近50条
              }))

              // 广播用户加入
              broadcast({
                type: 'user-joined',
                userId,
                username: message.username,
                onlineCount: onlineUsers.value.size
              })
              break

            case 'message': {
              if (!userId)
                return

              const chatMessage: ChatMessage = {
                id: generateId(),
                userId,
                username: message.username,
                content: message.content,
                timestamp: Date.now()
              }

              // 保存到历史
              chatHistory.value = [...chatHistory.value, chatMessage]

              // 广播消息
              broadcast({
                type: 'new-message',
                data: chatMessage
              })
              break
            }

            case 'typing': {
              broadcast({
                type: 'user-typing',
                userId,
                username: message.username
              }, ws)
              break
            }
          }
        }
        catch (error) {
          ws.send(JSON.stringify({
            type: 'error',
            message: 'Invalid message format'
          }))
        }
      })

      ws.on('close', () => {
        if (userId) {
          const user = onlineUsers.value.get(userId)
          onlineUsers.value.delete(userId)

          broadcast({
            type: 'user-left',
            userId,
            username: user?.username,
            onlineCount: onlineUsers.value.size
          })
        }
      })
    })

    // 清理
    onCleanup(() => {
      onlineUsers.value.clear()
    })
  }
})

function generateId(): string {
  return Math.random().toString(36).substring(2, 15)
}

场景 2:实时数据推送

mock/realtime-data.mock.ts
ts
import { defineMock } from 'vite-plugin-mock-dev-server'

export default defineMock({
  url: '/ws/realtime',
  ws: true,
  setup(wss, { onCleanup }) {
    // 模拟实时数据
    const generateMetrics = () => ({
      cpu: Math.random() * 100,
      memory: Math.random() * 100,
      network: Math.random() * 1000,
      timestamp: Date.now()
    })

    // 定期推送数据
    const interval = setInterval(() => {
      const data = generateMetrics()
      const message = JSON.stringify({
        type: 'metrics',
        data
      })

      wss.clients.forEach((client) => {
        if (client.readyState === 1) {
          client.send(message)
        }
      })
    }, 1000)

    wss.on('connection', (ws) => {
      // 立即发送当前数据
      ws.send(JSON.stringify({
        type: 'metrics',
        data: generateMetrics()
      }))
    })

    onCleanup(() => {
      clearInterval(interval)
    })
  }
})

场景 3:在线协作编辑

mock/collaboration.mock.ts
ts
import { defineMock, defineMockData } from 'vite-plugin-mock-dev-server'

interface Operation {
  id: string
  userId: string
  type: 'insert' | 'delete'
  position: number
  content?: string
  timestamp: number
}

const documentContent = defineMockData<string>('doc-content', '')
const operations = defineMockData<Operation[]>('operations', [])

export default defineMock({
  url: '/ws/collaboration/:docId',
  ws: true,
  setup(wss) {
    wss.on('connection', (ws, req) => {
      const docId = req.url?.split('/').pop()

      // 发送当前文档内容
      ws.send(JSON.stringify({
        type: 'init',
        content: documentContent.value,
        docId
      }))

      ws.on('message', (data) => {
        const message = JSON.parse(data.toString())

        if (message.type === 'operation') {
          const op: Operation = {
            id: generateId(),
            userId: message.userId,
            type: message.operation.type,
            position: message.operation.position,
            content: message.operation.content,
            timestamp: Date.now()
          }

          // 应用操作
          applyOperation(op)
          operations.value = [...operations.value, op]

          // 广播给其他客户端
          wss.clients.forEach((client) => {
            if (client !== ws && client.readyState === 1) {
              client.send(JSON.stringify({
                type: 'operation',
                data: op
              }))
            }
          })
        }
      })
    })

    function applyOperation(op: Operation) {
      const content = documentContent.value
      if (op.type === 'insert' && op.content) {
        documentContent.value
          = content.slice(0, op.position)
            + op.content
            + content.slice(op.position)
      }
      else if (op.type === 'delete') {
        documentContent.value
          = content.slice(0, op.position)
            + content.slice(op.position + (op.content?.length || 1))
      }
    }

    function generateId(): string {
      return Date.now().toString(36) + Math.random().toString(36).substr(2)
    }
  }
})

客户端使用示例

原生 WebSocket

client.ts
ts
const ws = new WebSocket('ws://localhost:5173/ws/chat')

// 连接建立
ws.onopen = () => {
  console.log('Connected')

  // 发送加入消息
  ws.send(JSON.stringify({
    type: 'join',
    userId: 'user-123',
    username: 'John'
  }))
}

// 接收消息
ws.onmessage = (event) => {
  const message = JSON.parse(event.data)

  switch (message.type) {
    case 'history':
      console.log('Chat history:', message.data)
      break
    case 'new-message':
      console.log('New message:', message.data)
      break
    case 'user-joined':
      console.log(`${message.username} joined`)
      break
  }
}

// 连接关闭
ws.onclose = () => {
  console.log('Disconnected')
}

// 发送消息
function sendMessage(content: string) {
  ws.send(JSON.stringify({
    type: 'message',
    username: 'John',
    content
  }))
}

使用 Socket.io

如果使用 Socket.io 客户端:

ts
import { io } from 'socket.io-client'

const socket = io('ws://localhost:5173', {
  path: '/ws/chat'
})

socket.on('connect', () => {
  console.log('Connected')
})

socket.on('new-message', (data) => {
  console.log('New message:', data)
})

socket.emit('message', {
  username: 'John',
  content: 'Hello!'
})

调试技巧

开启调试日志

ts
mockDevServerPlugin({
  log: 'debug'
})

使用 Chrome DevTools

  1. 打开 Chrome DevTools
  2. 切换到 Network 面板
  3. 选择 WS (WebSocket) 过滤器
  4. 查看 WebSocket 连接和消息

命令行测试

使用 wscat 工具测试:

bash
# 安装 wscat
npm install -g wscat

# 连接 WebSocket
wscat -c ws://localhost:5173/ws/chat

# 发送消息
> {"type":"join","userId":"1","username":"test"}

注意事项

  1. 连接数限制: 浏览器对同一域名的 WebSocket 连接数有限制(通常 6 个)
  2. 心跳机制: 建议实现心跳检测,防止连接被中间件断开
  3. 错误处理: 始终添加错误处理,避免未捕获的异常
  4. 资源清理: 使用 onCleanup 清理定时器和资源
  5. 消息格式: 建议统一使用 JSON 格式,便于处理

常见问题

Q: WebSocket 连接失败?

检查以下几点:

  1. wsPrefix 配置是否正确
  2. 确保没有在 vite.config.tsproxy 中配置相同路径
  3. 使用 ws://wss:// 协议,不是 http://

Q: 如何广播消息?

ts
wss.clients.forEach((client) => {
  if (client.readyState === 1) { // 确保连接打开
    client.send(message)
  }
})

Q: 如何获取客户端信息?

ts
wss.on('connection', (ws, req) => {
  console.log('IP:', req.socket.remoteAddress)
  console.log('URL:', req.url)
  console.log('Headers:', req.headers)
})

基于 MIT 许可发布