ts的装饰器
ts的装饰器(Decorators) 跟Spring的注解(Annotation) 看起来很像。他们的设计理念都来自装饰器模式.
装饰器模式
在不改变对象自身结构的前提下,向对象添加新的功能
首先通过下面2种方式打开装饰器支持.
命令行开启
tsc --target ES5 --experimentalDecorators
配置tsconfig.json文件
执行tsc --init
命令生成tsconfig.json
配置文件后将experimentalDecorators
设置为true
{
"compilerOptions": {
"target": "ES5",
/* Enable experimental support for legacy experimental decorators. */
"experimentalDecorators": true
}
}
1.装饰器
装饰器的本质是函数,通过@expression
的方式使用。
装饰器分为5类:
装饰器种类 |
---|
Class Decorators |
Method Decorators |
Accessor Decorators |
Property Decorators |
Parameter Decorators |
虽然很想马不停蹄的介绍接下来几个装饰器,但是在此之前不得不先介绍一下装饰器工厂
1.1 装饰器工厂
装饰器工厂同样本质上也是1个function
.装饰器工厂返回的表达式,供装饰器在运行时调用.
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
@color('blue')
看上去是不是和注解一模一样?在装饰器工厂中变量target
依据装饰器type
的不同可能是
- the constructor function of the class for a static member
- the prototype of the class for an instance member.
而装饰器工厂这样的写法用到了柯里化编程.
Currying
是一种编程技术,它可以将一个固定数量参数的函数转换为一个链式调用的函数,允许其参数分阶段提供。例如,add(x,y,z)
会允许它像 add(x)(y)(z)
或 add(x)(y,z)
等方式进行调用
// A curried function
let add = (x: number) => (y: number) => (z: number) => z + x + y;
// Simple usage
console.log(add(1)(4)(2));// Prints "7"
// partially applied
let add123 = add(123);
// fully apply the function
console.log(add123(456)(1000));// Prints "1579"
// 非箭头函数形式
function currying(x: number) {
console.log('curry X');
return function (y: number) {
console.log('curry Y');
return function (z: number) {
console.log('curry Z');
return x + z + y
}
}
}
console.log(currying(1)(2)(3));// Prints "6"
最后的展开形式看起来是不是跟装饰器工厂一模一样?
好了,终于到了介绍装饰器的环节😋
2. Class Decorators
类装饰器表达式会在运行时当作function
被调用,类的constructor
作为其唯一的参数
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明.这也是需要extends
的原因.
function decorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
// 在这里可以添加新的属性或方法
};
}
下面用一个详细的例子来说明吧,选自Class Decorators
function reportableClassDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
console.log(constructor); // Prints "[class BugReport]"
return class extends constructor {
reportedUser = "J4ck";
title = "Needs DARK mode"
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
// reportedUser!: string
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("Needs dark mode");
//只有在class definition中声明的属性才能直接调用
console.log(bug.type); // Prints "report"
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
//Property 'reportedUser' does not exist on type 'BugReport'.
console.log(bug.reportingURL);
/* decorator不会改变类型,因此为了能够调用有2种方法 */
/* 1.为该类添加该属性 */
/* 2.该类推导为any类型突破 _类型限制_ */
console.log((bug as any).reportedUser); // Prints "J4ck"
3. Method Decorators
在TypeScript中,方法装饰器是一种特殊类型的声明,它能够被附加到类的方法上。方法装饰器表达式会在运行时当作函数被调用,需要传入以下3个参数
参数 | 类型 | 是什么 |
---|---|---|
target | any | 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 |
propertyKey | string | 方法名 |
descriptor | PropertyDescriptor | 成员的属性描述符 |
我觉得这个例子非常好,还能顺便聊一聊装饰器的执行顺序
@expression(arg: type)
先从外层到内层(如果是函数工厂的话)@expression
时,是从内层到外层
function fn(str: string) {
console.log("求值装饰器:", str);
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("应用装饰器:", str);
};
}
function decorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("应用装饰器: decorator");
}
class T {
@fn("first")
@decorator
@fn("second")
method() { }
}
/*
求值装饰器: first
求值装饰器: second
应用装饰器: second
应用装饰器: decorator
应用装饰器: first
*/
4. Property Decorators
属性装饰器能够被附加到类的property
.属性装饰器表达式会在运行时当作函数被调用,需要传入以下2个参数:
参数 | 类型 | 是什么 |
---|---|---|
target | any | 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 |
propertyKey | string | 属性名 |
请注意,属性装饰器不能直接修改属性的值,修改的是原型中的值
import "reflect-metadata"; // npm install reflect-metadata
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
let greet = new Greeter('J4ck')
console.log(greet.greet()); // Prints: Hello, J4ck
5. Parameter Decorators
参数装饰器能够被附加到类的property
.参数装饰器表达式会在运行时当作函数被调用,需要传入以下3个参数:
参数 | 类型 | 是什么 |
---|---|---|
target | any | 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象 |
propertyKey | string | 方法名 |
parameterIndex | number | 参数在函数参数列表中的索引 |
参数装饰器和属性装饰器很像,因为装饰器在类声明后就已经确定并调用了,而属性需要实例化后才能确定,参数则是需要在function
调用时才能确定.
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
console.log(`Parameter ${parameterIndex} of method ${propertyKey} of class ${target.constructor.name} was accessed.`);
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
setGreeting(@logParameter greeting: string) {
this.greeting = greeting
}
}
// Prints "Parameter 0 of method setGreeting of class Greeter was accessed."
同样是1个很好的例子,我稍微将其改了1下,来自于一起读透TS装饰器#小试牛刀:方法参数类型校验.不过这个小例子使用了Metadata
,如果不熟悉的可以到第6章了解1下然后自行学习.
type Validator = (value: unknown) => boolean;
const isString = applyValidator((x) => typeof x === "string");
const isNumber = applyValidator((x) => typeof x === "number");
class Person {
@validate
saySomething(@isString a: any, @isNumber b: any) {
console.log(`a: ${a} || b: ${b}`);
}
}
function applyValidator(validator: Validator) {
return function (target: any, key: string, idx: number) {
let validators: Validator[] = [];
// validatorMap 用于收集不同方法参数校验器
let validatorMap: Map<string, Validator[]> = Reflect.getMetadata(key, target)
if (validatorMap) {
if (validatorMap.has(key)) {
//validatorMap && key 都存在
validators = (validatorMap.get(key) as Validator[]);
}
} else {
// validatorMap不存在_new_1个
validatorMap = new Map<string, Validator[]>();
}
/*
1. validatorMap不存在,刚new所以key不存在
2. validatorMap存在,但该key不存在
将新的检验器加入到数组中,数组第几项就对应第几个参数的校验器
出于简化目的,假设每一个参数最多只能有一个校验器
*/
validators[idx] = validator;
validatorMap.set(key, validators);
//将validatorMap通过defineMetadata重新赋值回去
Reflect.defineMetadata(key, validatorMap, target);
};
}
function validate(target: any, key: string, descriptor: PropertyDescriptor) {
const origin = descriptor.value;
descriptor.value = function (...args: unknown[]) {
// 获取validatorMap元数据
let validatorMap: Map<string, Validator[]> = Reflect.getMetadata(key, target)
// 如果该方法不需要校验,则直接运行原方法
if (!validatorMap.has(key)) {
return origin.apply(this, args);
}
let validators = validatorMap.get(key);
//先对方法的每一个参数进行校验,遇到不符合规则的情况,直接报错
if (validators) {
validators.forEach((validator, idx) => {
if (!validate) {
return;
}
if (!validator(args[idx])) {
throw new TypeError(`Type validate failed for parameterIndex\[${idx}\]:${args[idx]}`);
}
});
}
// 所有校验通过后再运行原方法
return origin.apply(this, args);
};
}
// Prints "a: str || b: 12"
new Person().saySomething("str", 12);
// Prints "Type validate failed for parameterIndex[0]:12"
new Person().saySomething(12, 12);
// Prints "Type validate failed for parameterIndex[1]:other str"
new Person().saySomething("str", "other str");
6. Metadata
- 命令行下载
npm install reflect-metadata
- 开启Metadata支持
命令行开启
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
配置tsconfig.json
开启
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Metadata
的项目地址👉reflect-metadata