欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

nestjs搭建HTTP與WebSocket服務(wù)詳細過程

 更新時間:2022年11月25日 08:27:51   作者:w4ngzhen  
這篇文章主要介紹了nestjs搭建HTTP與WebSocket服務(wù)詳細過程的相關(guān)資料,需要的朋友可以參考下

最近在做一款輕量級IM產(chǎn)品,后端技術(shù)??蚣苁褂昧薾odejs + nestjs作為服務(wù)端。同時,還需要滿足一個服務(wù)同時支持HTTP服務(wù)調(diào)用以及WebSocket服務(wù)調(diào)用,此文主要記錄本次搭建過程,以及基本的服務(wù)端設(shè)計。

基本環(huán)境搭建

node v14.17.5

nestjs 全局命令行工具(npm i -g @nestjs/cli

本文不再詳細介紹nestjs各種概念,請參考:First steps | NestJS - A progressive Node.js framework

直接創(chuàng)建一個Demo項目:

nest new nest-http-socket-demo

目錄劃分設(shè)計

等待項目完成以后(這個過程可能會持續(xù)比較久,因為創(chuàng)建好目錄結(jié)構(gòu)以后還會進行包安裝),結(jié)構(gòu)如下:

nest-http-websocket-demo
├─ .eslintrc.js
├─ .gitignore
├─ .prettierrc
├─ README.md
├─ nest-cli.json
├─ node_modules
│    └─ ... ...
├─ package.json
├─ src
│    ├─ app.controller.spec.ts
│    ├─ app.controller.ts
│    ├─ app.module.ts
│    ├─ app.service.ts
│    └─ main.ts
├─ test
│    ├─ app.e2e-spec.ts
│    └─ jest-e2e.json
├─ tsconfig.build.json
├─ tsconfig.json
└─ yarn.lock

初始的目錄結(jié)構(gòu)可能不太符合我們的期望,我們對目錄結(jié)構(gòu)進行適當?shù)恼{(diào)整。主要分為幾個目錄:

src/common。該目錄存放服務(wù)端和客戶端公共涉及的內(nèi)容。方便后續(xù)拆分出單獨的npm包供服務(wù)端和客戶端公用; src/base。該目錄存放整個服務(wù)需要用到的一些基礎(chǔ)內(nèi)容,譬如攔截器、過濾器等; src/module。后續(xù)存放按照不同的業(yè)務(wù)領(lǐng)域拆分出的子目錄; src/entity。存放數(shù)據(jù)定義等(本項目我們簡化模型,認為數(shù)據(jù)傳輸?shù)慕Y(jié)構(gòu)和服務(wù)中領(lǐng)域數(shù)據(jù)結(jié)構(gòu)一致)。

調(diào)整后的src目錄結(jié)構(gòu)如下:

- src
  ├─ base
  ├─ common
  ├─ entity
  └─ module

基礎(chǔ)類型定義

在規(guī)劃API之前,我們先設(shè)計定義一些服務(wù)端基本數(shù)據(jù)結(jié)構(gòu)。

服務(wù)端響應(yīng)封裝(ServerResponseWrapper)

眾所周知,一般的服務(wù)端都會對原始返回數(shù)據(jù)進行一定的包裝,增加返回碼、錯誤消息等來明確的指出具體的錯誤內(nèi)容,在我們的服務(wù)也不例外。于是,我們設(shè)計如下的結(jié)構(gòu)體:

export interface ServerResponseWrapper {
    /**
     * 服務(wù)端返回碼
     */
    returnCode: string;
    /**
     * 錯誤信息(如有,例如返回碼非成功碼)
     */
    errorMessage?: string;
    /**
     * 返回數(shù)據(jù)(如有)
     */
    data?: any;
}

對于該結(jié)構(gòu)來說,后續(xù)客戶端也會使用相同的數(shù)據(jù)結(jié)構(gòu)進行解析,所以我們可以考慮將該文件放在src/common中。

下面是一些常見的返回數(shù)據(jù)(純樣例):

// 獲取用戶基本信息成功
{
    "returnCode": "SUC00000",
    "data": {
        "username": "w4ngzhen",
        "lastLoginTime": "2022-11-22 11:50:22.000"
    }
}
// 獲取用戶名稱出錯(沒有提供對應(yīng)的userId)
{
    "returnCode": "ERR40000",
    "errorMessage": "user id is empty.",
}
// 獲取服務(wù)端時間
{
    "returnCode": "SUC0000",
    "data": "2022-11-22 11:22:33.000"
}

返回碼定義(ReturnCode)

為了統(tǒng)一返回碼,我們在定義了一個ReturnCode實體類,由該類統(tǒng)一封裝返回碼。作為外部會涉及了解到的內(nèi)容,我們也將該類放置于src/common中,且導(dǎo)出常用的錯誤碼,代碼如下:

export class ReturnCode {

    private readonly _preCode: 'SUC' | 'ERR';
    private readonly _subCode: string;

    private readonly _statusCode: number;

    get codeString(): string {
        return `${this._preCode}${this._subCode}`;
    }

    get statusCode(): number {
        return this._statusCode;
    }

    constructor(prefix: 'SUC' | 'ERR', subCode: string, statusCode: number) {
        this._preCode = prefix;
        this._subCode = subCode;
        this._statusCode = statusCode;
    }
}

export const SUCCESS = new ReturnCode('SUC', '00000', 200);
export const ERR_NOT_FOUND = new ReturnCode('ERR', '40400', 404);

服務(wù)業(yè)務(wù)異常(BizException)

為了便于在服務(wù)調(diào)用過程中,能夠按照具體的業(yè)務(wù)層面進行異常拋出。我們定義一個名為BizException的類來封裝業(yè)務(wù)異常。對于外部系統(tǒng)來說,該異常并不可見,所以我們把該類放置于src/base中:

import {ReturnCode} from "../common/return-code";

export class BizException {

    private readonly _errorCode: ReturnCode;
    private readonly _errorMessage: string;

    get errorCode(): ReturnCode {
        return this._errorCode;
    }

    get errorMessage(): string {
        return this._errorMessage;
    }

    protected constructor(errorEntity: ReturnCode, errorMessage: string) {
        this._errorMessage = errorMessage;
        this._errorCode = errorEntity;
    }

    static create(errEntity: ReturnCode, errMessage?: string): BizException {
        return new BizException(errEntity, errMessage);
    }
}

接下來,我們?yōu)榉?wù)器規(guī)劃兩個API,分別體現(xiàn)HTTP服務(wù)和WebSocket服務(wù)。

HTTP服務(wù)開發(fā)

基礎(chǔ)服務(wù)

首先,我們設(shè)計一個簡單用戶信息查詢服務(wù)接口。該接口可以根據(jù)傳遞而來的用戶ID(userId)返回對應(yīng)的用戶信息:

GET /users?userId=${userId}

為了實現(xiàn)上述接口,我們按照如下流程進行API搭建:

在src/entity目錄中,我們創(chuàng)建一個user目錄,并在其中創(chuàng)建user.dto.ts文件專門用于定義用戶User這個數(shù)據(jù)傳輸結(jié)構(gòu),內(nèi)容如下:

// src/entity/user/user.dto.ts
export interface UserDto {
    userId: string;
    username: string;
    age: number;
}

在src/module創(chuàng)建一個user目錄,劃分用戶user相關(guān)業(yè)務(wù)領(lǐng)域內(nèi)容。同時,在其中創(chuàng)建user.service.ts,存放處理用戶的相關(guān)服務(wù)代碼,內(nèi)容如下:

// src/module/user/user.service.ts
import {Injectable} from '@nestjs/common';
import {UserDto} from "../../entity/user/user.dto";

@Injectable()
export class UserService {

    async getUserById(userId: string): Promise<UserDto> {
        // 測試數(shù)據(jù)
        const demoData: UserDto[] = [
            {
                userId: 'tom',
                username: 'Tom',
                age: 10
            },
            {
                userId: 'jerry',
                username: 'Jerry',
                age: 11
            }
        ];

        return demoData.find(u => u.userId === userId);
    }
}

同樣的,我們在src/module/user中創(chuàng)建User的Controller(user.controller.ts),增加GET /users接口,請求參數(shù)并調(diào)用服務(wù):

import {Controller, Get, Param, Query} from '@nestjs/common';
import {UserService} from './user.service';
import {UserDto} from "../../entity/user/user.dto";

@Controller("users")
export class UserController {
    constructor(private readonly userService: UserService) {
    }

    @Get()
    async getHello(@Query('userId') userId: string): Promise<UserDto> {
        return this.userService.getUserById(userId);
    }
}

創(chuàng)建用戶模塊,將controller、service注冊到用戶模塊中(src/module/user/user.module.ts):

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

將用戶模塊注冊給全局總模塊app.module.ts中:

 import { AppService } from './app.service';
+import {UserModule} from "./module/user/user.module";

 @Module({
-  imports: [],
+  imports: [UserModule],
   controllers: [AppController],
   providers: [AppService],
 })

完成上述操作以后,我們就可以啟動服務(wù)進行驗證了:

成功響應(yīng)攔截器

上面的接口返回可以看出,Controller返回是什么樣的結(jié)構(gòu)體,前端請求到的數(shù)據(jù)就是什么結(jié)構(gòu),但我們希望將數(shù)據(jù)按照ServerResponseWrapper結(jié)構(gòu)進行封裝。在nestjs中,可以通過實現(xiàn)來自@nestjs/common中的NestInterceptor接口來編寫我們自己的響應(yīng)攔截,統(tǒng)一處理響應(yīng)來實現(xiàn)前面的需求。按照我們之前規(guī)劃,我們首先在src/base中創(chuàng)建interceptor目錄,然后在里面創(chuàng)建http-service.response.interceptor.ts,內(nèi)容如下:

// src/base/interceptor/http-service.response.interceptor.ts
import {CallHandler, ExecutionContext, NestInterceptor} from "@nestjs/common";
import {map, Observable} from "rxjs";
import {ServerResponseWrapper} from "../../common/server-response-wrapper";
import {SUCCESS} from "../../common/return-code";

/**
 * 全局Http服務(wù)響應(yīng)攔截器
 * 該Interceptor在main中通過
 * app.useGlobalInterceptors 來全局引入,
 * 僅處理HTTP服務(wù)成功響應(yīng)攔截,異常是不會進入該攔截器
 */
export class HttpServiceResponseInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext,
              next: CallHandler):
        Observable<any> | Promise<Observable<any>> {
        return next.handle().pipe(map(data => {
            // 進入該攔截器,說明沒有異常,使用成功返回
            const resp: ServerResponseWrapper = {
                returnCode: SUCCESS.codeString,
                data: data
            };
            return resp;
        }))
    }
}

創(chuàng)建完成后,我們在main入口中,需要將該響應(yīng)攔截器注冊到全局中:

 // src/main.ts
 async function bootstrap() {
   const app = await NestFactory.create(AppModule);
+
+  // 增加HTTP服務(wù)的成功響應(yīng)攔截器
+  app.useGlobalInterceptors(new HttpServiceResponseInterceptor());
+
   await app.listen(3000);
 }
 bootstrap();

完成配置以后,我們可以再次調(diào)用API來查看結(jié)果:

可以看到,盡管我們的Controller返回的是一個實際數(shù)據(jù)結(jié)構(gòu)(Promise也適用),但是經(jīng)過響應(yīng)攔截器的處理,我們完成了對響應(yīng)體的包裹封裝。

異常過濾器

上述我們完成一個調(diào)用,并對響應(yīng)成功的數(shù)據(jù)進行了包裹,但面對異常情況同樣適用嗎?如果不適用又需要如何處理呢?

首先,我們增加一個專門處理字段錯誤的錯誤碼ReturnCode:

// src/common/return-code.ts
 export const SUCCESS = new ReturnCode('SUC', '00000', 200);
+export const ERR_REQ_FIELD_ERROR = new ReturnCode('ERR', '40000', 400);
 export const ERR_NOT_FOUND = new ReturnCode('ERR', '40400', 404);

然后,我們在UserService中適當修改一下getUserById的實現(xiàn),加入userId判空判斷,并在為空的時候,拋出業(yè)務(wù)異常(這個過程我們順便安裝了lodash):

+import * as _ from 'lodash';
+import {BizException} from "../../common/biz-exception";
+import {ERR_REQ_FIELD_ERROR} from "../../common/return-code";

 @Injectable()
 export class UserService {

     async getUserById(userId: string): Promise<UserDto> {
+        if (_.isEmpty(userId)) {
+            throw BizException.create(ERR_REQ_FIELD_ERROR, 'user id is empty');
+        }
         ... ...
     }
}

完成上述修改后,我們嘗試發(fā)請求時候,故意不填寫userId,得到如下的結(jié)果:

可以看到,盡管nestjs幫助我們進行一定的封裝,但是結(jié)構(gòu)體與我們一開始定義的ServerResponseWrapper是不一致的。為了保持一致,我們需要接管nestjs的異常處理,并轉(zhuǎn)換為我們自己的wrapper結(jié)構(gòu),而接管的方式則是創(chuàng)建一個實現(xiàn)ExceptionFilter接口的類(按照路徑劃分,我們將這個類所在文件http-service.exception.filter.ts存放于src/base/filter目錄下):

import {ArgumentsHost, Catch, ExceptionFilter, HttpException} from "@nestjs/common";
import {ServerResponseWrapper} from "../../common/server-response-wrapper";
import {BizException} from "../../common/biz-exception";

/**
 * 全局Http服務(wù)的異常處理,
 * 該Filter在main中通過
 * app.useGlobalExceptionFilter來全局引入,
 * 僅處理HTTP服務(wù)
 */
@Catch()
export class HttpServiceExceptionFilter implements ExceptionFilter {
    catch(exception: any, host: ArgumentsHost): any {
        // 進入該攔截器,說明http調(diào)用中存在異常,需要解析異常,并返回統(tǒng)一處理
        let responseWrapper: ServerResponseWrapper;
        let httpStatusCode: number;
        if (exception instanceof BizException) {
            // 業(yè)務(wù)層Exception
            responseWrapper = {
                returnCode: exception.errorCode.codeString,
                errorMessage: exception.errorMessage
            }
            httpStatusCode = exception.errorCode.statusCode;
        } else if (exception instanceof HttpException) {
            // 框架層的Http異常
            responseWrapper = {
                returnCode: 'IM9009',
                errorMessage: exception.message,
            }
            httpStatusCode = exception.getStatus();
        } else {
            // 其他錯誤
            responseWrapper = {
                returnCode: 'IM9999',
                errorMessage: 'server unknown error: ' + exception.message,
            };
            httpStatusCode = 500;

        }

        // 該攔截器處理HTTP服務(wù)的異常,所以手動切換到HTTP Host
        // 并獲取響應(yīng)response,進行HTTP響應(yīng)的寫入
        const httpHost = host.switchToHttp();
        const response = httpHost.getResponse();
        response.status(httpStatusCode).json(responseWrapper);
    }
}

該類的核心點在于,對捕獲到的異常進行解析后,我們會通過參數(shù)ArgumentsHost來獲取實際的HTTP Host,并從中獲取response對象,調(diào)用相關(guān)支持的方法來控制響應(yīng)response的內(nèi)容(http狀態(tài)碼以及響應(yīng)體內(nèi)容)。

最后,我們依然在main里面進行注冊配置:

+import {HttpServiceExceptionFilter} from "./base/filter/http-service.exception.filter";

 async function bootstrap() {
   const app = await NestFactory.create(AppModule);

   // 增加HTTP服務(wù)的成功響應(yīng)攔截器
   app.useGlobalInterceptors(new HttpServiceResponseInterceptor());
+  // 增加HTTP服務(wù)的異常過濾器,進行響應(yīng)包裹
+  app.useGlobalFilters(new HttpServiceExceptionFilter());

   await app.listen(3000);
 }

完成開發(fā)配置以后,我們重啟服務(wù),通過調(diào)用接口可以看到對應(yīng)異常返回:

WebSocket服務(wù)

在nestjs中想要集成WebSocket服務(wù)也很容易。

首先,我們使用一個裝飾器@WebSocketGateway()來表明一個類是一個WebSocket的網(wǎng)關(guān)(Gateway),這個裝飾器可以指定WebSocket服務(wù)的端口等信息。通常情況下,我們可以設(shè)置與HTTP服務(wù)不一樣的端口,這樣我們就可以在一個臺服務(wù)上通過不同的端口暴露HTTP和WebSocket服務(wù)。當然,這不是必須,只是為了更好的區(qū)分服務(wù)。

其次,我們需要明白在nestjs可以使用ws或者socket.io兩種具體實現(xiàn)的websocket平臺。什么是具體平臺?簡單來講,nestjs只負責設(shè)置一個標準的WebSocket網(wǎng)關(guān)規(guī)范,提供通用的API、接口、裝飾器等,各個平臺則是根據(jù)nestjs提供的規(guī)范進行實現(xiàn)。在本例中,我們選擇使用socket.io作為nestjs上WebSocket具體的實現(xiàn),因為socket.io是一個比較著名websocket庫,同時支持服務(wù)端和客戶端,并且在客戶端/服務(wù)端均內(nèi)建支持了"請求 - 響應(yīng)"一來一回機制。

前置準備

依賴安裝

nestjs中的websocket是一個獨立的模塊,且我們選取了socket.io作為websocket的實現(xiàn),所以我們需要首先安裝一下的基礎(chǔ)模塊:

yarn add @nestjs/websockets @nestjs/platform-socket.io

網(wǎng)關(guān)創(chuàng)建

websocket的相關(guān)內(nèi)容,我們同樣作為一種模塊進行編寫。于是,我們在src/module/目錄中創(chuàng)建websocket文件夾,并在里面創(chuàng)建一個文件:my-websocket.gateway.ts,編寫WS網(wǎng)關(guān)MyWebSocketGateway類的內(nèi)容:

import {WebSocketGateway} from "@nestjs/websockets";

@WebSocketGateway(4000, {
    transports: ['websocket']
})
export class MyWebSocketGateway {

}

一個簡單的WebSocket網(wǎng)關(guān)就創(chuàng)建完成了。我們首先設(shè)定了WebSocket服務(wù)的端口號為4000(與HTTP服務(wù)的3000隔離開);其次,需要特別提一下transports參數(shù),可選擇的transport有兩種:

polling(HTTP長連接輪詢)

該機制由連續(xù)的 HTTP 請求組成:

長時間運行的請求,用于從服務(wù)器接收數(shù)據(jù)GET 短運行請求,用于將數(shù)據(jù)發(fā)送到服務(wù)器POST

由于傳輸?shù)男再|(zhì),連續(xù)的發(fā)出可以在同一 HTTP 請求中連接和發(fā)送。

也就是說,polling本質(zhì)上是利用HTTP請求+輪詢來完成所謂的雙工通訊,在某些古老的沒有實現(xiàn)真正WebSocket協(xié)議的瀏覽器作為一種實現(xiàn)方案。

websocket(網(wǎng)絡(luò)套接字)

WebSocket 傳輸由WebSocket 連接組成,該連接在服務(wù)器和客戶端之間提供雙向和低延遲的通信通道。這是真正的長連接雙工通訊協(xié)議。

所以,在通訊的過程中,服務(wù)端與客戶端要保持相匹配的傳輸協(xié)議。

模塊創(chuàng)建注冊

同樣的,我們在src/module/websocket中創(chuàng)建一個my-websocket.module.ts文件,內(nèi)容如下:

import {MyWebSocketGateway} from "./my-websocket.gateway";
import {Module} from "@nestjs/common";

@Module({
    providers: [MyWebSocketGateway]
})
export class MyWebSocketModule {

}

主要內(nèi)容是將MyWebSocketGateway注冊到模塊中。

最后我們將MyWebSocket模塊注冊到根模塊中:

+import {MyWebSocketModule} from "./module/websocket/my-websocket.module";

 @Module({
-  imports: [UserModule],
+  imports: [UserModule, MyWebSocketModule],
   controllers: [AppController],
   providers: [AppService],
 })
export class AppModule {}

基礎(chǔ)服務(wù)

我們先設(shè)定這樣一個場景:客戶端連接上WebSocket服務(wù)后,可以給服務(wù)端發(fā)送一份JSON數(shù)據(jù)(內(nèi)容加下方),服務(wù)端校驗該數(shù)據(jù)后,在控制臺打印數(shù)據(jù)。

{
    "name": "w4ngzhen"
}

對于服務(wù)端來說,我們首先需要訂閱事件(subscribe),假設(shè)發(fā)送JSON數(shù)據(jù)的事件為hello,那么我們可以通過如下的方式來進行訂閱:

export class MyWebSocketGateway {

    @SubscribeMessage('hello')
    hello(@MessageBody() reqData: { name: string }) {
        if (!reqData || !reqData.name) {
            throw BizException.create(ERR_REQ_FIELD_ERROR, 'data is empty');
        }
        console.log(JSON.stringify(reqData));
    }
    
}

測試WebSocket,可以使用postman來進行,只需要創(chuàng)建個一WebSocket的請求,在postman中按下CTRL+N(macOS為command+N),可以選擇WebSocket請求:

創(chuàng)建后,需要注意,由于我們nestjs集成的WebSocket實現(xiàn)使用的socket.io,所以客戶端需要匹配對應(yīng)的實現(xiàn)(這點主要是為了匹配”請求-響應(yīng)“一來一回機制)

完成配置后,我們可以采用如下的步驟進行事件發(fā)送:

發(fā)送完成后,就會看到postman的打印和nodejs服務(wù)控制臺的打印,符合我們的預(yù)期:

當然,我前面提到過socket.io支持事件一來一回的請求響應(yīng)模式。在nestjs中的WebSocket網(wǎng)關(guān),只需要在對應(yīng)的請求返回值即可:

     @SubscribeMessage('hello')
     hello(@MessageBody() reqData: { name: string }) {
         if (!reqData || !reqData.name) {
             throw BizException.create(ERR_REQ_FIELD_ERROR, 'data is empty');
         }
         console.log(JSON.stringify(reqData));
+        return 'received reqData';
     }

在postman的地方,我們需要發(fā)送的時候勾選上Acknowledgement

完成以后,我們重新連接服務(wù)并發(fā)送數(shù)據(jù),就可以看到一條完整的事件處理鏈路了:

至此,我們就完成了在Nestjs集成一個基礎(chǔ)的WebSocket服務(wù)了。

當然,我們的工作還沒有結(jié)束。在前面我們對HTTP服務(wù)編寫了成功響應(yīng)攔截器以及異常過濾器,接下來,我們按照同樣的方式編寫WebSocket的相關(guān)處理。

成功響應(yīng)攔截器

對于集成在nestjs中的WebSocket服務(wù),想要編寫并配置一個成功響應(yīng)攔截器并不復(fù)雜,沒有什么坑。

首先,我們仿照著http-service.response.interceptor.ts,編寫一個幾乎完全一樣的ws-service.response.interceptor.ts,與HTTP的成功響應(yīng)攔截器放在相同目錄src/base/interceptor中:

// src/base/interceptor/ws-service.response.interceptor.ts
import {CallHandler, ExecutionContext, NestInterceptor} from "@nestjs/common";
import {map, Observable} from "rxjs";
import {ServerResponseWrapper} from "../../common/server-response-wrapper";
import {SUCCESS} from "../../common/return-code";

/**
 * 全局WebSocket服務(wù)響應(yīng)攔截器
 * 該Interceptor在網(wǎng)關(guān)中通過裝飾器 @UseInterceptors 使用
 * 僅處理WebSocket服務(wù)成功響應(yīng)攔截,異常是不會進入該攔截器
 */
export class WsServiceResponseInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext,
              next: CallHandler):
        Observable<any> | Promise<Observable<any>> {
        return next.handle().pipe(map(data => {
            // 進入該攔截器,說明沒有異常,使用成功返回
            const resp: ServerResponseWrapper = {
                returnCode: SUCCESS.codeString,
                data: data
            };
            return resp;
        }))
    }
}

其次,與HTTP注冊攔截器不同的是,nestjs中注冊WebSocket的攔截器,需要在網(wǎng)關(guān)類上使用裝飾器進行:

+ // 安裝WebSocket成功響應(yīng)攔截器
+ @UseInterceptors(new WsServiceResponseInterceptor())
  @WebSocketGateway(4000, {
      transports: ['websocket']
  })
  export class MyWebSocketGateway {
  ... ...

配置完成以后,我們重啟服務(wù),再次使用postman進行WebSocket事件請求,則會看到經(jīng)過包裝后的響應(yīng)體:

異常過濾器

當然,我們嘗試不發(fā)送任何的數(shù)據(jù)。理論上,則會進入校驗流程不通過的場景,拋出BizException。在實際的發(fā)送中,我們會看到,postman無法接受到異常:

在服務(wù)端會看到一個異常報錯:

對于這個問題,我們的需求是無論是否有異常,都需要使用ServerResponseWrapper進行包裹。與HTTP不同的是,WebSocket的異常過濾器需要實現(xiàn)WsExceptionFilter接口,實現(xiàn)該接口的catch方法:

import {ArgumentsHost, Catch, ExceptionFilter, HttpException, WsExceptionFilter} from "@nestjs/common";
import {ServerResponseWrapper} from "../../common/server-response-wrapper";
import {BizException} from "../../common/biz-exception";

/**
 * 全局WebSocket服務(wù)的異常處理,
 * 該Filter在網(wǎng)關(guān)中通過 使用 @UseFilters 來進行注冊
 * 僅處理WebSocket網(wǎng)關(guān)服務(wù)
 */
@Catch()
export class WsServiceExceptionFilter implements WsExceptionFilter {
    catch(exception: any, host: ArgumentsHost): any {
        // 進入該攔截器,說明http調(diào)用中存在異常,需要解析異常,并返回統(tǒng)一處理
        let responseWrapper: ServerResponseWrapper;
        if (exception instanceof BizException) {
            // 業(yè)務(wù)層Exception
            responseWrapper = {
                returnCode: exception.errorCode.codeString,
                errorMessage: exception.errorMessage
            }
        } else {
            // 其他錯誤
            responseWrapper = {
                returnCode: 'IM9999',
                errorMessage: 'server unknown error: ' + exception.message,
            };
        }
        // 對異常進行封裝以后,需要讓框架繼續(xù)進行調(diào)用處理,才能正確的響應(yīng)給客戶端
        // 此時,需要提取到callback這個函數(shù)
        // 參考:https://stackoverflow.com/questions/61795299/nestjs-return-ack-in-exception-filter
        const callback = host.getArgByIndex(2);
        if (callback && typeof callback === 'function') {
            callback(responseWrapper);
        }
    }
}

這個Filter與HTTP服務(wù)中的異常過濾器差異點主要三點:

1)WebSocket中不存在HTTP狀態(tài)碼且不存在HTTP異常,所以我們只需要解析區(qū)分BizException與非BizException。

2)WebSocket的異常過濾器中,想要繼續(xù)后的數(shù)據(jù)處理,需要在方法返回前,從host中取到第三個參數(shù)對象(索引值為2),該值是一個回調(diào)函數(shù),將處理后的數(shù)據(jù)作為參數(shù),調(diào)用該callback方法,框架才能繼續(xù)處理。—— WebSocket異常過濾器最終返回的關(guān)鍵點。

        // 對異常進行封裝以后,需要讓框架繼續(xù)進行調(diào)用處理,才能正確的響應(yīng)給客戶端
        // 此時,需要提取到callback這個函數(shù)
        // 參考:https://stackoverflow.com/questions/61795299/nestjs-return-ack-in-exception-filter
        const callback = host.getArgByIndex(2);
        if (callback && typeof callback === 'function') {
            callback(responseWrapper);
        }

3)注冊該異常過濾器同樣和WebSocket的響應(yīng)攔截器一樣,需要在網(wǎng)關(guān)類上使用@UseFilters裝飾器。

// 安裝WebSocket成功響應(yīng)攔截器
@UseInterceptors(new WsServiceResponseInterceptor())
+ // 安裝WebSocket異常過濾器
+ @UseFilters(new WsServiceExceptionFilter())
@WebSocketGateway(4000, {
    transports: ['websocket']
})

完成該配置后,我們再次重啟服務(wù),使用postman,可以看到wrapper包裝后的效果:

附錄

本次demo已經(jīng)提交至github

w4ngzhen/nest-http-websocket-demo (github.com)

同時,按照每一階段進行了適配提交:

add: 添加WebSocket異常過濾器并注冊到WebSocket網(wǎng)關(guān)中。
add: 添加WebSocket成功響應(yīng)攔截器并注冊到WebSocket網(wǎng)關(guān)中。
modify: 添加WebSocket的事件響應(yīng)數(shù)據(jù)。
modify: 增減對事件”hello“的處理,并在控制臺打印請求。
add: 創(chuàng)建一個基本的WebSocket網(wǎng)關(guān)以及將網(wǎng)關(guān)模塊進行注冊。
add: 增加nestjs websocket依賴、socket.io平臺實現(xiàn)。
add: 添加HTTP服務(wù)異常過濾器,對異常進行解析并返回Wrapper包裹數(shù)據(jù)。
modify: 修改獲取用戶信息邏輯,加入userId判空檢查。
add: 添加HTTP服務(wù)成功響應(yīng)攔截器,對返回體進行統(tǒng)一Wrapper包裹。
modify: 注冊user模塊到app主模塊。
add: 新增用戶User模塊相關(guān)的dto定義、service、controller以及module。
add: 添加ServerResponseWrapper作為服務(wù)端響應(yīng)數(shù)據(jù)封裝;添加返回碼類,統(tǒng)一定義返回碼;添加業(yè)務(wù)異常類,封裝業(yè)務(wù)異常。
init: 初始化項目結(jié)構(gòu)

我會逐步完善這個demo,接入各種常用的模塊(數(shù)據(jù)庫、Redis、S3-ECS等)。本文是本demo的初始階段,已經(jīng)發(fā)布于1.0版本tag。

到此這篇關(guān)于nestjs搭建HTTP與WebSocket服務(wù)詳細過程的文章就介紹到這了,更多相關(guān)nestjs搭建HTTP與WebSocket服務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 如何在node環(huán)境實現(xiàn)“get數(shù)據(jù)解析”代碼實例

    如何在node環(huán)境實現(xiàn)“get數(shù)據(jù)解析”代碼實例

    這篇文章主要介紹了如何在node環(huán)境實現(xiàn)“get數(shù)據(jù)解析”代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-07-07
  • 20行代碼簡單實現(xiàn)koa洋蔥圈模型示例詳解

    20行代碼簡單實現(xiàn)koa洋蔥圈模型示例詳解

    這篇文章主要為大家介紹了20行代碼簡單實現(xiàn)koa洋蔥圈模型示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • TypeScript開發(fā)Node.js程序的方法

    TypeScript開發(fā)Node.js程序的方法

    這篇文章主要介紹了TypeScript開發(fā)Node.js程序的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-04-04
  • Node.js編寫爬蟲的基本思路及抓取百度圖片的實例分享

    Node.js編寫爬蟲的基本思路及抓取百度圖片的實例分享

    這篇文章主要介紹了Node.js編寫爬蟲的基本思路及抓取百度圖片的實例分享,其中作者提到了需要特別注意GBK轉(zhuǎn)碼的轉(zhuǎn)碼問題,需要的朋友可以參考下
    2016-03-03
  • Nodejs使用SQL模糊查詢的過程詳解

    Nodejs使用SQL模糊查詢的過程詳解

    最近在改一個比較久的項目,是使用nodejs寫的,但是對于長期寫java的后端開發(fā)來說,還是有點難維護,不過不改bug的話,就需要重新開發(fā),所以本文介紹了NodeJs如何使用SQL模糊查詢,需要的朋友可以參考下
    2024-07-07
  • 談?wù)刵ode.js中的模塊系統(tǒng)

    談?wù)刵ode.js中的模塊系統(tǒng)

    這篇文章主要介紹了node.js中的模塊系統(tǒng),幫助大家更好的理解和學習node.js框架,感興趣的朋友可以了解下
    2020-09-09
  • nodemailer郵箱發(fā)送驗證碼的實現(xiàn)

    nodemailer郵箱發(fā)送驗證碼的實現(xiàn)

    郵箱注冊是常見的功能,通常需要發(fā)送郵箱驗證碼驗證,本文就來介紹一下nodemailer郵箱發(fā)送驗證碼的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下
    2023-10-10
  • Node.js 數(shù)據(jù)加密傳輸淺析

    Node.js 數(shù)據(jù)加密傳輸淺析

    這篇文章主要給大家介紹的是Node.js數(shù)據(jù)加密傳輸,本文主要介紹的是明文傳輸,文中通過示例代碼介紹的很詳細,相信對于大家的理解和學習會很有幫助,有需要的朋友們下面來一起學習學習吧。
    2016-11-11
  • 在Node.js中使用Swagger自動生成API接口文檔

    在Node.js中使用Swagger自動生成API接口文檔

    這篇文章主要給大家介紹了如何在Node.js項目中使用 Swagger 來自動生成 API接口文檔,使用生成方式有很多種,本文基于swagger-jsdoc+swagger-ui-express快速實現(xiàn),文中通過代碼示例介紹的非常詳細,需要的朋友可以參考下
    2024-01-01
  • node.js事件循環(huán)機制及與js區(qū)別詳解

    node.js事件循環(huán)機制及與js區(qū)別詳解

    這篇文章主要為大家介紹了node.js事件循環(huán)機制及與js區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-09-09

最新評論