1. 理解装饰器

装饰器(Decorator) 其实是面向对象中的概念,在一些纯粹的面向对象的类型语言中早就有装饰器的内容了,在java中叫注解,在C# 中叫特征。装饰器并不是Typescript新引出的概念,是JavaScript本身就支持的内容。而且,出来的还特别早,在ES6的时候,就已经提出了装饰器。只不过,将近10年过去了,装饰器的规范几乎从头开始重写了好几次,但它还没有成为规范的一部分。到现在,2024年,也才刚刚进展到第3阶段不久

前些年随着面向对象语言的流行,JavaScript的装饰器也一直备受期待,不过由于 JavaScript 不是仅仅局限于基于浏览器的应用程序,规范的制定者必须考虑到可以执行 JavaScript 的各种平台上javascript上执行的情况,规范也迟迟未定下来。

不过世事变迁,现在纯前端的框架也来到了react18,vue3的时代,这两个框架都倾向于使用更加模块化和函数式的编程风格。这种风格更有利于实现摇树优化(Tree Shaking),这是现代前端构建工具(如 Webpack、Rollup)中的一个关键特性

不过Angular 就一直在广泛使用装饰器,还有nodejs流行的后端框架NestJS对装饰器也有很好的支持

无论怎么样,装饰器理论是很优秀的,对于我们对整个程序设计的理解是有帮助的。

1.1 装饰器模式

其实在程序设计中,一直有装饰器模式,它一种结构设计模式,通过将对象置于包含行为的特殊包装器对象中,可以将新的行为附加到对象上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 组件接口
class TextMessage {
constructor(message) {
this.message = message;
}

getText() {
return this.message;
}
}

// 装饰器基类
class MessageDecorator {
constructor(textMessage) {
this.textMessage = textMessage;
}

getText() {
return this.textMessage.getText();
}
}

// 具体装饰器
class HTMLDecorator extends MessageDecorator {
getText() {
const msg = super.getText();
return `<p>${msg}</p>`;
}
}

class EncryptDecorator extends MessageDecorator {
getText() {
const msg = super.getText();
// 加密逻辑
return this.encrypt(msg);
}
encrypt(msg) {
return msg.split("").reverse().join("");
}
}

// 使用
let message = new TextMessage("Hello World");
message = new HTMLDecorator(message);
message = new EncryptDecorator(message);

console.log(message.getText()); // 输出加密的 HTML 格式文本

这是面向对象的写法,其实在js中,我们也能写成函数式的,因为上面的写法,我们完全可以使用高阶函数替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 基础消息类
class TextMessage {
constructor(message) {
this.message = message;
}

getText() {
return this.message;
}
}

// 高阶函数 - HTML装饰器
function HtmlDecoratedClass(BaseClass) {
return class extends BaseClass {
getText() {
const originalText = super.getText();
return `<p>${originalText}</p>`;
}
};
}

// 高阶函数 - 加密装饰器
function EncryptDecoratedClass(BaseClass) {
return class extends BaseClass {
getText() {
const originalText = super.getText();
// 这里应该是你的加密逻辑
return this.encrypt(originalText);
}
encrypt(msg) {
// 简单处理加密
return msg.split("").reverse().join("");
}
};
}

// 使用装饰器
let DecoratedClass = HtmlDecoratedClass(TextMessage);
DecoratedClass = EncryptDecoratedClass(DecoratedClass);

const messageInstance = new DecoratedClass("Hello World");
console.log(messageInstance.getText()); // 输出被 HTML 格式化并加密的文本

1.2 装饰器的作用

这样很简单的实现了装饰器的设计模式,但是这样的代码实际上在工作中还是有一些问题,比如我们创建一个用户,然后可能在后期,我们需要对用户中的数据进行验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User { 
// 注意:严格检查(strict)不赋初始值会报错
// 演示可以设置 strictPropertyInitialization: false
loginId: string; // 必须是3-5个字符
loginPwd: string; // 必须是6-12个字符
age: number; // 必须是0-100之间的数字
gender: "男" | "女";
}

const u = new User();

// 对用户对象的数据进行验证
function validateUser(user: User) {
// 对账号进行验证
// 对密码进行验证
// 对年龄进行验证
// ...
}

这实际上,是要求对类的属性都需要进行处理,是不是就要进行装饰。我们下面的validateUser这个函数,实际就在处理这个问题。这咋一看没有什么问题,但是,其实应该在我们写类,写属性的时候,对这个属性应该怎么处理才是最了解的。而不是当需要验证的时候,再写函数进行处理。

当然,你可能会说,把validateUser这个函数写到类中去不就行了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User { 
// 注意:严格检查(strict)不赋初始值会报错
// 演示可以设置 strictPropertyInitialization: false
loginId: string; // 必须是3-5个字符
loginPwd: string; // 必须是6-12个字符
age: number; // 必须是0-100之间的数字
gender: "男" | "女";

validate() {
// 对账号进行验证
// 对密码进行验证
// 对年龄进行验证
// ...
}
}

可以,但是并没有解决我们提出的问题:当我们写这个类的属性的时候,对这个属性应该是最了解的。如果我们能在写属性的时候,就直接可以定义这些验证是最舒服的。

还有一个问题,现在仅仅是User这个类,那么我们还有其他的类需要验证,也是差不多的验证长度啊,是不是必须得啊,对这个属性的描述是什么啊,这些都需要。那我们肯定在另外一个类中,还需要来上validate函数这一套。能不能有一种语法机制,帮我们处理这个问题呢?

装饰器就可以帮我们解决这些问题

1、关注点分离:写属性,然后再写函数处理,其实就分离了我们的关注点

2、代码重复:不同的类可能只是属性不一样,但是可能需要验证,分析或者处理的内容实际上差不多

伪代码

1
2
3
4
5
6
7
class User { 
@required
@range(3, 5)
@description("账号")
loginId: string; // 中文描述:账号,验证:1.必填 2.必须是3-5个字符
......
}

这两个问题产生的根源其实就是我们在定义某些信息的时候,能够附加的信息有限,如果能给这些信息装饰一下,添加上有用的信息,就能处理了,这就是装饰器

所以装饰器的作用:为某些属性、类、方法、参数提供元数据信息(metadata)

又来一个名词:

元数据:描述数据的数据,上面的伪代码中,这三个装饰器@required @range(3, 5) @description("账号") 其实就是用来描述loginId这么一个数据的。其实meta这个词,我们早就见过,在html中,meta标签就是用来描述这个html文档信息的

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 文档编码 -->
<meta charset="UTF-8">
<!-- 视口尺寸 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>

还有著名的公司Facebook,改了名字,叫Meta,你就知道这个公司名真正的含义了

2. 装饰器的本质

无论如何,在JS中,装饰器的本质是什么?虽然作用是提供元数据,但是并不是一个简单数据就能搞定的,因此装饰器本身就是就是一个函数,并且装饰器是属于JS的,并不是简简单单的TS的类型检查,是要参与运行的。

装饰器可以修饰:

  • 成员(属性 + 方法)
  • 参数

3. tsconfig设置

由于现在装饰器没有正式形成规范,因此,在TS中使用装饰器,需要打开装饰器设置:

1
"experimentalDecorators": true

4. 类装饰器

类装饰器本质是一个函数,该函数接收一个参数,表示类本身(构造函数本身)

使用装饰器 @ 得到一个函数

在TS中,构造函数的表示

  • Function
  • new (...args:any[]) => any
1
2
3
4
5
6
7
// 定义为Function
function classDecoration(target: Function) {
console.log("classDecoration");
console.log(target)
}
@classDecoration
class A { }
1
2
3
4
5
6
7
// 定义为构造函数
function classDecoration(target: new (...args: any[]) => any) {
console.log("classDecoration");
console.log(target)
}
@classDecoration
class A { }

并且构造器是在定义这个类的时候,就会运行。而不是必须要等到你 new 对象的时候才会执行。

从执行之后的打印结果可以看出,target就是这个类本身:

1
2
classDecoration
[class A]

上面的代码,我们编译之后,是下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function classDecoration(target) {
console.log("classDecoration");
}
let A = class A {
};
A = __decorate([
classDecoration
], A);

其实通过编译之后的代码,就可以看到,直接运行了__decorate函数

4.1 泛型约束

我们之前讲过泛型构造函数,所以构造函数可以写成泛型的

1
2
3
4
5
6
7
8
// 泛型类型别名
type constructor<T = any> = new (...args: any[]) => T;
function classDecoration(target: constructor) {
console.log("classDecoration");
console.log(target)
}
@classDecoration
class A { }

这样,我们可以通过泛型约束,对要使用装饰器的类进行约束了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type constructor<T = any> = new (...args: any[]) => T;
type User = {
id: number
name: string
info(): void
}

function classDecoration<T extends constructor<User>>(target: T) {
console.log("classDecoration");
console.log(target)
}
@classDecoration
class A {
constructor(public id: number, public name: string) { }
info(){}
}

我们说装饰器其实是个函数,我们也能通过像函数那样调用,甚至传参,但是现在第一个参数target给固定限制了,该怎么处理呢?

4.2 装饰器工厂模式

我们可以工厂模式就能轻松解决这个问题,普通函数,返回一个装饰器函数就行了

1
2
3
4
5
6
7
8
9
10
11
type constructor<T = any> = new (...args: any[]) => T;
function classDecorator<T extends constructor>(str: string) {
console.log("普通方法的参数:" + str);
return function (target: T) {
console.log("类装饰器" + str)
}
}

@classDecorator("hello")
class A {
}

通过工厂模式既然能够返回一个函数,那么也能返回一个类,我们其实也能通过这种方式对原来的类进行修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type constructor<T = any> = new (...args: any[]) => T;

function classDecorator<T extends constructor>(target: T) {
return class extends target {
public newProperty = "new property";
public hello = "override";
info() {
console.log("this is info");
}
};
}
@classDecorator
class A{
public hello = "hello world";
}
const objA = new A();

console.log(objA.hello);
console.log((objA as any).newProperty); // 很明显,没有类型
(objA as any).info();
export {}

虽然可以这么做,但是很明显,返回的新的类,并不知道有新加的内容。

4.3 多装饰器

类装饰器不仅仅能写一个,还能写多个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type constructor<T = any> = new (...args: any[]) => T;
function classDecorator1<T extends constructor>(str: string) {
console.log("classDecorator1的参数:" + str);
return function (target: T) {
console.log("classDecorator1类装饰器" + str)
}
}
function classDecorator2<T extends constructor>(str: string) {
console.log("classDecorator2的参数:" + str);
return function (target: T) {
console.log("classDecorator2类装饰器" + str)
}
}


@classDecorator1("1")
@classDecorator2("2")
class A {
}

不过,注意执行之后的打印顺序:

1
2
3
4
classDecorator1的参数:1
classDecorator2的参数:2
classDecorator2类装饰器2
classDecorator1类装饰器1

装饰器的执行很明显是从下到上

5. 属性装饰器

属性装饰器也是一个函数,该函数至少需要两个参数

参数一: 如果是静态属性,为类本身;如果是实例属性,为类的原型

参数二: 字符串,表示属性名

1
2
3
4
5
6
7
8
9
10
11
function d(target: any, key: string) { 
console.log(target, key);
console.log(target === A.prototype);
}

class A {
@d
prop1: string;
@d
prop2: string;
}

当然,属性装饰器也能写成工厂模式:

1
2
3
4
5
6
7
8
9
10
11
12
function d() {
return function d(target: any, key: string) {
console.log(target, key)
}
}

class A {
@d()
prop1: string;
@d()
prop2: string;
}

也可以传值进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function d(value: string) { 
return function d(target: any, key: string) {
target[key] = value;
}
}

class A {
@d("hello")
prop1: string;
@d("world")
prop2: string;
}

console.log(A.prototype);

注意,target是类的原型,因此这里赋值其实是赋值在类原型上的,而不是实例上。

当属性为静态属性时,target得到的结果是A的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
function d() {
return function d(target: any, key: string) {
console.log(target, key)
}
}

class A {
@d()
prop1: string;
@d()
static prop2: string;
}

补充: 当你尝试通过装饰器给属性赋值时,它实际上是在原型上设置了这些值,这意味着所有实例将共享这些属性值,而不是每个实例拥有自己的独立值。

如果你要解决这个问题,你需要确保装饰器在每个类实例创建时为实例属性赋值。这通常是通过在构造函数中设置这些属性来完成的,但是由于装饰器不能直接访问类的构造函数,我们可以使用一点策略来解决。

下面的做法需要设置:"noImplicitAny": false,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function d(value: string) {
return function (target: any, key: string) {
if (!target.__initProperties) {
target.__initProperties = function () {
for (let prop in target.__props) {
this[prop] = target.__props[prop];
}
};
target.__props = {};
}
target.__props[key] = value;
};
}

class A {
@d("hello")
prop1: string;

@d("world")
prop2: string;

constructor() {
if (typeof this["__initProperties"] === "function") {
this["__initProperties"]();
}
}
}

const a = new A();
console.log(a.prop1); // Output: "hello"
console.log(a.prop2); // Output: "world"

6. 方法装饰器

方法装饰器也是一个函数,该函数至少需要三个参数

参数一: 如果是静态方法,为类本身*(类构造函数类型);如果是实例方法,为类的原型(对象类型)*

参数二: 字符串,表示方法名

参数三: 属性描述对象,其实就是js的Object.defineProperty()中的属性描述对象{value:xxx,writable:xxx, enumerable:xxx, configurable:xxx}

上节课属性不是也讲过参数一也是这种情况吗?如果非要区分开静态方法和实例方法,其实分开设置也行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function d0() {
return function d(target: Record<string,any>, key: string) {
console.log(target, key)
}
}
function d1() {
return function d(target: Record<string,any>, key: string, descriptor: PropertyDescriptor) {
console.log(target, key, descriptor)
}
}
function d2() {
return function d(target: new (...args:any[])=>any, key: string, descriptor: PropertyDescriptor) {
console.log(target, key, descriptor)
}
}

class A {
@d0()
prop1: string;
prop2: string;
@d1()
method1() { }
@d2()
static method2() { }
}

为了减少讲解的麻烦,这里还是直接用any

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function d() {
return function d(target: any, key: string, descriptor: PropertyDescriptor) {
console.log(target, key, descriptor)
}
}

class A {
prop1: string;
prop2: string;
@d()
method1(){}
}

const objA = new A();

for(let prop in objA){
console.log(prop)
}

结果:

1
2
3
4
5
6
7
8
{} method1 {
value: [Function: method1],
writable: true,
enumerable: false,
configurable: true
}
prop1
prop2

通过结果可以看到,方法默认并没有遍历,因为enumerable: false,那我们完全可以通过属性描述符进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function enumerable() {
return function d(target: any, key: string, descriptor: PropertyDescriptor) {
console.log(target, key, descriptor)
descriptor.enumerable = true;
}
}

class A {
prop1: string;
prop2: string;
@enumerable()
method1(){}
}

const objA = new A();

for(let prop in objA){
console.log(prop)
}

既然可以这么做,那么我们的操作性就大大增加了,比如我们完全可以修改属性描述符的value值,让其变为执行其他的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function enumerable() {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log(target, key, descriptor)
descriptor.enumerable = true;
}
}

// 被废弃的方法
function noUse() {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.value = function () {
console.log("被废弃的方法");
}
}
}

class A {
prop1: string;
prop2: string;
@enumerable()
method1() { }

@enumerable()
@noUse()
method2() {
console.log("正常执行......")
}
}

const objA = new A();

for(let prop in objA){
console.log(prop)
}
// 执行被废弃的方法
objA.method2();

甚至于,我们还能实现方法的拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function enumerable() {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
console.log(target, key, descriptor)
descriptor.enumerable = true;
}
}

// 被废弃的方法
function noUse() {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.value = function () {
console.log("被废弃的方法");
}
}
}

function interceptor(str: string) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const temp = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("前置拦截---" + str);
temp.call(this, args);
console.log("后置拦截---" + str);
}
}
}

class A {
prop1: string;
prop2: string;
@enumerable()
method1() { }

@enumerable()
@noUse()
method2() {
console.log("正常执行......")
}

@enumerable()
@interceptor("interceptor")
method3(str: string) {
console.log("正在执行 method3:" + str)
}
}

const objA = new A();

for(let prop in objA){
console.log(prop)
}
// 执行被废弃的方法
objA.method2();

// 拦截
objA.method3("hello world");

7. 访问器属性装饰器

参数一: 类的原型(对象类型)

参数二: 字符串,表示方法名

参数三: 属性描述对象,其实就是js的Object.defineProperty()中的属性描述对象{set:Function,get:Function, enumerable:xxx, configurable:xxx}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function d(str: string) {
return function d<T>(target: any, key: string, descriptor: TypedPropertyDescriptor<T>) {
console.log(target, key)
const temp = descriptor.set!;
descriptor.set = function (value: T) {
console.log("前置", str)
temp.call(this, value);
console.log("后置", str)
}
}
}

class User{
public id: number;
public name: string;
private _age: number;

@d("hello")
set age(v: number) {
console.log("set", v);
this._age = v;
}
}

const u = new User();
u.age = 10;

8. 方法参数装饰器

方法参数几乎和属性装饰器一致,只是多了一个属性

参数一: 如果是静态属性,为类本身;如果是实例属性,为类的原型

参数二: 字符串,表示方法名

参数三: 表示参数顺序

1
2
3
4
5
6
7
8
9
10
11
12
function paramDecorator(target: any, key: string, index: number) { 
console.log(target, key, index)
}

class A {
method1(@paramDecorator id: number, @paramDecorator name: string) {
console.log("---", id, name)
}
}

const objA = new A();
objA.method1(1, "hello");

当然,也能写成工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function paramDecorator() { 
return function(target: any, key: string, index: number) {
console.log(target, key, index)
}
}

class A {
method1(@paramDecorator() id: number, @paramDecorator() name: string) {
console.log("---", id, name)
}
}

const objA = new A();
objA.method1(1, "hello");

我们稍微处理一下,在原型上加上属性看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function paramDecorator(paramName: string) { 
return function(target: any, key: string, index: number) {
!target.__params && (target.__params = {});
target.__params[index] = paramName;
}
}

class A {
method1(@paramDecorator("id") id: number, @paramDecorator("name") name: string) {
console.log("---", id, name)
}
}

const objA = new A();
console.log(A.prototype); // { __params: { '0': 'id', '1': 'name' } }

9. reflect-metadata

reflect-metadata 是一个 JavaScript 库,用于在运行时访问和操作装饰器的元数据。它提供了一组 API,可以读取和写入装饰器相关的元数据信息。

我上面通过自己封装函数来处理类和类成员相关的元数据,但是相关能力比较薄弱,借助 Reflect-metadata 来解决提供元数据的处理能力。

9.1 安装

1
npm install reflect-metadata

tsconfig.json设置

1
2
"experimentalDecorators": true,
"emitDecoratorMetadata": true

引入

1
import "reflect-metadata";

9.2 基本语法

9.2.1 定义元数据

声明性定义:

1
@Reflect.metadata(metadataKey, metadataValue)
1
2
3
4
5
@Reflect.metadata("classType", "A类-1")
class A {
prop1: string;
method() { }
}

命令式定义:

1
Reflect.defineMetadata(metadataKey, metadataValue, 定义元数据的对象, propertyKey?);
1
2
3
4
5
6
class A { 
prop1: string;
method() { }
}

Reflect.defineMetadata("classType", "A类-2", A);

9.2.2 获取元数据

1
Reflect.getMetadata(metadataKey, 定义元数据类):返回metadataValue
1
console.log(Reflect.getMetadata("classType", A));

9.3 工厂模式

也可以将上面的处理封装为工厂模式,使用起来更加方便

方式1:

1
2
3
4
5
6
7
8
9
10
11
12
13
const ClassTypeMetaKey = Symbol("classType");

function ClassType(type: string) {
return Reflect.metadata(ClassTypeMetaKey, type);
}

@ClassType("A类-1")
class A {
prop1: string;
method() { }
}

console.log(Reflect.getMetadata(ClassTypeMetaKey, A));

方式2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type constructor<T = any> = new (...args: any[]) => T;

const ClassTypeMetaKey = Symbol("classType");

function ClassType(type: string) {
return <T extends constructor>(target:T) => {
Reflect.defineMetadata(ClassTypeMetaKey, type, target);
}
}

@ClassType("A类-2")
class A {
prop1: string;
method() { }
}

console.log(Reflect.getMetadata(ClassTypeMetaKey, A));

9.4 成员属性和方法的处理

基本语法API都基本差不多,不过属性和方法是有两种状态的,实例的和静态的,对应的对象分别是对象原型和类本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A{
// @Reflect.metadata("propType1", "prop1-value")
prop1: string;
// @Reflect.metadata("propType2", "prop2-value")
static prop2: string;

@Reflect.metadata("methodType1","method1-value")
method1() { }

@Reflect.metadata("methodType2","method2-value")
static method2() {}
}

Reflect.defineMetadata("propType1", "prop1-value", A.prototype, "prop1");
Reflect.defineMetadata("propType2", "prop2-value", A, "prop2");

console.log(Reflect.getMetadata("propType1", A.prototype, "prop1"));
console.log(Reflect.getMetadata("propType2", A, "prop2"));

console.log(Reflect.getMetadata("methodType1", A.prototype, "method1"));
console.log(Reflect.getMetadata("methodType2", A, "method2"));

我们可以稍微封装一下,简单的得到一些我们想要的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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);
}
}

const objG = new Greeter("world");
// console.log(objG.greet()); // "Hello, world"

const objG = new Greeter("world");
// console.log(objG.greet());

// greet封装在外面也是一样的道理
function greet(obj: any, key: string) {
let formatString = getFormat(obj, key);
return formatString.replace("%s", obj[key]);
}

const g = greet(objG, "greeting");
console.log(g);

10. class-transformer

给大家介绍两个基于reflect-metadata元数据实现的比较有用的功能库

class-transformer可以很方便的将普通对象转换为类的某些实例,这个功能在某一些时候非常好用。

比如我们在很多时候,从后端获取的数据,都是一些简单的json格式的数据,有些数据可能需要经过前端的再处理,如下:

1
2
3
4
5
6
{
"id": 1,
"firstName": "Nancy",
"lastName": "Lopez",
"age": 35
}

为了简单方便,可以使用远程的mock模拟数据,比如easy mock,直接简单登录之后即可使用,使用过程就两步:

1、创建项目

2、创建接口

再复杂一点的时候可以自己去阅读网站的文档

我们可以创建如下的简单数据

1
2
3
4
5
6
7
8
9
10
{
code: 200,
"data|10": [{
"id|+1": 1,
"firstName": "@first",
"lastName": "@last",
"age|9-45": 1
}],
msg: "成功"
}

从后端获取的是上面的数据,可能前端还需要一些功能,比如获取全名,比如判断是否成年,我们可以创建一个类进行封装处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class User { 
id: number
firstName: string
lastName: string
age: number

getFullName() {
return this.firstName + " "+ this.lastName;
}

isAdult() {
return this.age > 18 ? "成年人" : "未成年人";
}
}

// 模拟数据返回格式
interface Result<T> {
code: number;
data: T;
msg: string
}

这在我们获取数据的时候,如果直接获取的就是简单json数据,倒是没什么影响,但是不能访问自己封装的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
fetch("https://mock.mengxuegu.com/mock/65b1f3d1c4cd67421b34cd0c/mock_ts/list")
.then(res => res.json())
.then( (res:Result<User[]>) => {
console.log(res.code);
console.log(res.msg);

const users = res.data;

for (const u of users) {
console.log(u.id + " " + u.firstName);
// console.log(u.id + " " + u.getFullName() + " " + u.isAdult()); //error
}
})

这里,就可以使用class-transformer它可以自动的将数据和我们封装的类进行映射,使用也非常的简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import "reflect-metadata"
import { plainToInstance } from 'class-transformer';

fetch("https://mock.mengxuegu.com/mock/65b1f3d1c4cd67421b34cd0c/mock_ts/list")
.then(res => res.json())
.then( (res:Result<User[]>) => {
console.log(res.code);
console.log(res.msg);

const users = res.data;
const us = plainToInstance(User, users);

for (const u of us) {
// console.log(u.id + " " + u.firstName);
console.log(u.id + " " + u.getFullName() + " " + u.isAdult());
}
})

这样就正常的获取了User类所修饰的内容

11. class-validator

这个库同样是基于reflect-metadata元数据实现的比较有用的功能库,通过名字大家就知道,这个库可以用来对类进行验证

这个库使用也非常的简单,基本也就知晓两步就ok

1、相关装饰器的绑定

2、验证方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "reflect-metadata";
import { validate, IsNotEmpty, Length, Min, Max, IsPhoneNumber } from "class-validator";

class User {
@IsNotEmpty({ message: "账号不能为空" })
@Length(3, 5, { message: "账号必须是3-5个字符" })
loginId: string;

@Min(9)
@Max(45)
age: number;

@IsPhoneNumber("CN")
tel: string;
}

const u = new User();

validate(u).then(errors => {
console.log(errors)
})