Nestjs最佳實踐教程(五):自動驗證,序列化與異常處理

pincman1988發表於2022-09-01

注意: 此處文件只起配合作用,為了您的身心愉悅,請看B站影片教程,不要直接略過影片直接看這個文件,這樣你將什麼都看不到?

另,本人在找工作中,希望能有遠端工作匹配(無法去外地),有需要的老闆可以看一下我的個人介紹:pincman.com/about

學習目標

  • 全域性自動資料驗證管道
  • 全域性資料序列化攔截器
  • 全域性異常處理過濾器

檔案結構

本節內容主要聚焦於CoreModule

src/core
├── constants.ts
├── core.module.ts
├── decorators
│   ├── dto-validation.decorator.ts
│   └── index.ts
├── helpers.ts
├── index.ts
├── providers
│   ├── app.filter.ts
│   ├── app.interceptor.ts
│   ├── app.pipe.ts
│   └── index.ts
└── types.ts

應用編碼

本節中用到一個新的Typescript知識點-自定義裝飾器和matedata,詳細使用請檢視我寫的一篇相關文章

裝飾器

新增一個用於為Dto構造metadata資料的裝飾器

// src/core/decorators/dto-validation.decorator.ts
export const DtoValidation = (
    options?: ValidatorOptions & {
        transformOptions?: ClassTransformOptions;
    } & { type?: Paramtype },
) => SetMetadata(DTO_VALIDATION_OPTIONS, options ?? {});

驗證管道

自定義一個全域性的驗證管道(繼承自Nestjs自帶的ValidationPipe管道)

程式碼: src/core/providers/app.pipe.ts

大致驗證流程如下

  1. 獲取要驗證的dto類
  2. 獲取Dto自定義的matadata資料(透過上面的裝飾器定義)
  3. 合併預設驗證選項(透過在CoreModule註冊管道時定義)與matadata
  4. 根據DTO類上設定的type來設定當前的DTO請求型別(‘body’ | ‘query’ | ‘param’ | ‘custom’)
  5. 如果被驗證的DTO設定的請求型別與被驗證的資料的請求型別不是同一種型別則跳過此管道
  6. 合併當前transform選項和自定義選項(驗證後的資料使用class-transfomer`序列化)
  7. 如果dto類的中存在transform靜態方法,則返回撥用進一步transform之後的結果
  8. 重置驗證選項和transform選項為預設

序列化攔截器

預設的序列化攔截器是無法對分頁資料進行處理的,所以自定義的全域性序列化攔截器類重寫serialize方法,以便對分頁資料進行攔截並序列化

// src/core/providers/app.interceptor.ts
serialize(
        response: PlainLiteralObject | Array<PlainLiteralObject>,
        options: ClassTransformOptions,
    ): PlainLiteralObject | PlainLiteralObject[] {
        const isArray = Array.isArray(response);
        if (!isObject(response) && !isArray) return response;
        // 如果是響應資料是陣列,則遍歷對每一項進行序列化
        if (isArray) {
            return (response as PlainLiteralObject[]).map((item) =>
                this.transformToPlain(item, options),
            );
        }
        // 如果是分頁資料,則對items中的每一項進行序列化
        if (
            'meta' in response &&
            'items' in response &&
            Array.isArray(response.items)
        ) {
            return {
                ...response,
                items: (response.items as PlainLiteralObject[]).map((item) =>
                    this.transformToPlain(item, options),
                ),
            };
        }
        // 如果響應是個物件則直接序列化
        return this.transformToPlain(response, options);
    }

異常處理過濾器

Typeorm在找不到模型資料時會丟擲EntityNotFound的異常,而此異常不會被捕獲進行處理,以至於直接丟擲500錯誤,一般在資料找不到時我們需要丟擲的是404異常,所以需要定義一個全域性異常處理的過濾器來進行捕獲並處理.

全域性的異常處理過濾器繼承自Nestjs自帶的BaseExceptionFilter,在自定義的類中定義一個物件屬性,並複寫catch方法以根據此屬性中不同的異常進行判斷處理

// src/core/providers/app.filter.ts
protected resExceptions: Array<
        { class: Type<Error>; status?: number } | Type<Error>
    > = [{ class: EntityNotFoundError, status: HttpStatus.NOT_FOUND }];
catch(exception: T, host: ArgumentsHost) {...}

註冊全域性

CoreModule中分別為全域性的驗證管道,序列化攔截器和異常處理過濾器進行註冊

在註冊全域性管道驗證時傳入預設引數

// src/core/core.module.ts
providers: [
        {
            provide: APP_PIPE,
            useFactory: () =>
                new AppPipe({
                    transform: true,
                    forbidUnknownValues: true,
                    validationError: { target: false },
                }),
        },
        {
            provide: APP_FILTER,
            useClass: AppFilter,
        },
        {
            provide: APP_INTERCEPTOR,
            useClass: AppIntercepter,
        },
    ],
})

邏輯程式碼

  • 對於驗證器需要修改DtoController
  • 對於攔截器需要修改EntityController
  • 對於過濾器需要修改Service

自動序列化

PostEntity為例,比如在顯示文章列表資料的時候為了減少資料量不需要顯示body內容,而單獨訪問一篇文章的時候則需要,這時候可以新增新增一個序列化組post-detail,而為了確定每個模型的欄位在讀取資料時只顯示我們需要的,所以在類前新增一個@Exclude裝飾器

對於物件型別需要透過@Type裝飾器的欄位轉義

示例

// src/modules/content/entities/post.entity.ts
    ...
    @Expose()
    @Type(() => Date)
    @CreateDateColumn({
        comment: '建立時間',
    })
    createdAt!: Date;
    @Expose()
    @Type(() => CategoryEntity)
    @ManyToMany((type) => CategoryEntity, (category) => category.posts, {
        cascade: true,
    })
    @JoinTable()
    categories!: CategoryEntity[];
    @Expose({ groups: ['post-detail'] })
    @Column({ comment: '文章內容', type: 'longtext' })
    body!: string;

然後可以在在控制器中針對有特殊配置的序列化新增@SerializeOptions裝飾器,如序列化組

示例

// src/modules/content/controllers/post.controller.ts
    ...
    @Get(':post')
    @SerializeOptions({ groups: ['post-detail'] })
    async show(
        @Param('post', new ParseUUIDEntityPipe(PostEntity))
        post: string,
    ) {
        return this.postService.detail(post);
    }

自動驗證

為了程式碼簡潔,把所有針對同一模型的DTO類全部放入一個檔案,於是有了以下2個dto檔案

  • src/modules/content/dtos/category.dto.ts
  • src/modules/content/dtos/post.dto.ts

dto檔案中需要傳入自定義驗證引數的類新增@DtoValidation裝飾器,比如@DtoValidation({ groups: ['create'] })

注意的是預設的paramTypebody,所以對於query,需要額外加上type: 'query'

示例

// src/modules/content/dtos/category.dto.ts
@Injectable()
@DtoValidation({ type: 'query' })
export class QueryCategoryDto implements PaginateDto {
...
}

現在可以在控制器中刪除所有的new ValidatePipe(...)程式碼了,因為全域性驗證管道會自行處理

自動處理異常

現在把服務中的findOne等查詢全部改成findOneOrFail等,把丟擲的NotFoundError這些異常去除就可以在typeorm丟擲預設的EntityNotFound異常時就會響應404

示例

// src/modules/content/services/post.service.ts
    async findOne(id: string) {
        const query = await this.getItemQuery();
        const item = await query.where('post.id = :id', { id }).getOne();
        if (!item)
            throw new EntityNotFoundError(PostEntity, `Post ${id} not exists!`);
        return item;
    }
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章