nestjs搭建HTTP與WebSocket服務(wù)詳細過程
最近在做一款輕量級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ù)解析”代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-07-07nodemailer郵箱發(fā)送驗證碼的實現(xiàn)
郵箱注冊是常見的功能,通常需要發(fā)送郵箱驗證碼驗證,本文就來介紹一下nodemailer郵箱發(fā)送驗證碼的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-10-10node.js事件循環(huán)機制及與js區(qū)別詳解
這篇文章主要為大家介紹了node.js事件循環(huán)機制及與js區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09