一、什么是装饰器?
- 装饰器: 就是一个方法,可以注入到类、方法、属性参数上来扩展类、属性、方法、参数的功能。
- 装饰器可以用于各种不同的场景,例如:
- 类装饰器:应用于类声明之前,用于修改类的行为或元数据。
- 方法装饰器:应用于方法声明之前,用于修改方法的行为或元数据。
- 属性装饰器:应用于属性声明之前,用于修改属性的行为或元数据。
- 参数装饰器:应用于方法参数声明之前,用于修改参数的行为或元数据。
二、用类装饰器的来简单认识一下如何使用装饰器
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,写操作未生效
readonly装饰器接收三个参数:target,name,和descriptor。target是类的原型对象,name是方法的名称,descriptor是该方法的属性描述符对象。- 在装饰器函数中,您将
descriptor.writable设置为false,这将禁止对方法进行写操作,使其成为只读方法。 - 返回修改后的属性描述符对象
descriptor。 - 我们创建了一个
Person类,并在name方法上应用了@readonly装饰器。然后,我们尝试对test.name进行写操作,将其覆盖为一个新的方法。但是,当我们再次调用test.name()时,它仍然返回原始值111,说明写操作未生效,name方法仍然是只读的。
这证明了@readonly装饰器的功能,它成功地将name方法设置为只读,防止对其进行写操作。
三、在 Nest 框架中,装饰器广泛用于实现各种功能和特性。下面是一些常见的装饰器在 Nest 中的使用示例
@Module装饰器:用于定义一个模块。
@Module({
imports: [ConfigModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
@Controller装饰器:用于定义一个控制器。
@Controller('users')
export class UsersController {
// ...
}
@Injectable装饰器:用于定义一个可注入的服务类。
@Injectable()
export class UserService {
// ...
}
@Inject装饰器:用于指定依赖注入的依赖项。
@Injectable()
export class UserService {
constructor(
@Inject(LoggerService) private readonly logger: LoggerService,
private readonly authService: AuthService,
) {
// ...
}
}
@Get、@Post、@Put等 HTTP 方法装饰器:用于定义路由处理方法和路由路径。
@Controller('users')
export class UsersController {
@Get(':id')
getUser(@Param('id') id: string): User {
// ...
}
@Post()
createUser(@Body() createUserDto: CreateUserDto): User {
// ...
}
}
@Param、@Query、@Body等参数装饰器:用于获取请求中的参数。
@Controller('users')
export class UsersController {
@Get(':id')
getUser(@Param('id') id: string): User {
// ...
}
}
@UseGuards装饰器:用于为路由或控制器指定守卫。
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
// ...
}
@UseInterceptors装饰器:用于为路由或控制器指定拦截器。
@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UsersController {
// ...
}
四、Nest框架的装饰器可以分为以下三类:核心类装饰器、HTTP类装饰器和模块类装饰器。
-
核心类装饰器:
@Module():用于标识一个类作为Nest模块,用于组织应用程序中的组件。@Controller():用于标识一个类作为Nest控制器,负责处理传入的HTTP请求。@Injectable():用于标识一个类作为Nest提供者,用于实例化和管理应用程序的依赖项。@Middleware():用于标识一个类作为Nest中间件,用于处理HTTP请求和响应的中间操作。@Guard():用于标识一个类作为Nest守卫,用于执行身份验证和授权逻辑的中间件。
-
HTTP类装饰器:
@Get()、@Post()、@Put()、@Delete()等:用于标识控制器中的方法作为特定HTTP请求方法的处理程序。@Param()、@Query()、@Body()等:用于标识控制器方法中的参数来源,从URL路径参数、查询参数和请求体中提取值。@Render():用于标识控制器方法返回视图模板的装饰器。
-
模块类装饰器:
@Imports():用于在模块中导入其他模块。@Providers():用于在模块中定义提供者,以供依赖注入使用。@Controllers():用于在模块中定义控制器。@Exports():用于将模块中的提供者暴露给其他模块。@Global():用于标识一个模块为全局模块,使得其提供者在整个应用程序中可见。
这些装饰器提供了一种声明性的方式来定义和配置Nest应用程序的各个组件。它们通过元编程的方式,使开发人员能够轻松地定义路由、请求处理程序、依赖注入和模块之间的关系。通过合理使用这些装饰器,可以构建出高度可维护和可扩展的Nest应用程序。
五、 从HTTP中request-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 作为元数据传递进去。
最后,定义了一系列具体的路由处理器装饰器,如 Post、Get、Delete、Put 等,它们都是通过调用 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 在背后使用的机制,使得开发人员可以方便地进行依赖注入的编程。
七、当使用反射和元数据时,可以实现一些其他的功能
- 如动态路由映射和自定义参数解析器。下面是两个示例代码,演示了如何使用反射和元数据来实现这些功能:
- 动态路由映射:
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 函数使用反射和元数据来获取路由处理器类的元数据,并根据元数据创建路由映射。它遍历所有的路由处理器类,获取类和方法的元数据,然后打印出路由映射的信息。
- 自定义参数解析器:
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,我们可以在应用程序中实现更多复杂的功能,如依赖注入、动态路由映射、自定义参数解析器等。这些功能为开发人员提供了更大的灵活性和可扩展性,使得代码的编写和维护更加简洁和高效。