学习Nest装饰器实现原理

553 阅读11分钟

一、什么是装饰器?

  1. 装饰器: 就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。
  2. 装饰器可以用于各种不同的场景,例如:
    • 类装饰器:应用于类声明之前,用于修改类的行为或元数据。
    • 方法装饰器:应用于方法声明之前,用于修改方法的行为或元数据。
    • 属性装饰器:应用于属性声明之前,用于修改属性的行为或元数据。
    • 参数装饰器:应用于方法参数声明之前,用于修改参数的行为或元数据。

二、用类装饰器的来简单认识一下如何使用装饰器

class Person {
    @readonly
    name() { return '111' }
}

function readonly(target: any, name: any, descriptor: any) {
    // descriptor对象原来的值如下
    // {
    //   value: specifiedFunction,
    //   enumerable: false,
    //   configurable: true,
    //   writable: true
    // };
    descriptor.writable = false;
    return descriptor;
}

const test = new Person();

console.log(test.name()); // Output: 111

test.name = function() { return '222' }; // 尝试对只读方法进行写操作

console.log(test.name()); // Output: 111,写操作未生效
  1. readonly装饰器接收三个参数:targetname,和descriptor
  2. target是类的原型对象,name是方法的名称,descriptor是该方法的属性描述符对象。
  3. 在装饰器函数中,您将descriptor.writable设置为false,这将禁止对方法进行写操作,使其成为只读方法。
  4. 返回修改后的属性描述符对象descriptor
  5. 我们创建了一个Person类,并在name方法上应用了@readonly装饰器。然后,我们尝试对test.name进行写操作,将其覆盖为一个新的方法。但是,当我们再次调用test.name()时,它仍然返回原始值111,说明写操作未生效,name方法仍然是只读的。

这证明了@readonly装饰器的功能,它成功地将name方法设置为只读,防止对其进行写操作。

三、在 Nest 框架中,装饰器广泛用于实现各种功能和特性。下面是一些常见的装饰器在 Nest 中的使用示例

  1. @Module 装饰器:用于定义一个模块。
@Module({
  imports: [ConfigModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  1. @Controller 装饰器:用于定义一个控制器。
@Controller('users')
export class UsersController {
  // ...
}
  1. @Injectable 装饰器:用于定义一个可注入的服务类。
@Injectable()
export class UserService {
  // ...
}
  1. @Inject 装饰器:用于指定依赖注入的依赖项。
@Injectable()
export class UserService {
  constructor(
    @Inject(LoggerService) private readonly logger: LoggerService,
    private readonly authService: AuthService,
  ) {
    // ...
  }
}
  1. @Get@Post@Put 等 HTTP 方法装饰器:用于定义路由处理方法和路由路径。
@Controller('users')
export class UsersController {
  @Get(':id')
  getUser(@Param('id') id: string): User {
    // ...
  }

  @Post()
  createUser(@Body() createUserDto: CreateUserDto): User {
    // ...
  }
}
  1. @Param@Query@Body 等参数装饰器:用于获取请求中的参数。
@Controller('users')
export class UsersController {
  @Get(':id')
  getUser(@Param('id') id: string): User {
    // ...
  }
}
  1. @UseGuards 装饰器:用于为路由或控制器指定守卫。
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  // ...
}
  1. @UseInterceptors 装饰器:用于为路由或控制器指定拦截器。
@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UsersController {
  // ...
}

四、Nest框架的装饰器可以分为以下三类:核心类装饰器、HTTP类装饰器和模块类装饰器。

  1. 核心类装饰器:

    • @Module():用于标识一个类作为Nest模块,用于组织应用程序中的组件。
    • @Controller():用于标识一个类作为Nest控制器,负责处理传入的HTTP请求。
    • @Injectable():用于标识一个类作为Nest提供者,用于实例化和管理应用程序的依赖项。
    • @Middleware():用于标识一个类作为Nest中间件,用于处理HTTP请求和响应的中间操作。
    • @Guard():用于标识一个类作为Nest守卫,用于执行身份验证和授权逻辑的中间件。
  2. HTTP类装饰器:

    • @Get()@Post()@Put()@Delete()等:用于标识控制器中的方法作为特定HTTP请求方法的处理程序。
    • @Param()@Query()@Body()等:用于标识控制器方法中的参数来源,从URL路径参数、查询参数和请求体中提取值。
    • @Render():用于标识控制器方法返回视图模板的装饰器。
  3. 模块类装饰器:

    • @Imports():用于在模块中导入其他模块。
    • @Providers():用于在模块中定义提供者,以供依赖注入使用。
    • @Controllers():用于在模块中定义控制器。
    • @Exports():用于将模块中的提供者暴露给其他模块。
    • @Global():用于标识一个模块为全局模块,使得其提供者在整个应用程序中可见。

这些装饰器提供了一种声明性的方式来定义和配置Nest应用程序的各个组件。它们通过元编程的方式,使开发人员能够轻松地定义路由、请求处理程序、依赖注入和模块之间的关系。通过合理使用这些装饰器,可以构建出高度可维护和可扩展的Nest应用程序。

五、 从HTTPrequest-mapping.decorator源码文件分析,HTTP类装饰器的实现原理

import { METHOD_METADATA, PATH_METADATA } from '../../constants';
import { RequestMethod } from '../../enums/request-method.enum';

export interface RequestMappingMetadata {
  path?: string | string[];
  method?: RequestMethod;
}

const defaultMetadata = {
  [PATH_METADATA]: '/',
  [METHOD_METADATA]: RequestMethod.GET,
};

export const RequestMapping = (
  metadata: RequestMappingMetadata = defaultMetadata,
): MethodDecorator => {
  const pathMetadata = metadata[PATH_METADATA];
  const path = pathMetadata && pathMetadata.length ? pathMetadata : '/';
  const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;

  return (
    target: object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, requestMethod, descriptor.value);
    return descriptor;
  };
};

const createMappingDecorator =
  (method: RequestMethod) =>
  (path?: string | string[]): MethodDecorator => {
    return RequestMapping({
      [PATH_METADATA]: path,
      [METHOD_METADATA]: method,
    });
  };
export const Post = createMappingDecorator(RequestMethod.POST);
export const Get = createMappingDecorator(RequestMethod.GET);
export const Delete = createMappingDecorator(RequestMethod.DELETE);
export const Put = createMappingDecorator(RequestMethod.PUT);
export const Patch = createMappingDecorator(RequestMethod.PATCH);
export const Options = createMappingDecorator(RequestMethod.OPTIONS);
export const Head = createMappingDecorator(RequestMethod.HEAD);
export const All = createMappingDecorator(RequestMethod.ALL);
export const Search = createMappingDecorator(RequestMethod.SEARCH);

首先,代码中定义了一个接口 RequestMappingMetadata,用于描述路由映射的元数据,其中包含了 path 和 method 两个属性。

然后,定义了一个常量 defaultMetadata,它包含了默认的元数据值,其中 PATH_METADATA 属性默认为 '/'METHOD_METADATA 属性默认为 RequestMethod.GET

接下来,定义了装饰器函数 RequestMapping,它接受一个 metadata 参数,默认值为 defaultMetadata。在函数内部,首先根据传入的元数据获取 path 和 requestMethod。然后,使用 Reflect.defineMetadata 方法将 path 和 requestMethod 分别定义到 descriptor.value(即目标方法)的元数据中。最后,返回该方法的描述符。PS:如果不懂Reflect.defineMetadata的,可以学习参考文献2

接着,定义了一个辅助函数 createMappingDecorator,它接受一个 method 参数,返回一个装饰器函数。该装饰器函数接受一个可选的 path 参数,并调用 RequestMapping 装饰器函数,将 path 和 method 作为元数据传递进去。

最后,定义了一系列具体的路由处理器装饰器,如 PostGetDeletePut 等,它们都是通过调用 createMappingDecorator 辅助函数生成的装饰器。这些装饰器分别将对应的 HTTP 方法和指定的路径作为元数据传递给 RequestMapping 装饰器。

这段代码的作用是简化在 Nest 框架中定义路由处理器的过程。通过使用这些装饰器,开发者可以更加直观地指定处理不同 HTTP 方法的请求的路径,并将它们与特定的方法关联起来。这些装饰器可以应用于控制器类的方法上,用于定义具体的路由处理逻辑。

@Injectable()的源码实现以及作用

export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

装饰器接受一个可选的 options 参数,用于指定注入的作用域。这个装饰器的作用是为类添加一些元数据,以便在运行时进行依赖注入和作用域控制。 依赖注入是如何实现的呢?假设我们有一个 UserService 类,用于处理用户相关的操作,它依赖于一个 UserRepository 类来访问数据库。

class UserRepository {
  getUsers(): User[] {
    // 从数据库获取用户数据
  }

  createUser(user: User): void {
    // 在数据库中创建新用户
  }
}

class UserService {
  private userRepository: UserRepository;

  constructor(userRepository: UserRepository) {
    this.userRepository = userRepository;
  }

  getUsers(): User[] {
    return this.userRepository.getUsers();
  }

  createUser(user: User): void {
    this.userRepository.createUser(user);
  }
}

在传统的方式中,UserService 类需要自己创建 UserRepository 对象:

class UserService {
  private userRepository: UserRepository;

  constructor() {
    this.userRepository = new UserRepository();
  }

  // ...
}

然而,这样的实现会导致 UserService 与 UserRepository 紧密耦合在一起。如果我们想要替换 UserRepository 或在测试中使用模拟对象,就会变得困难。

通过使用依赖注入,我们可以改进代码:

class UserService {
  private userRepository: UserRepository;

  constructor(userRepository: UserRepository) {
    this.userRepository = userRepository;
  }

  // ...
}

现在,UserService 类不再负责创建 UserRepository 对象,而是通过构造函数参数接收它。这意味着我们可以将不同的 UserRepository 实现注入到 UserService 中,实现了解耦和可替换性。

在 NestJS 中,通过使用 @Injectable() 装饰器和依赖注入容器,可以实现类似的依赖注入机制,并通过声明所需的依赖来创建和管理对象。这样,我们可以轻松地编写可测试、可扩展且松耦合的代码。

六、 NestJS 使用了反射(reflection)和元数据(metadata)来识别类的依赖关系。

NestJS 使用了 TypeScript 的装饰器和反射来实现依赖注入的机制,下面是一个示例代码,演示如何使用反射实现依赖注入:

import 'reflect-metadata';

// 定义一个装饰器,用于标记可注入的类
function Injectable() {
  return function (target: any) {
    // 添加元数据,标记该类为可注入的
    Reflect.defineMetadata('injectable', true, target);
  };
}

// 定义一个装饰器,用于标记类的构造函数参数的依赖
function Inject(dependency: any) {
  return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
    // 获取类的构造函数参数的元数据
    const existingDependencies = Reflect.getMetadata('design:paramtypes', target) || [];
    // 将依赖添加到对应的参数索引位置
    existingDependencies[parameterIndex] = dependency;
    // 更新类的构造函数参数的元数据
    Reflect.defineMetadata('design:paramtypes', existingDependencies, target);
  };
}

// 定义一个示例的依赖类
@Injectable()
class LoggerService {
  log(message: string) {
    console.log(message);
  }
}

// 定义一个示例的服务类,依赖于 LoggerService
@Injectable()
class ExampleService {
  constructor(@Inject(LoggerService) private readonly logger: LoggerService) {}

  run() {
    this.logger.log('Executing example...');
  }
}

// 创建一个 ExampleService 实例并运行
const exampleService = new ExampleService();
exampleService.run();

在上述代码中,我们使用了 TypeScript 的装饰器和 Reflect API 来实现反射和元数据的操作。

  • Injectable 装饰器用于标记可注入的类。在装饰器函数中,我们使用 Reflect.defineMetadata 将元数据 'injectable' 添加到类上,以便后续的依赖解析。
  • Inject 装饰器用于标记类的构造函数参数的依赖关系。在装饰器函数中,我们使用 Reflect.getMetadata 获取类的构造函数参数的元数据,并使用 Reflect.defineMetadata 更新元数据,将依赖添加到对应的参数索引位置。

在 ExampleService 的构造函数中,我们使用 @Inject(LoggerService) 装饰器标记了 logger 参数的依赖关系。这告诉 NestJS ExampleService 依赖于 LoggerService

当我们创建 ExampleService 的实例时,NestJS 使用反射和元数据来解析依赖关系。它通过 Reflect.getMetadata 获取 ExampleService 构造函数参数的元数据,并找到 LoggerService 作为依赖项。然后,它实例化 LoggerService,并将其注入到 ExampleService 的构造函数参数中。

通过这种方式,我们使用 TypeScript 的装饰器和 Reflect API 实现了反射和元数据的操作,从而实现了依赖注入的功能。这是 NestJS 在背后使用的机制,使得开发人员可以方便地进行依赖注入的编程。

七、当使用反射和元数据时,可以实现一些其他的功能

  • 如动态路由映射和自定义参数解析器。下面是两个示例代码,演示了如何使用反射和元数据来实现这些功能:
  1. 动态路由映射:
import 'reflect-metadata';

// 定义一个装饰器,用于标记路由处理器类
function Controller(route: string) {
  return function (target: any) {
    // 添加元数据,存储路由路径
    Reflect.defineMetadata('route', route, target);
  };
}

// 定义一个装饰器,用于标记处理方法
function Get(route: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 添加元数据,存储请求方法和路由路径
    Reflect.defineMetadata('method', 'GET', descriptor.value);
    Reflect.defineMetadata('route', route, descriptor.value);
  };
}

// 定义一个示例的路由处理器类
@Controller('/users')
class UsersController {
  @Get('/')
  getUsers() {
    // 处理 GET /users 请求
    console.log('Get Users');
  }
}

// 获取路由处理器类的元数据,并创建路由映射
function createRoutes() {
  const controllers = [UsersController]; // 所有的路由处理器类

  for (const controller of controllers) {
    const routePath = Reflect.getMetadata('route', controller);
    const instance = new controller();

    const methods = Object.getOwnPropertyNames(controller.prototype);
    for (const methodName of methods) {
      const method = instance[methodName];
      const methodRoute = Reflect.getMetadata('route', method);
      const methodType = Reflect.getMetadata('method', method);

      // 创建路由映射
      console.log(`[${methodType}] ${routePath}${methodRoute}`);
    }
  }
}

// 创建路由映射
createRoutes();

在上述代码中,我们定义了 Controller 装饰器和 Get 装饰器,并使用它们来标记路由处理器类和处理方法。这些装饰器使用 Reflect.defineMetadata 添加元数据,以存储路由的路径、请求方法等信息。

createRoutes 函数使用反射和元数据来获取路由处理器类的元数据,并根据元数据创建路由映射。它遍历所有的路由处理器类,获取类和方法的元数据,然后打印出路由映射的信息。

  1. 自定义参数解析器:
import 'reflect-metadata';

// 定义一个装饰器,用于标记参数解析器
function CustomParam() {
  return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
    // 添加元数据,标记该参数为自定义参数
    Reflect.defineMetadata('customParam', true, target, propertyKey);
  };
}

// 定义一个示例的控制器类
class UsersController {
  getUser(@CustomParam() id: string) {
    // 处理获取用户的逻辑
    console.log(`Get User by ID: ${id}`);
  }
}

// 解析控制器方法的参数
function resolveParams(controller: any, methodName: string) {
  const method = controller[methodName];
  const params = Reflect.getMetadata('design:paramtypes', controller, methodName);

  const paramNames = Object.getOwnPropertyNames(method);
  for (const paramName of paramNames) {
    const isCustomParam = Reflect.getMetadata('customParam', method, paramName);
    if (isCustomParam) {
      // 解析自定义参数
      console.log(`Resolving custom param: ${paramName}`);
    }
  }
}

// 解析控制器方法的参数
resolveParams(UsersController.prototype, 'getUser');

在上述代码中,我们定义了 CustomParam 装饰器,并使用它来标记控制器方法的参数。该装饰器使用 Reflect.defineMetadata 添加元数据,以标记参数为自定义参数。

resolveParams 函数使用反射和元数据来解析控制器方法的参数。它获取方法的元数据和参数类型,并遍历参数列表。对于被标记为自定义参数的参数,它执行自定义的解析逻辑。

八、结语

这些示例代码演示了如何使用反射和元数据来实现不同的功能。通过利用 TypeScript 的装饰器和 Reflect API,我们可以在应用程序中实现更多复杂的功能,如依赖注入、动态路由映射、自定义参数解析器等。这些功能为开发人员提供了更大的灵活性和可扩展性,使得代码的编写和维护更加简洁和高效。

九、参考文献

  1. 阮一峰-es6-装饰器
  2. 一篇文章让你搞懂js里的Reflect和Reflect Metadata