装饰器不只是语法糖:Nest 如何解析 @Controller()?
在 NestJS 中,我们经常使用各种装饰器,如 @Controller()、@Get()、@Post() 等来定义路由和处理 HTTP 请求。但这些装饰器到底是什么?它们仅仅是语法糖吗?实际上,它们是 NestJS 强大功能的核心机制之一。本文将深入探讨装饰器的本质以及 NestJS 如何解析和使用它们。
1. 装饰器基础概念
装饰器是 JavaScript 的一个提案特性,目前处于 Stage 2 阶段。在 TypeScript 中已经可以使用,并且是构建 NestJS 框架的基础。
1.1 装饰器的类型
TypeScript 支持以下几种装饰器:
typescript
// 类装饰器
@MyClassDecorator()
class MyClass {}
// 属性装饰器
class MyClass {
@MyPropertyDecorator()
property: string;
}
// 方法装饰器
class MyClass {
@MyMethodDecorator()
method() {}
}
// 参数装饰器
class MyClass {
method(@MyParameterDecorator() param: string) {}
}1.2 装饰器的本质
装饰器本质上是一个函数,它可以在类、方法、属性或参数被定义时修改它们的行为:
typescript
// 类装饰器示例
function MyClassDecorator() {
return function (constructor: Function) {
// 修改构造函数或原型
console.log('装饰类:', constructor.name);
};
}
@MyClassDecorator()
class MyClass {}2. @Controller() 装饰器详解
让我们从最基础的 @Controller() 装饰器开始:
typescript
import { Controller } from '@nestjs/common';
@Controller('users')
export class UserController {
// 控制器逻辑
}2.1 源码分析
查看 @Controller() 的源码实现:
typescript
// nestjs/common/decorators/core/controller.decorator.ts(简化版)
export function Controller(prefix?: string | string[]): ClassDecorator {
return (target: object) => {
// 定义元数据
Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
// 存储控制器前缀
if (prefix) {
Reflect.defineMetadata(PATH_METADATA, prefix, target);
}
// 标记为可路由
Reflect.defineMetadata(ROUTE_METADATA, [], target);
};
}2.2 元数据的作用
@Controller() 装饰器通过 Reflect.defineMetadata 在类上定义了元数据:
CONTROLLER_WATERMARK: 标记这是一个控制器类PATH_METADATA: 存储控制器的路由前缀ROUTE_METADATA: 存储路由方法信息
3. 路由方法装饰器
3.1 @Get()、@Post() 等装饰器
typescript
@Controller('users')
export class UserController {
@Get(':id')
findOne(@Param('id') id: string) {
return { id, name: 'User Name' };
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return { id: '1', ...createUserDto };
}
}3.2 装饰器源码实现
typescript
// nestjs/common/decorators/http/request-mapping.decorator.ts(简化版)
export function RequestMapping(metadata: RequestMappingMetadata = {}): MethodDecorator {
return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
// 存储路由方法信息
Reflect.defineMetadata(METHOD_METADATA, metadata.method, descriptor.value);
Reflect.defineMetadata(PATH_METADATA, metadata.path, descriptor.value);
// 将路由添加到控制器的路由列表中
const routes = Reflect.getMetadata(ROUTE_METADATA, target.constructor) || [];
routes.push(descriptor.value);
Reflect.defineMetadata(ROUTE_METADATA, routes, target.constructor);
};
}
// 具体的 HTTP 方法装饰器
export const Get = (path?: string | string[]): MethodDecorator =>
RequestMapping({ method: RequestMethod.GET, path });
export const Post = (path?: string | string[]): MethodDecorator =>
RequestMapping({ method: RequestMethod.POST, path });4. NestJS 如何扫描和解析装饰器
4.1 模块扫描过程
当 NestJS 应用启动时,会进行模块扫描:
typescript
// 简化的扫描过程
class DependenciesScanner {
async scan(module: Type<any>) {
// 扫描模块中的控制器
await this.scanForControllers(module);
}
async scanForControllers(module: Type<any>) {
// 获取模块元数据中的控制器列表
const controllers = Reflect.getMetadata(MODULE_PATH, module) || [];
for (const controller of controllers) {
// 解析控制器的路由信息
this.extractRouterPath(controller);
}
}
}4.2 路由映射构建
typescript
class RouterExplorer {
extractRouterPath(controller: Type<any>): string {
// 获取控制器前缀
const controllerPath = Reflect.getMetadata(PATH_METADATA, controller) || '';
// 获取控制器中的所有路由方法
const routes = Reflect.getMetadata(ROUTE_METADATA, controller) || [];
for (const route of routes) {
// 获取路由方法和路径
const method = Reflect.getMetadata(METHOD_METADATA, route);
const path = Reflect.getMetadata(PATH_METADATA, route);
// 构建完整路由路径
const fullPath = this.normalizePath(controllerPath, path);
// 注册路由到 Express/Fastify
this.registerRoute(method, fullPath, controller, route.name);
}
return controllerPath;
}
}5. 装饰器与元数据反射
5.1 Reflect Metadata API
NestJS 依赖于 reflect-metadata 库来处理元数据:
typescript
// 需要在应用入口处引入
import 'reflect-metadata';
// 定义元数据
Reflect.defineMetadata('key', 'value', target);
// 获取元数据
const value = Reflect.getMetadata('key', target);5.2 编译器选项
要使装饰器正常工作,需要在 tsconfig.json 中启用相关选项:
json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}6. 自定义装饰器示例
6.1 创建自定义路由装饰器
typescript
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// 使用自定义装饰器
@Controller('users')
export class UserController {
@Public()
@Get('health')
health() {
return { status: 'ok' };
}
}6.2 在守卫中使用自定义装饰器
typescript
// guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@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();
return this.validateRequest(request);
}
private validateRequest(request: any): boolean {
// 实际的认证逻辑
return !!request.user;
}
}7. 装饰器执行时机
7.1 类定义时执行
装饰器在类被定义时执行,而不是在实例化时:
typescript
console.log('1. 类定义前');
@MyDecorator()
class MyClass {
constructor() {
console.log('3. 实例化类');
}
}
console.log('2. 类定义后');
new MyClass(); // 输出: 3. 实例化类
function MyDecorator() {
console.log('0. 装饰器执行');
return function (constructor: Function) {
console.log('1.5. 装饰器函数执行');
};
}7.2 多个装饰器的执行顺序
typescript
function Decorator1() {
console.log('Decorator1 factory');
return function (target: any) {
console.log('Decorator1 executed');
};
}
function Decorator2() {
console.log('Decorator2 factory');
return function (target: any) {
console.log('Decorator2 executed');
};
}
@Decorator1()
@Decorator2()
class MyClass {}输出顺序:
- Decorator2 factory
- Decorator1 factory
- Decorator2 executed
- Decorator1 executed
8. 最佳实践
8.1 合理使用装饰器
typescript
// 推荐:保持装饰器简单
@Controller('users')
export class UserController {
@Get(':id')
@UseInterceptors(LoggingInterceptor)
@UseGuards(AuthGuard)
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}
// 不推荐:过度使用装饰器导致代码难以理解
@Controller('users')
@UseInterceptors(LoggingInterceptor)
@UseGuards(AuthGuard)
@SetMetadata('roles', ['admin'])
export class UserController {
@Get(':id')
@UseInterceptors(TransformInterceptor)
@UseGuards(RoleGuard)
@SetMetadata('permissions', ['read'])
findOne(
@Param('id') id: string,
@Headers('x-request-id') requestId: string
) {
return this.userService.findOne(id);
}
}8.2 组合装饰器
typescript
// 创建组合装饰器
export function AuthAndRoles(roles: string[]) {
return applyDecorators(
UseGuards(AuthGuard, RolesGuard),
SetMetadata('roles', roles),
);
}
// 使用组合装饰器
@Controller('users')
export class UserController {
@Get(':id')
@AuthAndRoles(['admin'])
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}9. 总结
装饰器绝不仅仅是语法糖,它们是 NestJS 框架的核心机制:
- 元数据定义:装饰器通过
Reflect.defineMetadata定义元数据 - 模块扫描:NestJS 在启动时扫描装饰器定义的元数据
- 路由构建:基于元数据构建路由映射
- 运行时处理:在请求处理过程中使用元数据
通过理解装饰器的工作原理,我们可以:
- 更好地理解 NestJS 的内部机制
- 创建自定义装饰器扩展框架功能
- 优化应用结构和代码组织
- 解决框架使用中的疑难问题
在下一篇文章中,我们将探讨 @Module() 装饰器如何构建应用的依赖图谱。