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. 装饰器执行顺序
当多个装饰器应用于同一个声明时,它们的执行顺序如下:
- 参数装饰器,从上到下
- 方法装饰器,从上到下
- 访问器装饰器,从上到下
- 属性装饰器,从上到下
- 类装饰器,从上到下
10. 实际应用
在NestJS中,自定义装饰器通常用于:
- 权限控制:如
@Roles()装饰器 - 日志记录:如
@Log()装饰器 - 缓存控制:如
@Cache()装饰器 - 速率限制:如
@Throttle()装饰器 - 自定义路由:如
@CustomRoute()装饰器
实践案例分析
案例:基于装饰器的权限控制系统
需求分析
我们需要创建一个基于装饰器的权限控制系统,包括:
@Roles()装饰器:指定路由所需的角色@Public()装饰器:标记路由为公开访问@Permissions()装饰器:指定路由所需的具体权限- 权限检查守卫:验证用户权限
- 可扩展的权限管理系统
实现步骤
- 创建角色装饰器
- 创建公开路由装饰器
- 创建权限装饰器
- 创建权限检查守卫
- 测试权限控制系统
代码实现
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"
}代码解析
装饰器实现:
- 使用
SetMetadata函数创建装饰器,存储元数据 - 定义了
Roles、Public和Permissions装饰器
- 使用
权限检查守卫:
- 使用
Reflector获取装饰器设置的元数据 - 检查用户是否具有所需的角色和权限
- 支持公开路由跳过权限检查
- 使用
认证中间件:
- 模拟用户认证和角色分配
- 在实际应用中,应该验证JWT令牌并从数据库获取用户信息
测试控制器:
- 提供了不同权限级别��测试场景
- 演示了装饰器的组合使用
模块配置:
- 正确配置了中间件和守卫
- 确保权限检查在请求处理过程中正确执行
互动思考问题
思考:装饰器和继承的区别是什么?它们各自的使用场景是什么?
讨论:在实际应用中,如何设计一个可扩展的装饰器系统?需要考虑哪些因素?
实践:尝试创建一个日志记录装饰器,记录方法的调用参数、返回值和执行时间。
挑战:如何创建一个基于装饰器的缓存系统,支持缓存键生成和过期时间设置?
扩展:了解NestJS的
@nestjs/swagger模块,思考它如何使用装饰器生成API文档。
小结
本集我们学习了NestJS自定义装饰器的核心概念和使用方法,包括:
- 装饰器的基本概念和作用
- 不同类型装饰器的创建和使用
- 装饰器工厂的实现原理
- 元数据反射的使用方法
- 装饰器的执行顺序
- 实际应用场景和案例
通过实践案例,我们创建了一个完整的基于装饰器的权限控制系统,展示了如何使用装饰器实现灵活的权限管理。装饰器是NestJS的重要特性,它使得代码更加简洁、可读和可维护。
在下一集中,我们将学习NestJS的配置管理(Configuration),了解如何使用配置模块管理不同环境的配置,以及如何实现配置验证和加载。