☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
WebSockets

Adapters (Bộ chuyển đổi)

Bản dịch từ docs.nestjs.com/websockets/adapter

Module WebSockets độc lập với nền tảng (platform-agnostic), do đó bạn có thể mang theo thư viện riêng (hoặc thậm chí một implementation thuần) bằng cách sử dụng interface WebSocketAdapter. Interface này buộc bạn phải implement một vài phương thức được mô tả trong bảng sau:

Ghi chú người dịch Cơ chế này tương tự cách NestJS trừu tượng hóa tầng HTTP server qua HttpAdapter (cho phép bạn chạy cùng một app trên Express hoặc Fastify mà code hầu như không đổi). WebSocketAdapter làm điều tương tự cho tầng WebSocket, nhờ đó Nest có thể hỗ trợ cả socket.io lẫn ws (hoặc bất kỳ thư viện WebSocket nào khác) thông qua cùng một tập decorator và gateway.

create Tạo một socket instance dựa trên các tham số được truyền vào
bindClientConnect Gắn sự kiện kết nối của client
bindClientDisconnect Gắn sự kiện ngắt kết nối của client (tùy chọn*)
bindMessageHandlers Gắn message đến với hàm xử lý message tương ứng
close Kết thúc một server instance

Mở rộng socket.io (Extend socket.io)

Package socket.io được bọc (wrap) trong một class IoAdapter. Điều gì sẽ xảy ra nếu bạn muốn mở rộng chức năng cơ bản của adapter? Ví dụ, yêu cầu kỹ thuật của bạn cần khả năng broadcast sự kiện qua nhiều instance load-balanced của web service. Để làm điều này, bạn có thể extend IoAdapter và override một phương thức duy nhất, có nhiệm vụ khởi tạo các socket.io server mới. Nhưng trước tiên, hãy cài đặt package cần thiết.

Cảnh báo Để dùng socket.io với nhiều instance load-balanced, bạn phải tắt polling bằng cách set transports: ['websocket'] trong cấu hình socket.io của client, hoặc bật cookie based routing trong load balancer. Chỉ dùng Redis thôi là chưa đủ. Xem tại đây để biết thêm thông tin.

$ npm i --save redis socket.io @socket.io/redis-adapter

Sau khi cài đặt package xong, ta có thể tạo class RedisIoAdapter.

import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;

  async connectToRedis(): Promise<void> {
    const pubClient = createClient({ url: `redis://localhost:6379` });
    const subClient = pubClient.duplicate();

    await Promise.all([pubClient.connect(), subClient.connect()]);

    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}

Ghi chú người dịch Vấn đề ở đây giống hệt khi bạn scale horizontal một app Express/Socket.IO thông thường: mỗi instance server chỉ “biết” các client đang kết nối trực tiếp tới nó. Redis adapter dùng cơ chế pub/sub để mọi instance cùng “nghe” được sự kiện broadcast từ các instance khác, nhờ đó server.emit() vẫn tới đúng client dù client A và client B đang kết nối tới hai instance khác nhau sau load balancer.

Sau đó, chỉ cần chuyển sang dùng Redis adapter vừa tạo.

const app = await NestFactory.create(AppModule);
const redisIoAdapter = new RedisIoAdapter(app);
await redisIoAdapter.connectToRedis();

app.useWebSocketAdapter(redisIoAdapter);

Thư viện Ws (Ws library)

Một adapter khác có sẵn là WsAdapter, đóng vai trò như một proxy giữa framework và thư viện ws cực nhanh, được kiểm thử kỹ lưỡng. Adapter này tương thích hoàn toàn với WebSocket gốc của trình duyệt (native browser WebSockets) và nhanh hơn đáng kể so với package socket.io. Tuy nhiên, nó có ít tính năng có sẵn hơn nhiều. Trong một số trường hợp, bạn không thực sự cần đến những tính năng đó.

Ghi chú người dịch Khác biệt cốt lõi giữa ws và socket.io: ws chỉ implement đúng chuẩn WebSocket protocol (không tự động reconnect, không có room/namespace, không fallback sang long-polling), trong khi socket.io xây một custom protocol trên nền WebSocket nên nặng hơn nhưng nhiều tính năng hơn. Vì vậy client dùng thư viện socket.io-client sẽ không kết nối được tới server dùng WsAdapter — bạn cần dùng client WebSocket chuẩn (thư viện ws, hoặc WebSocket API có sẵn của trình duyệt).

Gợi ý Thư viện ws không hỗ trợ namespace (kênh giao tiếp được socket.io phổ biến hóa). Tuy nhiên, để mô phỏng phần nào tính năng này, bạn có thể mount nhiều ws server trên các path khác nhau (ví dụ: @WebSocketGateway({ path: '/users' })).

Để dùng ws, trước tiên ta cần cài đặt package cần thiết:

$ npm i --save @nestjs/platform-ws

Sau khi cài đặt package xong, ta có thể chuyển sang adapter khác:

const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));

Gợi ý WsAdapter được import từ @nestjs/platform-ws.

wsAdapter được thiết kế để xử lý message ở định dạng { event: string, data: any }. Nếu bạn cần nhận và xử lý message ở định dạng khác, bạn sẽ cần cấu hình một message parser để chuyển đổi chúng về định dạng yêu cầu này.

const wsAdapter = new WsAdapter(app, {
  // Để xử lý message ở định dạng [event, data]
  messageParser: (data) => {
    const [event, payload] = JSON.parse(data.toString());
    return { event, data: payload };
  },
});

Ngoài ra, bạn cũng có thể cấu hình message parser sau khi adapter đã được tạo, bằng phương thức setMessageParser.

Nâng cao (custom adapter) (Advanced (custom adapter))

Để minh họa, chúng ta sẽ tích hợp thư viện ws một cách thủ công. Như đã đề cập, adapter cho thư viện này đã được tạo sẵn và được export từ package @nestjs/platform-ws dưới dạng class WsAdapter. Dưới đây là cách mà một implementation đơn giản hóa có thể trông như thế nào:

import WebSocket from 'ws';
import { WebSocketAdapter, INestApplicationContext } from '@nestjs/common';
import { MessageMappingProperties } from '@nestjs/websockets';
import { Observable, fromEvent, EMPTY } from 'rxjs';
import { mergeMap, filter } from 'rxjs/operators';

export class WsAdapter implements WebSocketAdapter {
  constructor(private app: INestApplicationContext) {}

  create(port: number, options: any = {}): any {
    return new WebSocket.Server({ port, ...options });
  }

  bindClientConnect(server, callback: Function) {
    server.on('connection', callback);
  }

  bindMessageHandlers(
    client: WebSocket,
    handlers: MessageMappingProperties[],
    process: (data: any) => Observable<any>,
  ) {
    fromEvent(client, 'message')
      .pipe(
        mergeMap(data => this.bindMessageHandler(data, handlers, process)),
        filter(result => result),
      )
      .subscribe(response => client.send(JSON.stringify(response)));
  }

  bindMessageHandler(
    buffer,
    handlers: MessageMappingProperties[],
    process: (data: any) => Observable<any>,
  ): Observable<any> {
    const message = JSON.parse(buffer.data);
    const messageHandler = handlers.find(
      handler => handler.message === message.event,
    );
    if (!messageHandler) {
      return EMPTY;
    }
    return process(messageHandler.callback(message.data));
  }

  close(server) {
    server.close();
  }
}

Gợi ý Khi bạn muốn tận dụng thư viện ws, hãy dùng WsAdapter có sẵn thay vì tự tạo adapter riêng.

Sau đó, ta có thể thiết lập một custom adapter bằng phương thức useWebSocketAdapter():

const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));

Ví dụ (Example)

Một ví dụ hoạt động sử dụng WsAdapter có sẵn tại đây.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.