Skip to content
npm version

WebSocket:前后端双向通信

概述

实现前端与后端的类型安全双向通信,支持前端调用后端方法、后端推送消息到前端,并提供完整的类型推断。

核心功能

  • 前端调用后端:通过 emitter 直接调用后端网关方法
  • 后端推送前端:使用 emitWith 向不同目标发送类型安全的消息
  • 事件监听:前端通过 createListener 监听后端推送的事件
  • 请求响应:支持带 ACK 的请求-响应模式

后端实现

事件定义

事件定义示例:

ts
import { Emit } from 'vtzac/typed-emit';

export class ChatEvents {
  @Emit('welcome')
  welcome(nickname: string) {
    return {
      message: `欢迎 ${nickname}!`,
      timestamp: Date.now(),
    };
  }

  @Emit('message')
  message(text: string) {
    return { text, timestamp: Date.now() };
  }

  @Emit('pong')
  pong() {
    return { message: 'pong', timestamp: Date.now() };
  }
}

网关实现

后端网关示例:

ts
import type { Server, Socket } from 'socket.io';
import {
  ConnectedSocket,
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { emitWith } from 'vtzac/typed-emit';
import { ChatEvents } from './chat-events';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  private readonly events = new ChatEvents();

  @WebSocketServer()
  server!: Server;

  // 新客户端连接时发送欢迎消息
  handleConnection(client: Socket): void {
    const nickname = `用户${client.id.slice(-4)}`;
    emitWith(this.events.welcome, this.events)(nickname).toClient(client);
    // 实际会发送的事件:
    // emit 'welcome' { message: '欢迎 用户1234!', timestamp: 1703123456789 }
  }

  // 心跳检测
  @SubscribeMessage('ping')
  handlePing(@ConnectedSocket() client?: Socket): void {
    emitWith(this.events.pong, this.events)().toClient(client!);
    // 实际会发送的事件:
    // emit 'pong' { message: 'pong', timestamp: 1703123456789 }
  }

  // 广播消息
  @SubscribeMessage('say')
  handleSay(@MessageBody() data: { text: string }): void {
    emitWith(this.events.message, this.events)(data.text).toServer(this.server);
    // 实际会发送的事件:
    // 向所有客户端广播 'message' { text: 'Hello everyone!', timestamp: 1703123456789 }
  }

  // 加入房间
  @SubscribeMessage('joinRoom')
  handleJoinRoom(
    @MessageBody() room: string,
    @ConnectedSocket() client?: Socket
  ) {
    client!.join(room); // 先加入房间
    emitWith(
      this.events.message,
      this.events
    )(`已加入房间 ${room}`).toRoomAll(this.server, room);
    // 实际会发送的事件:
    // 向房间中的所有客户端发送 'message' 事件
  }
}

前端实现

前端调用示例:

ts
import { _socket } from 'vtzac';
import { ChatGateway } from './chat.gateway';
import { ChatEvents } from './chat-events';

// 建立连接
const { emitter, createListener, socket, disconnect } = _socket(
  'http://localhost:3000',
  ChatGateway,
  {
    socketIoOptions: { transports: ['websocket'] },
  }
);

// 调用后端方法
emitter.handlePing();
console.log('发送心跳'); // 输出:发送心跳

emitter.handleSay({ text: 'Hello everyone!' });
console.log('发送消息'); // 输出:发送消息

emitter.handleJoinRoom('room1');
console.log('加入房间'); // 输出:加入房间

// 创建事件监听器
const events = createListener(ChatEvents);

// 监听后端推送的事件
events.pong(data => {
  console.log('收到心跳响应:', data);
  // 输出:收到心跳响应: { message: 'pong', timestamp: 1703123456789 }
});

events.welcome(data => {
  console.log('收到欢迎消息:', data);
  // 输出:收到欢迎消息: { message: '欢迎 用户1234!', timestamp: 1703123456789 }
});

events.message(data => {
  console.log('收到消息:', data);
  // 输出:收到消息: { text: 'Hello everyone!', timestamp: 1703123456789 }
});

// 断开连接
setTimeout(() => disconnect(), 10000);
// 实际会发起的 WebSocket 事件:
// emit 'ping' (无数据)
// emit 'say' { text: 'Hello everyone!' }
// emit 'joinRoom' 'room1'
// 监听事件:'pong', 'welcome', 'message'

高级功能

请求响应模式

后端方法返回值时,前端调用会返回 Promise

后端 ACK 响应示例:

ts
@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('getOnlineCount')
  handleGetOnlineCount() {
    return { count: 42 }; // 返回在线人数
  }
}

前端 ACK 调用示例:

ts
const { emitter } = _socket('http://localhost:3000', ChatGateway);
// 如果有返回值则会自动调整为 emitWithAck 调用
const result = await emitter.handleGetOnlineCount();
console.log('在线人数:', result.count); // 输出:在线人数: 42

房间管理

房间广播示例:

ts
// 发送到所有客户端
emitWith(events.message, events)('全站公告').toServer(server);

// 发送到指定房间
emitWith(events.message, events)('房间公告').toRoomAll(server, 'room1');

命名空间

命名空间网关示例:

ts
@WebSocketGateway({ cors: { origin: '*' }, namespace: '/chat' })
export class ChatGateway {}

前端连接命名空间:

ts
// 自动连接到 /chat 命名空间
const { emitter } = _socket('http://localhost:3000', ChatGateway);

原生 Socket 访问

可以直接访问原生 Socket 实例进行自定义操作:

原生 Socket 使用示例:

ts
import { _socket } from 'vtzac';

const { socket } = _socket('http://localhost:3000', ChatGateway);

// 使用原生 Socket API
socket.on('connect', () => {
  console.log('连接成功'); // 输出:连接成功
});

socket.emit('customEvent', { data: 'test' });
console.log('发送自定义事件'); // 输出:发送自定义事件

小结

  • 类型安全:前后端通信全程保持类型推断和检查
  • 简化开发:避免手写事件名和数据结构,减少错误
  • 双向通信:支持前端调用后端、后端推送前端的完整场景