深入了解Nest自定义装饰器

4,775 阅读7分钟

装饰器

在刚刚开篇的时候 ,我们就专门针对装饰器做了了解,这是一个JS开发的高级概念,也是IoC的基础概念之一。在Nest中,装饰器被频繁地使用到,至此,我们所用到的装饰器都是内建的装饰器,例如:请求方法GetPut等,还有管道、守卫装饰器UseGuardUsePipes等。在这一章节中,我们着重来了解Nest装饰器的应用,其中包括总结性的了解一下所有Nest的装饰器,以及如何使用自定义装饰器。

Nest内建装饰器

Nest的装饰器可以分为三类:核心类、Http类和模块类。

核心装饰器

  • 异常过滤器装饰器@Catch:用于声明一个普通的类为异常过滤器;仅用于类装饰;
  • 控制器装饰器@Controller:用于著名某个类为控制器;仅用于类装饰;
  • 绑定异常过滤器装饰器@UseFilters:用于将某个控制器类或者方法与制定的异常过滤器关联;
  • 显式依赖声明装饰器@Inject:在构造函数中需要被注入的依赖项,显式声明类型;
  • 被依赖装饰器@Injectable:用于将类型交由Nest托管,并在必要时注入依赖类;
  • 可选项装饰器@Optional:用于声明某个参数为可选;仅用于参数装饰;
  • 设置源数据装饰器@SetMetadata:用于设置对象的源数据信息,K V值;
  • 绑定守卫装饰器@UseGuards:用于为类或者方法绑定守卫;
  • 绑定拦截器装饰器@UseInterceptors:用于为类或者方法绑定制定拦截器;
  • 绑定管道装饰器@UsePipes:用于为某个方法或者类数绑定管道;

Http相关的装饰器

  • 自定义参数装饰器@createParamDecorator:严格意义上来说,它并不是一种装饰器,而是装饰器模板,本章后续再具体讲解;
  • 限制响应头装饰器@Header:用于限制响应数据中Header的字段值;仅用于方法装饰;
  • Http状态码装饰器@HttpCode:用户方法返回的Http状态码;仅用于方法装饰;
  • 重定向装饰器@Redirect:用于将请求重定向到其他资源上;仅用于方法装饰;
  • 渲染装饰器@Render:用于指明请求方法需要渲染的HTML模板;仅用于方法装饰;
  • 请求映射装饰器@RequestMapping:请求路径映射的基础方法,有这个方法加上不同的请求方式GetPost等,扩展出@Get@Post等装饰器。仅用于方法装饰;
  • 路由参数装饰,以createRouteParamDecorator为基础函数,扩展出@Request@Response@Next@Ip@Session@Headers装饰器,用于提取底层平台等相关参数或对象。此处的@Header装饰器与上文的不同,这里所有的装饰器均限于参数装饰器;
  • 管道路由参数装饰器,以createPipesRouteParamDecorator为基础函数,扩展出@Query@Body@Param@HostParam装饰器,用于提取请求对象中的相关参数(数据)的装饰器;
  • 文件上传装饰器@UploadFile:以Multer为逻辑内核,以createPipesRouteParamDecorator为基础函数的装饰器,用于处理上传文件;
  • SSE装饰器@Sse:用于将某个方法定义为SSE方法的装饰器;

模块装饰器

  • 全局装饰器@Global:定义某个类为全局范围内有效;
  • 模块装饰器@Module:定义某个普通类为模块属性;

自定义装饰器

在Nest框架中,有两类自定义装饰器,一种是开篇将Nest命令行的时候有提到generator命令中有一个decorator的创建自定义装饰器的命令;还有一种是专门用于HTTP下的路由参数装饰器。利用用命令创建的装饰器,主要是用于设置对象的元数据,它可以用于类、方法或者参数,而后者仅可以用于参数装饰。我们先来看看用命令行nest g d ID生成的装饰器源码:

import { SetMetadata } from '@nestjs/common';

export const Id = (...args: string[]) => SetMetadata('id', args);

以下是createParamDecorator函数,仅用于定义HTTP请求中的路由参数。以下是源码:

export type CustomParamFactory<TData = any, TInput = any, TOutput = any> = (
  data: TData,
  input: TInput,
) => TOutput;

export function createParamDecorator<
  FactoryData = any,
  FactoryInput = any,
  FactoryOutput = any
>(
  factory: CustomParamFactory<FactoryData, FactoryInput, FactoryOutput>,
  enhancers: ParamDecoratorEnhancer[] = [],
): (
  ...dataOrPipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
) => ParameterDecorator {
  const paramtype = uuid();
  return (
    data?,
    ...pipes: (Type<PipeTransform> | PipeTransform | FactoryData)[]
  ): ParameterDecorator => (target, key, index) => {
    const args =
      Reflect.getMetadata(ROUTE_ARGS_METADATA, target.constructor, key) || {};

    const isPipe = (pipe: any) =>
      pipe &&
      ((isFunction(pipe) &&
        pipe.prototype &&
        isFunction(pipe.prototype.transform)) ||
        isFunction(pipe.transform));

    const hasParamData = isNil(data) || !isPipe(data);
    const paramData = hasParamData ? (data as any) : undefined;
    const paramPipes = hasParamData ? pipes : [data, ...pipes];

    Reflect.defineMetadata(
      ROUTE_ARGS_METADATA,
      assignCustomParameterMetadata(
        args,
        paramtype,
        index,
        factory,
        paramData,
        ...(paramPipes as PipeTransform[]),
      ),
      target.constructor,
      key,
    );
    enhancers.forEach(fn => fn(target, key, index));
  };
}

TS还不是特别精通的同学看到这段代码可能有点晕了(不管怎样,还是建议同学们细品之)。createParamDecorator方法类型限制为返回一个参数装饰器的闭包。从上述代码的const args =...开始时正式逻辑,过程大致分为:读取装饰对象(即被装饰参数)的元数据,判断参数类型,重新写入元数据。不难看出createParamDecorator是一个类似于工厂模式的装饰器产生函数。两者用途有着明显的差别。

创建一个自定义的装饰器

打开利用先前命令行创建的装饰器程序文件,修改一下:

import { SetMetadata } from '@nestjs/common';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const ID = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const id = request.query.id;
    return id;
  },
);

再将其安排到一个路由中:

@Get('a')
idQuery(@ID() id): string {
  console.log(id);
  return this.appService.getHello();
}

眼尖的同学可能已经发现了,这不就是一个管道吗~也确实如此。不过除了管道常规操作外,我们还有SetMetadata方法可以使用。

使用管道

我们创建一个管道nest g pipe Double,随便填充一些代码,例如将输入的值平方一下:

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class DoublePipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value ** 2;
  }
}

将管道应用到这个装饰器中:

  @Get('a')
  idQuery(@ID(new DoublePipe()) id): string {
    console.log(id);
    return this.appService.getHello();
  }

再次访问这个url curl -X GET "http://localhost:3000/a?id=4",控制台输出了平房后的ID值。甚至可以使用多个管道操作:

  @Get('a')
  idQuery(@ID(new DoublePipe(), new DoublePipe()) id): string {
    console.log(id);
    return this.appService.getHello();
  }

自定义装饰器组合多种元素

Nest框架中有个名为applyDecorators方法,可以将多个装饰器组合使用,源码:

export function applyDecorators(
  ...decorators: Array<ClassDecorator | MethodDecorator | PropertyDecorator>
) {
  return <TFunction extends Function, Y>(
    target: TFunction | object,
    propertyKey?: string | symbol,
    descriptor?: TypedPropertyDescriptor<Y>,
  ) => {
    for (const decorator of decorators) {
      if (target instanceof Function && !descriptor) {
        (decorator as ClassDecorator)(target);
        continue;
      }
      (decorator as MethodDecorator | PropertyDecorator)(
        target,
        propertyKey,
        descriptor,
      );
    }
  };
}

可见,其参数只要是装饰器类的,都可以被支持。例如将先前的一些案例都集成进项目后,可以写这样一个组合:

import {
  SetMetadata,
  UseFilters,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { applyDecorators } from '@nestjs/common';
import { AllexceptionFilter } from './allexception.filter';
import { AuthGuard } from './auth.guard';
import { ClassInterceptor } from './class.interceptor';

export function Custom(...allow: Array<string>) {
  return applyDecorators(
    SetMetadata('allowHeader', allow),
    UseGuards(AuthGuard),
    UseFilters(AllexceptionFilter),
    UseInterceptors(ClassInterceptor),
  );
}

路由的方法中这样使用:

  @Get()
  @Custom('FireFox', 'Chrome')
  getHello(): string {
    return this.appService.getHello();
  }

由于第一条组合中就设置了对象的元数据,所以在后续的任意一个中间过程中,都可以调取这个元数据。例如,我们在守卫中加入以下代码:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log(
      this.reflector.get<string[]>('allowHeader', context.getHandler()),
    );
    return true;
  }
}

执行到此时,将SetMetadata的元数据给读取出来了。

对先前提到的反射和元数据概念不是特别明白的同学可以参考之前写的关于反射的文章,这里至只针对性的做一下补充说明。

Nest可以分别对类、方法、属性和参数的元数据进行读取和写入。当前,读取的前提条件就是获取上下文(详见过滤器章节)。写入元数据的方法是SetMetadata()方法,它已经封装了根据被装饰对象(类、方法、属性还是参数)的判断以及写入逻辑,源代码如下:

export const SetMetadata = <K = string, V = any>(
  metadataKey: K,
  metadataValue: V,
): CustomDecorator<K> => {
  const decoratorFactory = (target: object, key?: any, descriptor?: any) => {
    if (descriptor) {
      Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
      return descriptor;
    }
    Reflect.defineMetadata(metadataKey, metadataValue, target);
    return target;
  };
  decoratorFactory.KEY = metadataKey;
  return decoratorFactory;
};

关于更多反射与元数据的内容,可以参考守卫章节。