NestJS 装饰器
一、装饰器是什么?
装饰器(Decorator)本质上是一种特殊的函数,可以作用于类、方法、属性或参数,用于添加元数据或修改原有行为。
装饰器不是 NestJS 发明的,而是 TypeScript 支持的一种语法机制。
例如:
@Controller('users')
export class UserController {
@Get(':id')
getUser(@Param('id') id: string) {
return { id };
}
}
这里的装饰器分别表示:
@Controller('users'):声明 Controller 和路由前缀。@Get(':id'):声明方法对应的 HTTP 路由。@Param('id'):声明参数从 URL 中提取。
这些装饰器主要负责声明信息,实际的路由注册、参数提取和请求处理由 NestJS 框架完成。
二、为什么使用装饰器?有什么优点?
装饰器主要有四个优点:
| 优点 | 说明 |
|---|---|
| 声明式编程 | 直接在类或方法上描述功能,不需要单独配置 |
| 减少重复代码 | 将日志、权限、校验等通用逻辑抽离 |
| 关注点分离 | 业务方法只处理业务,其他功能交给装饰器或框架 |
| 易于扩展 | 可以自定义装饰器,为类和方法添加额外能力 |
例如,不使用装饰器时:
router.get('/users', controller.getUsers);
router.post('/users', controller.createUser);
NestJS 使用装饰器:
@Controller('users')
class UserController {
@Get()
getUsers() {}
@Post()
createUser() {}
}
这样路由信息直接写在对应的方法旁边,代码更集中,也方便 NestJS 统一管理。
三、装饰器是怎么实现的?
装饰器本质上是函数。它在被装饰的类定义时执行,而不是每次调用业务方法时都执行。
1. 最简单的类装饰器
function MyDecorator(target: Function) {
console.log(target.name);
}
@MyDecorator
class UserService {}
可以近似理解为:
class UserService {}
MyDecorator(UserService);
其中 target 就是被装饰的类。
2. 为什么装饰器可以传参数?
例如:
@Controller('users')
这是因为 Controller() 是一个返回装饰器的函数,也叫装饰器工厂。
function Controller(path: string) {
return function (target: Function) {
console.log(path, target.name);
};
}
@Controller('users')
class UserController {}
相当于:
const decorator = Controller('users');
decorator(UserController);
外层函数接收配置参数,内层函数才是真正的装饰器。
四、NestJS 装饰器的核心实现原理
NestJS 最常见的实现方式是:装饰器添加元数据,框架读取元数据,然后执行相应逻辑。
例如模拟 @Controller():
import 'reflect-metadata';
function Controller(path: string) {
return function (target: Function) {
Reflect.defineMetadata('path', path, target);
};
}
@Controller('users')
class UserController {}
这里:
Reflect.defineMetadata('path', 'users', UserController);
相当于给类关联了一条元数据:
UserController
└── Metadata
└── path = "users"
元数据通常保存在运行时内存中,由 reflect-metadata 提供读写能力。
读取元数据:
const path = Reflect.getMetadata(
'path',
UserController,
);
console.log(path); // users
NestJS 会在初始化过程中扫描 Controller、读取元数据,并建立路由映射。
- 声明装饰器:
@Get('users') - 写入元数据:
Reflect.defineMetadata() - NestJS 初始化:扫描并读取元数据
- 注册路由:建立 URL 与方法的映射
- 请求到来:执行对应的 Controller 方法
注意,装饰器本身并不负责执行 HTTP 请求,也不负责实际的权限检查。它只是声明配置,真正执行的是 NestJS 框架。
五、装饰器有哪些常见用途?
装饰器主要分为两种使用方式。
1. 添加元数据(NestJS 最常见)
例如:
@UseGuards(AuthGuard)
@Get('users')
getUsers() {}
@UseGuards() 的核心工作是将 Guard 信息保存到元数据中。
NestJS 在构建请求处理流程时读取这些元数据,并在请求到来时调用 Guard 的 canActivate()。
其他常见例子:
| 装饰器 | 用途 |
|---|---|
@Controller() |
声明 Controller |
@Get()、@Post() |
声明路由 |
@Injectable() |
标记类可由 NestJS 依赖注入系统管理 |
@UseGuards() |
配置权限守卫 |
@UseInterceptors() |
配置拦截器 |
@UsePipes() |
配置参数转换和校验 |
@SetMetadata() |
自定义元数据 |
@Param()、@Body() |
声明请求参数的提取方式 |
其中 @Param()、@Body() 还涉及参数位置等元数据,并不是所有装饰器都简单地把信息存到类上。
2. 直接修改方法行为
装饰器也可以直接包装或替换原来的方法。
例如实现一个日志装饰器:
function Log(
target: any,
key: string,
descriptor: PropertyDescriptor,
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log('执行前');
const result = original.apply(this, args);
console.log('执行后');
return result;
};
return descriptor;
}
使用:
class UserService {
@Log
getUser() {
console.log('获取用户');
}
}
new UserService().getUser();
输出:
执行前
获取用户
执行后
原理是通过 descriptor.value 替换原方法。
这里要区分两个执行时机:
@Log装饰器在类定义时执行,完成方法包装。- 包装后的方法在每次调用
getUser()时执行。
这种方式不依赖 NestJS,普通 TypeScript 类也可以使用。
常见用途包括日志记录、性能统计、缓存、失败重试、防抖、参数校验等。
不过在 NestJS 中,对于 HTTP 请求的日志、缓存、权限等功能,通常更推荐使用 Interceptor、Guard、Pipe 等框架机制。
六、装饰器有哪几种?
在 NestJS 常用的 TypeScript legacy 装饰器体系中:
| 类型 | 作用对象 | 常见例子 |
|---|---|---|
| 类装饰器 | Class | @Controller() |
| 方法装饰器 | Method | @Get() |
| 属性装饰器 | Property | @Inject() |
| 参数装饰器 | Parameter | @Param() |
TypeScript 还有新版标准装饰器机制,其函数签名与 legacy 装饰器不同。本文示例使用的是 NestJS 常见的 legacy 装饰器语法。
七、最终总结
NestJS 装饰器需要掌握的核心知识:
- 本质:装饰器是一种特殊的函数,可以扩展类、方法、属性或参数。
- 作用:主要用于添加元数据,也可以直接修改方法行为。
- 实现:通过装饰器函数接收目标对象,使用
Reflect.defineMetadata()保存信息,或者通过descriptor.value修改方法。 - 执行时机:装饰器在类定义时应用,而不是每次请求时重新执行。
- NestJS 工作机制:装饰器声明信息 → NestJS 读取元数据 → 框架在适当时机执行对应逻辑。
- 优势:声明式、减少重复代码、关注点分离、易于扩展。
- 常见用途:路由注册、依赖注入、权限守卫、拦截器、参数校验、自定义元数据,以及日志、缓存等方法包装。
一句话理解:装饰器就是在不直接修改业务代码的情况下,为类或方法附加信息或扩展行为的一种机制;NestJS 主要利用它来声明配置,再由框架统一解释和执行。