NestJS自定义装饰器 (Custom Decorators)

学习目标

  • 理解装饰器在NestJS中的作用和地位
  • 掌握装饰器的基本原理和工作机制
  • 学会创建和使用不同类型的装饰器
  • 理解如何使用装饰器增强代码功能
  • 能够应用装饰器优化代码结构和可读性

核心知识点

1. 装饰器概念

装饰器是一种特殊类型的声明,它可以附加到类声明、方法、访问器、属性或参数上。装饰器使用@expression语法,其中expression必须是一个函数,它会在运行时被调用,被装饰的声明信息作为参数传递给它。

在NestJS中,装饰器被广泛使用:

  • @Controller():标记一个类为控制器
  • @Get()@Post()等:标记方法为HTTP路由处理程序
  • @Injectable():标记一个类为可注入的提供者
  • @Module():标记一个类为模块

2. 装饰器类型

TypeScript支持以下几种类型的装饰器:

  • 类装饰器:应用于类声明
  • 方法装饰器:应用于方法声明
  • 访问器装饰器:应用于访问器声明
  • 属性装饰器:应用于属性声明
  • 参数装饰器:应用于参数声明

3. 装饰器工厂

装饰器工厂是一个返回装饰器函数的函数,它允许我们为装饰器提供参数。在NestJS中,大多数装饰器都是以工厂形式实现的:

// 装饰器工厂
function Roles(...roles: string[]) {
  // 返回装饰器函数
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 实现装饰器逻辑
    Reflect.defineMetadata('roles', roles, target, propertyKey);
  };
}

// 使用装饰器工厂
class UserController {
  @Roles('admin', 'moderator')
  getUser() {
    // 方法实现
  }
}

4. 参数装饰器

参数装饰器应用于函数的参数声明。在NestJS中,@Body()@Param()@Query()等都是参数装饰器:

// 创建一个参数装饰器
function CustomParam(key: string) {
  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
    // 存储参数信息
    const existingParams = Reflect.getMetadata('customParams', target, propertyKey) || [];
    existingParams.push({ key, index: parameterIndex });
    Reflect.defineMetadata('customParams', existingParams, target, propertyKey);
  };
}

// 使用参数装饰器
class CatsController {
  @Get(':id')
  findOne(@CustomParam('id') id: string) {
    return `Cat with id ${id}`;
  }
}

5. 方法装饰器

方法装饰器应用于方法声明。在NestJS中,@Get()@Post()等HTTP方法装饰器都是方法装饰器:

// 创建一个方法装饰器
function LogExecutionTime() {
  return function (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const start = Date.now();
      const result = originalMethod.apply(this, args);
      const end = Date.now();
      console.log(`Method ${propertyKey.toString()} executed in ${end - start}ms`);
      return result;
    };
    
    return descriptor;
  };
}

// 使用方法装饰器
class CatsService {
  @LogExecutionTime()
  findAll() {
    // 方法实现
    return ['cat1', 'cat2'];
  }
}

6. 类装饰器

类装饰器应用于类声明。在NestJS中,@Controller()@Module()等都是类装饰器:

// 创建一个类装饰器
function Controller(prefix: string) {
  return function (target: Function) {
    Reflect.defineMetadata('prefix', prefix, target);
  };
}

// 使用类装饰器
@Controller('cats')
class CatsController {
  // 控制器方法
}

7. 属性装饰器

属性装饰器应用于属性声明:

// 创建一个属性装饰器
function Inject(value: string) {
  return function (target: Object, propertyKey: string | symbol) {
    Reflect.defineMetadata('inject', value, target, propertyKey);
  };
}

// 使用属性装饰器
class CatsService {
  @Inject('DATABASE_CONNECTION')
  private database: any;
  
  // 服务方法
}

8. 元数据反射

装饰器通常与元数据反射一起使用,Reflect API提供了以下方法:

  • Reflect.getMetadata(key, target, propertyKey?):获取元数据
  • Reflect.defineMetadata(key, value, target, propertyKey?):定义元数据
  • Reflect.hasMetadata(key, target, propertyKey?):检查是否存在元数据
  • Reflect.deleteMetadata(key, target, propertyKey?):删除元数据

在NestJS中,@nestjs/core模块的Reflector类提供了更高级的元数据访问方法。

9. 装饰器执行顺序

当多个装饰器应用于同一个声明时,它们的执行顺序如下:

  1. 参数装饰器,从上到下
  2. 方法装饰器,从上到下
  3. 访问器装饰器,从上到下
  4. 属性装饰器,从上到下
  5. 类装饰器,从上到下

10. 实际应用

在NestJS中,自定义装饰器通常用于:

  • 权限控制:如@Roles()装饰器
  • 日志记录:如@Log()装饰器
  • 缓存控制:如@Cache()装饰器
  • 速率限制:如@Throttle()装饰器
  • 自定义路由:如@CustomRoute()装饰器

实践案例分析

案例:基于装饰器的权限控制系统

需求分析

我们需要创建一个基于装饰器的权限控制系统,包括:

  • @Roles()装饰器:指定路由所需的角色
  • @Public()装饰器:标记路由为公开访问
  • @Permissions()装饰器:指定路由所需的具体权限
  • 权限检查守卫:验证用户权限
  • 可扩展的权限管理系统

实现步骤

  1. 创建角色装饰器
  2. 创建公开路由装饰器
  3. 创建权限装饰器
  4. 创建权限检查守卫
  5. 测试权限控制系统

代码实现

1. 创建装饰器
// decorators.ts
import { SetMetadata } from '@nestjs/common';

// 角色装饰器
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

// 公开路由装饰器
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// 权限装饰器
export const PERMISSIONS_KEY = 'permissions';
export const Permissions = (...permissions: string[]) => SetMetadata(PERMISSIONS_KEY, permissions);
2. 创建权限检查守卫
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, IS_PUBLIC_KEY, PERMISSIONS_KEY } from './decorators';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 检查是否为公开路由
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (isPublic) {
      return true;
    }

    // 获取请求对象
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    
    // 检查用户是否已认证
    if (!user) {
      throw new UnauthorizedException('User not authenticated');
    }

    // 检查角色
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (requiredRoles) {
      const hasRole = requiredRoles.some((role) => user.roles?.includes(role));
      if (!hasRole) {
        throw new ForbiddenException('Insufficient role permissions');
      }
    }

    // 检查权限
    const requiredPermissions = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    
    if (requiredPermissions) {
      const hasPermission = requiredPermissions.some((permission) => user.permissions?.includes(permission));
      if (!hasPermission) {
        throw new ForbiddenException('Insufficient permissions');
      }
    }

    return true;
  }
}
3. 创建测试控制器
// users.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { Roles, Public, Permissions } from './decorators';

@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  // 公开路由
  @Get('public')
  @Public()
  getPublicInfo() {
    return { message: 'Public information' };
  }

  // 需要认证,但不需要特定角色
  @Get('profile')
  getProfile(@Body() req: any) {
    return req.user;
  }

  // 需要管理员角色
  @Get('all')
  @Roles('admin')
  getAllUsers() {
    return [
      { id: 1, username: 'admin' },
      { id: 2, username: 'user' },
    ];
  }

  // 需要编辑用户权限
  @Post()
  @Permissions('edit:users')
  createUser(@Body() user: { username: string; password: string }) {
    return { id: 3, ...user };
  }

  // 需要管理员角色和删除用户权限
  @Post('delete/:id')
  @Roles('admin')
  @Permissions('delete:users')
  deleteUser(@Body('id') id: number) {
    return { message: `User ${id} deleted` };
  }
}
4. 创建认证中间件
// auth.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    // 模拟用户认证
    // 在实际应用中,这里应该验证JWT令牌
    const authHeader = req.headers.authorization;
    
    if (authHeader) {
      // 模拟不同用户角色和权限
      if (authHeader.includes('admin')) {
        req.user = {
          id: 1,
          username: 'admin',
          roles: ['admin'],
          permissions: ['read:users', 'edit:users', 'delete:users'],
        };
      } else if (authHeader.includes('user')) {
        req.user = {
          id: 2,
          username: 'user',
          roles: ['user'],
          permissions: ['read:users'],
        };
      }
    }
    
    next();
  }
}
5. 配置模块
// users.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { UsersController } from './users.controller';
import { AuthGuard } from './auth.guard';
import { AuthMiddleware } from './auth.middleware';

@Module({
  controllers: [UsersController],
  providers: [AuthGuard],
})
export class UsersModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes('users');
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}

测试结果

1. 访问公开路由

请求

GET /users/public

响应

{
  "message": "Public information"
}
2. 管理员访问用户列表

请求

GET /users/all
Authorization: Bearer admin-token

响应

[
  { "id": 1, "username": "admin" },
  { "id": 2, "username": "user" }
]
3. 普通用户尝试访问用户列表

请求

GET /users/all
Authorization: Bearer user-token

响应

{
  "statusCode": 403,
  "message": "Insufficient role permissions",
  "error": "Forbidden"
}
4. 管理员创建用户

请求

POST /users
Authorization: Bearer admin-token
Content-Type: application/json

{
  "username": "newuser",
  "password": "password123"
}

响应

{
  "id": 3,
  "username": "newuser",
  "password": "password123"
}
5. 普通用户尝试创建用户

请求

POST /users
Authorization: Bearer user-token
Content-Type: application/json

{
  "username": "newuser",
  "password": "password123"
}

响应

{
  "statusCode": 403,
  "message": "Insufficient permissions",
  "error": "Forbidden"
}

代码解析

  1. 装饰器实现

    • 使用SetMetadata函数创建装饰器,存储元数据
    • 定义了RolesPublicPermissions装饰器
  2. 权限检查守卫

    • 使用Reflector获取装饰器设置的元数据
    • 检查用户是否具有所需的角色和权限
    • 支持公开路由跳过权限检查
  3. 认证中间件

    • 模拟用户认证和角色分配
    • 在实际应用中,应该验证JWT令牌并从数据库获取用户信息
  4. 测试控制器

    • 提供了不同权限级别��测试场景
    • 演示了装饰器的组合使用
  5. 模块配置

    • 正确配置了中间件和守卫
    • 确保权限检查在请求处理过程中正确执行

互动思考问题

  1. 思考:装饰器和继承的区别是什么?它们各自的使用场景是什么?

  2. 讨论:在实际应用中,如何设计一个可扩展的装饰器系统?需要考虑哪些因素?

  3. 实践:尝试创建一个日志记录装饰器,记录方法的调用参数、返回值和执行时间。

  4. 挑战:如何创建一个基于装饰器的缓存系统,支持缓存键生成和过期时间设置?

  5. 扩展:了解NestJS的@nestjs/swagger模块,思考它如何使用装饰器生成API文档。

小结

本集我们学习了NestJS自定义装饰器的核心概念和使用方法,包括:

  • 装饰器的基本概念和作用
  • 不同类型装饰器的创建和使用
  • 装饰器工厂的实现原理
  • 元数据反射的使用方法
  • 装饰器的执行顺序
  • 实际应用场景和案例

通过实践案例,我们创建了一个完整的基于装饰器的权限控制系统,展示了如何使用装饰器实现灵活的权限管理。装饰器是NestJS的重要特性,它使得代码更加简洁、可读和可维护。

在下一集中,我们将学习NestJS的配置管理(Configuration),了解如何使用配置模块管理不同环境的配置,以及如何实现配置验证和加载。

« 上一篇 NestJS拦截器 (Interceptors) 下一篇 » NestJS配置管理 (Configuration)