V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lichnow
V2EX  ›  Node.js

Nestjs 最佳实践教程:5 自动验证,序列化与异常处理

  •  
  •   lichnow · 2022-07-11 10:18:18 +08:00 · 2784 次点击
    这是一个创建于 648 天前的主题,其中的信息可能已经有所发展或是发生改变。

    注意: 此处文档只起配合作用,为了您的身心愉悦,请看 B 站视频教程,不要直接略过视频直接看这个文档,这样你将什么都看不到😄

    另,本人在找工作中,希望能有远程工作匹配(无法去外地),有需要的老板可以看一下我的个人介绍: https://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;
        }
    
    1 条回复    2023-01-06 08:59:26 +08:00
    pincmancc
        1
    pincmancc  
       2023-01-06 08:59:26 +08:00
    。。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5004 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 09:39 · PVG 17:39 · LAX 02:39 · JFK 05:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.