装饰器
装饰器是一种特殊类型的函数,它以@符号为前缀,后跟一个函数表达式。这个函数会在类声明之后立即执行
。
装饰器根据它们可以附着的代码元素类型分为类装饰器、方法装饰器、属性装饰器、参数装饰器和访问器装饰器。
要使用传统装饰器,需要在 tsconfig.json 中启用 experimentalDecorators 选项。
{
"compilerOptions": {
"experimentalDecorators": true
}
}
类装饰器
类装饰器函数有一个参数,即类的构造函数。
function logClass(target: Function) {
console.log("Class logged: ", target.name); // MyClass
}
@logClass
class MyClass {
// ...
}
类装饰器可以返回一个新类,这个新类将替换原始类。利用继承,我们可以做一些事情。
function logClass(target: new () => any) {
return class extends target {
name = "hello";
};
}
@logClass
class MyClass {
name: string = "";
}
const instance = new MyClass();
console.log(instance.name); // hello
方法装饰器
方法装饰器函数接收三个参数:类的原型对象、方法名和方法的属性描述符。
function methodLog(target: any, key: string, descriptor: PropertyDescriptor) {
// target 是 MyClass 的原型对象
// key 是 sayHello 方法名
// descriptor 是 sayHello 方法的属性描述符
console.log(target, key, descriptor);
}
class MyClass {
@methodLog
sayHello() {}
}
descriptor
的 value
属性就是方法本身,所以我们可以利用它来修改方法。下面的示例中,用自定义的函数替换了原来的方法。所以instance.sayHello();
执行的是自定义的函数,打印了 hello 和 world。
function methodLog(target: any, key: string, descriptor: PropertyDescriptor) {
// 保存原来的方法
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("hello"); // 打印 hello
return originalMethod.apply(this, args); // 打印 world
};
}
class MyClass {
@methodLog
sayHello() {
console.log("world");
return "log return";
}
}
const instance = new MyClass();
instance.sayHello(); // log return
属性装饰器
属性装饰器函数接收两个参数:类的原型对象和属性名。
function logProperty(target: any, propertyName: string) {
let value = target[propertyName];
const getter = function () {
console.log(`Get ${propertyName}: ${value}`); // 输出: Get message: Hello
return value;
};
const setter = function (newVal: any) {
console.log(`Set ${propertyName}: ${newVal}`); // 输出: Set message: World
value = newVal;
};
Object.defineProperty(target, propertyName, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class MyClass {
@logProperty
message: string = "Hello";
}
const myClass = new MyClass();
console.log(myClass.message); // hello
myClass.message = "World";
console.log(myClass.message); // world
参数装饰器
参数装饰器函数接收三个参数:类的原型对象、属性名和参数在函数参数列表中的索引。
function Params(target: any, name: string, index: number) {
// target: MyClass原型对象
// name: sayHello
// index: 0 该参数在函数参数列表中的索引为0
console.log(target, name, index);
}
class MyClass {
sayHello(@Params name: string) {}
}
元数据
上面我们简单介绍了装饰器,似乎没有感觉到装饰器有什么作用,当装饰器与元数据结合使用时,就可以做到很多事情了。
元数据是关于代码中的实体(如类、方法、属性等)的描述性信息。它可以在类定义的时候,给类添加一些属性,这些属性可以在运行时访问。nestjs
框架就是利用装饰器和元数据来实现依赖注入的。
使用元数据,需要先安装reflect-metadata
包。
npm i reflect-metadata
引入这个包后,Reflect
对象上会多出很多操作元数据的方法。
-
Reflect.defineMetadata(metadataKey, metadataValue, target); 用于添加元数据,metadataKey:元数据的键,metadataValue:元数据值,target:目标对象。
-
Reflect.getMetadata(metadataKey, target); 用于获取元数据,metadataKey:元数据的键,target:目标对象。
import "reflect-metadata";
class MyClass {}
Reflect.defineMetadata("key", "value", MyClass);
console.log(Reflect.hasMetadata("key", MyClass)); // 判断是否含有元数据,结果为:true
console.log(Reflect.getMetadata("key", MyClass)); // 获取元数据,结果为:value
emitDecoratorMetadata
在 ts 中,开启 emitDecoratorMetadata 后,类会添加一些内置的元数据。
通过下面的配置,开启 emitDecoratorMetadata 选项。
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true // 开启 emitDecoratorMetadata 选项
}
}
给类的成员添加装饰器(空的类装饰器就行)
import "reflect-metadata";
function Injectable(target: Function) {}
function propDecorator(target: object, propertyKey: string) {}
function paramDecorator(
target: object,
propertyKey: any,
parameterIndex: number
) {}
function methodDecorator(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {}
@Injectable
class MyClass {
@propDecorator
name: string = "";
constructor(
@paramDecorator serviceA: string,
@paramDecorator serviceB: string
) {}
@methodDecorator
myMethod(@paramDecorator a: number, @paramDecorator b: number): string {
return "hello";
}
}
之后,类中就会内置下面这些元数据。
design:type
用于属性的类型元数据design:paramtypes
用于构建函数或方法参数的类型元数据design:returntype
用于方法的返回类型元数据
import "reflect-metadata";
function Injectable(target: Function) {}
function propDecorator(target: object, propertyKey: string) {}
function paramDecorator(
target: object,
propertyKey: any,
parameterIndex: number
) {}
function methodDecorator(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {}
@Injectable
class MyClass {
@propDecorator
name: string = "";
constructor(
@paramDecorator serviceA: string,
@paramDecorator serviceB: string
) {}
@methodDecorator
myMethod(@paramDecorator a: number, @paramDecorator b: number): string {
return "hello";
}
}
// 获取属性 name 的类型构造函数 [Function: String]
console.log(Reflect.getMetadata("design:type", MyClass.prototype, "name"));
// 获取方法 myMethod 的类型构造函数 [Function: Function]
console.log(Reflect.getMetadata("design:type", MyClass.prototype, "myMethod"));
// 获取方法 MyClass constructor参数类型的构造函数 [ [Function: String], [Function: String] ]
console.log(Reflect.getMetadata("design:paramtypes", MyClass));
// 获取法法 myMethod 的参数类型的构造函数 [ [Function: Number], [Function: Number] ]
console.log(
Reflect.getMetadata("design:paramtypes", MyClass.prototype, "myMethod")
);
// 获取 myMethod 的返回类型的构造函数 [Function: String]
console.log(
Reflect.getMetadata("design:returntype", MyClass.prototype, "myMethod")
);
nest 中的装饰器
nest 中,装饰器可以让代码更通俗易懂,通过元数据注入,实现了控制反转。
@Controller
给一个类定义@Controller
装饰器,目标类就变成了路由控制器。这个装饰器还可以传递参数,用于定义路由前缀。
下面代码中,@Controller("/user")
就定义了路由前缀为/user
的路由控制器。
@Controller("/user")
class User {}
@Controller
装饰器内部实现大概就是给目标类添加了路由前缀的元数据。之后 ioc 容器创建类实例的时候,可以从元数据中获取路由前缀,然后拼接成完整的路由。
function Controller(path: string) {
return (target: any) => {
Reflect.defineMetadata("prefix", path, target);
};
}
@Get
方法装饰器也是同理,给方法添加@Get
装饰器,目标方法就变成了路由方法。
@Controller("/user")
class User {
@Get("login")
login() {
// do something
}
}
内部会给目标方法添加路径的元数据。在创建 ioc 创建类实例的时候,就可以从元数据中获取路径,然后拼接成完整的路由。
function Get(path: string) {
return (target: any, propertyKey: string) => {
Reflect.defineMetadata("path", path, target[propertyKey]);
Reflect.defineMetadata("method", "GET", target[propertyKey]);
};
}
添加了元数据后,在下面代码中遍历类的原型上的方法名,然后拿到目标方法,就可以从元数据中获取路径和路由方法名,通过express
或者fastify
进行路由绑定。
function init() {
const prefix = Reflect.getMetadata("prefix", Controller) || "/";
const controllerPrototype = User.prototype;
// 遍历类的原型上的方法名
for (const methodName of Object.getOwnPropertyNames(controllerPrototype)) {
// 拿出login方法
const method = controllerPrototype[methodName];
// 取得此函数上绑定的方法名的元数据
const httpMethod = Reflect.getMetadata("method", method); // GET
// 取得此函数上绑定的路径的元数据
const pathMetadata = Reflect.getMetadata("path", method); // login
const routePath = path.posix.join("/", prefix, pathMetadata); // /user/login
// TODO: 有了路由的方法名和路径,就可以进行路由的绑定了
}
}
class-validator
这是一个可以用来验证类属性的库,通过装饰器来定义验证规则。
import { IsInt, Max, Min, validate } from "class-validator";
// 定义一个类
export class Post {
@IsInt() // 必须是整数
@Min(0) // 这个字段的值不能小于0
@Max(10) // 这个字段的值不能大于10
rating: number;
}
let post = new Post();
post.rating = 11; // 超过最大值
validate(post).then((errors) => {
// 未通过验证
if (errors.length > 0) {
console.log("validation failed. errors: ", errors);
} else {
console.log("validation succeed");
}
});
class-transformer
加入我们有一个 User dto 类
export class User {
@IsInt()
@Min(0)
@Max(100)
age: number;
@IsString()
name: string;
}
从请求体中获取到一个对象
const obj = {
name: "张三",
age: 18,
};
这时候如果想要将这个对象赋值给 User 类的实例,就需要使用 class-transformer
import { instanceToPlain, plainToClass } from "class-transformer";
export class User {
@IsInt()
@Min(0)
@Max(100)
age: number;
@IsString()
name: string;
}
const obj = {
name: "张三",
age: 18,
};
// 将普通对象转换为类实例
const ins = plainToClass(User, obj);
console.log(ins); // User类实例
这个 ins 也可以调用 class-validator
的 validate
方法,进行验证。
validate(ins).then((errors) => {
// 未通过验证
if (errors.length > 0) {
console.log("validation failed. errors: ", errors);
} else {
console.log("validation passed.");
}
});
如果想要将类实例转换为普通对象,可以使用 instanceToPlain
方法。
const newObj = instanceToPlain(ins);
console.log(newObj);
新版装饰器
TS5 支持的是 Stage3 装饰器,进入到 Stage3 的特性基本上就可以认为可以加入 js 标准了,在语法上,新版和传统装饰器有一些参数上的差异,要想使用新版装饰器,需要关闭experimentalDecorators
选项。
{
"compilerOptions": {
"experimentalDecorators": false,
"emitDecoratorMetadata": false
}
}
新版类装饰器
function logged(
value: any,
context: { kind: any; name: any; metadata: any; addInitializer: any }
) {
console.log(value); // 目标类 [class myClass]
// {kind:类型,name:类名,metadata:元数据,addInitializer:初始化函数}
console.log(context);
if (context.kind === "class") {
return class extends value {};
}
}
// @ts-ignore
@logged
class myClass {
constructor(private a: number) {}
}
new myClass(1);
export {};
新版方法装饰器
function logged(
value: Function, // 方法本身
context: {
kind: string; // method,表示类型是method
name: string; // 函数名: sum
static: boolean; // 是否是静态属性
private: boolean; //是否是私有属性
access: { has: Function; get: Function };
metadata?: any; // 装饰器元数据
addInitializer: Function;
}
) {
context.addInitializer(() => {
// 初始化的时候执行
console.log("initializing");
});
if (context.kind === "method") {
//说明这是一个类的方法装饰器
return function (...args: number[]) {
console.log(`starting ${context.name} with arguments ${args.join(",")}`);
const result = value.apply(null, args);
console.log(`ending ${context.name}`);
return result;
};
}
}
class Class {
// @ts-ignore
@logged
sum(a: number, b: number) {
return a + b;
}
}
const result = new Class().sum(1, 2);
console.log(result);
新版属性装饰器
function logged(value: any, context: any) {
console.log(value); // undefined
console.log(context);
// context如下
/**
*
kind: 'field',
name: 'x',
static: false,
private: false,
access: { has: [Function: has], get: [Function: get], set: [Function: set] },
metadata: undefined,
addInitializer: [Function (anonymous)]
*/
if (context.kind === "field") {
return function (initialValue: any) {
console.log(`initializing ${context.name} with value ${initialValue}`);
return initialValue + 1;
};
}
}
class Class {
// @ts-ignore
@logged x = 1;
}
console.log(new Class().x); // 2
export {};
新版访问器装饰器
function logged(value: any, context: any) {
console.log("value", value); // [Function: set x]
console.log("context", context);
/**
*
context {
kind: 'setter',
name: 'x',
static: false,
private: false,
access: { has: [Function: has], set: [Function: set] },
metadata: undefined,
addInitializer: [Function (anonymous)]
}
*/
if (context.kind === "getter" || context.kind === "setter") {
return function (...args: any[]) {
console.log(`starting ${context.name} with arguments ${args.join(",")}`);
const result = value.call(null, ...args);
console.log(`ending ${context.name}`);
return result;
};
}
}
class Class {
@logged
set x(args) {}
@logged
get x() {
return 2;
}
}
let clazz = new Class();
console.log(clazz.x);
export {};
总结
装饰器可以让我们的代码更加优雅易懂,期待未来能在前端生态中得到更多的应用。