原来TypeScript的装饰器并不难,一篇文章让你掌握4种类型的装饰器!
如果你使用过Angular或Nest.js,你应该熟悉装饰器。
import { Controller, Get } from '@nestjs/common'
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats'
}
}
在上面的代码中?@XXX
?是语法糖,要启用实验性装饰器功能,您必须在命令行或?tsconfig.json
?文件中启用?experimentalDecorators
?编译器选项。???????
{
"compilerOptions": {
"target": "es2016",
"experimentalDecorators": true
}
}
装饰器主要有四种类型:
类装饰器
属性装饰器
方法装饰器
参数装饰器
?
接下来,我将分别为大家介绍这4类装饰器。
类装饰器,顾名思义,是用来装饰类的。类装饰器对应的类型在?lib.es5.d.ts
?文件中定义:???????
// node_modules/typescript/lib/lib.es5.d.ts
declare type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
从类装饰器的类型定义可以看出,它接收一个表示要装饰的类的参数。根据类decorator的定义,我们可以创建一个?Greeter
??decorator来为被装饰的类添加一个?greet
?方法:???????
// user.ts
function Greeter(target: Function): void {
target.prototype.greet = function (): void {
console.log("Hello Bytefer!");
};
}
@Greeter
class User {}
let bytefer = new User();
(bytefer as any).greet();
当您成功运行上述代码时,终端将输出结果-??Hello Bytefer!
?。类装饰器看起来很简单,但问题来了。如果我们想要定制?greet
?方法的输出,我们应该怎么做?为了满足这个功能,我们需要使用装饰器工厂。
?
所谓装饰器工厂就是在被调用后返回装饰器。利用高阶函数的特性,我们来更新前面定义的?Greeter
?函数:???????
function Greeter(msg: string) {
return (target: Function): void => {
target.prototype.greet = function (): void {
console.log(msg);
};
}
}
在更新?Greeter
?装饰器工厂后,我们需要按照以下方式使用它:???????
@Greeter("Hello TypeScript!")
class User {}
let bytefer = new User();
(bytefer as any).greet();
当您成功运行上述代码时,终端将输出结果-??Hello TypeScript!
?。应该注意的是,我们可以为同一个类使用多个类装饰器。例如,在下面的代码中,我添加了一个?Log
?类装饰器:
function Log(target: Function): void {
target.prototype.log = (msg: string) => {
console.log(`From ${target.name}: `, msg);
};
}
@Log
@Greeter("Hello TypeScript!")
class User {}
let bytefer = new User();
(bytefer as any).greet();
(bytefer as any).log("Hello Kakuqo!");
在上面的代码中,我们为?User
?类添加了2个装饰器,之后,我们可以在?User
?实例上调用?greet
?和?log
?方法。
Hello TypeScript!
From User: Hello Kakuqo!
属性装饰器用于装饰类的属性。其对应的类型声明如下:???????
// node_modules/typescript/lib/lib.es5.d.ts
declare type PropertyDecorator =
(target: Object, propertyKey: string | symbol) => void;
属性装饰函数有两个参数:?target
?和?property
?。根据属性装饰器的定义,让我们定义一个属性装饰器?logProperty
?来跟踪用户对属性的操作。???????
function logProperty(target: any, key: string) {
let value = target[key];
const getter = function () {
console.log(`Getter for ${key} returned ${value}`);
return value;
};
const setter = function (newVal: any) {
console.log(`Set ${key} to ${newVal}`);
value = newVal;
};
// Replace the property
if (delete target[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
}
一旦我们有了?logProperty
?属性装饰器,我们就可以把它应用到类的属性上。例如,我在?User
?类的?name
?属性上使用了?logProperty
?装饰符:???????
class User {
@logProperty
public name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User("Bytefer");
user.name = "Kakuqo";
console.log(user.name);
当您成功运行上述代码时,终端将输出结果:???????
Set name to Bytefer
Set name to Kakuqo
Getter for name returned Kakuqo
Kakuqo
除了能够修饰类的属性外,我们还可以装饰类的方法。
?
方法装饰器用于装饰类的方法。其对应的类型声明如下:???????
// node_modules/typescript/lib/lib.es5.d.ts
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
与属性装饰器相比,方法装饰器多了一个描述符参数。参数类型如下:???????
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
如果你不需要使用泛型,你也可以使用?PropertyDescriptor
?接口:???????
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
根据方法装饰器的定义,我们定义一个方法装饰器?logMethod
?来跟踪类成员方法的调用。???????
function logMethod(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Before invoking method: ${propertyKey}`);
let result = originalMethod.apply(this, args);
console.log(`After invoking method: ${propertyKey}`);
return result;
};
}
一旦有了?logMethod
?方法装饰符,就可以将它应用到类的成员方法上。例如,我在?User
?类的?greet
?成员方法上使用了?logMethod
?装饰符:???????
class User {
@logMethod
greet(msg: string): string {
return `Hello ${msg}!`;
}
}
let user = new User();
let msg = user.greet("Bytefer");
console.log(msg);
当您成功运行上述代码时,终端将输出结果:???????
Before invoking method: greet
After invoking method: greet
Hello Bytefer!
掌握了方法装饰器之后,我们可以定义一些有用的方法装饰器。如?delay
?、?throttle
?等。
延迟???????
function delay(milliseconds: number = 0): any {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
setTimeout(() => {
originalMethod.apply(this, args);
}, milliseconds);
};
return descriptor;
};
}
节流???????
const throttleFn = require('lodash.throttle');
function throttle(milliseconds: number = 0, options = {}): any {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = throttleFn(originalMethod, milliseconds, options);
return descriptor;
};
}
最后,让我们介绍参数装饰器。
???????
参数装饰器用于装饰方法中的参数。其对应的类型声明如下:???????
// node_modules/typescript/lib/lib.es5.d.ts
declare type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number) => void;
根据参数decorator的定义,我们定义一个方法decorator??logParameter
?:???????
function logParameter(target: Object, key: string, parameterIndex: number) {
console.log(`The parameter in position ${parameterIndex} at ${key} has been decorated`);
}
有了?logParameter
?装饰符,我们将它应用到?User
?类中?greet
?方法的msg参数:???????
class User {
greet(@logParameter msg: string): void {
console.log(msg);
}
}
let user = new User();
user.greet("Bytefer");
当您成功运行上述代码时,终端将输出结果:???????
The parameter in position 0 at greet has been decorated
Bytefer
既然已经介绍了这4个装饰器,如果你想知道装饰器是如何工作的,你可以使用TypeScript Playground来查看编译后的JavaScript代码。
在开发装饰器时,我们通常还使用reflect-metadata库来处理元信息。例如,开发IoC容器或扩展web框架的功能。如果你想了解更多关于装饰器的知识,我建议你阅读overnight项目的源代码(ExpressJS服务器的TypeScript装饰器),它比Nest.js框架更容易理解。
?欢迎关注公众号:文本魔术,了解更多