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());
|
这是面向对象的写法,其实在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; } }
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());
|
1.2 装饰器的作用
这样很简单的实现了装饰器的设计模式,但是这样的代码实际上在工作中还是有一些问题,比如我们创建一个用户,然后可能在后期,我们需要对用户中的数据进行验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class User { loginId: string; loginPwd: string; age: number; 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 { loginId: string; loginPwd: string; age: number; gender: "男" | "女";
validate() { } }
|
可以,但是并没有解决我们提出的问题:当我们写这个类的属性的时候,对这个属性应该是最了解的。如果我们能在写属性的时候,就直接可以定义这些验证是最舒服的。
还有一个问题,现在仅仅是User这个类,那么我们还有其他的类需要验证,也是差不多的验证长度啊,是不是必须得啊,对这个属性的描述是什么啊,这些都需要。那我们肯定在另外一个类中,还需要来上validate
函数这一套。能不能有一种语法机制,帮我们处理这个问题呢?
装饰器就可以帮我们解决这些问题
1、关注点分离:写属性,然后再写函数处理,其实就分离了我们的关注点
2、代码重复:不同的类可能只是属性不一样,但是可能需要验证,分析或者处理的内容实际上差不多
伪代码
1 2 3 4 5 6 7
| class User { @required @range(3, 5) @description("账号") loginId: string; ...... }
|
这两个问题产生的根源其实就是我们在定义某些信息的时候,能够附加的信息有限,如果能给这些信息装饰一下,添加上有用的信息,就能处理了,这就是装饰器
所以装饰器的作用:为某些属性、类、方法、参数提供元数据信息(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 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); console.log(a.prop2);
|
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);
|
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{ prop1: string; 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");
const objG = new Greeter("world");
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);
|
给大家介绍两个基于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); } })
|
这里,就可以使用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.getFullName() + " " + u.isAdult()); } })
|
这样就正常的获取了User类所修饰的内容
这个库同样是基于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) })
|