2020的最后一天,不妨了解下装饰器
flytam Lv4

装饰器目前还处于提案阶段,要在javascript中使用装饰器,我们必须借助babeltypescript的转码能力

为什么要用装饰器

引入装饰器更能够便于代码逻辑的解藕和复用。举一个例子

举一个非常常见的需求。假设我们有一个类Network,它有一个异步getList方法

1
2
3
4
5
class Network {
async getList() {
return await list();
}
}

有一天,我们想给它加个全局loading,那么我们可能会这么写

1
2
3
4
5
6
7
8
class Network {
async getList() {
loading.show();
const res = await list();
loading.hide();
return res;
}
}

如果需要对另一个方法使用全局 loading,可能又需要再写一遍。并且这个代码还入侵了函数本身的逻辑。这时候使用装饰器就可以相对优雅解决这个问题。

实现一个loadingDecorator装饰器

1
2
3
4
5
6
7
function loadingDecorator(target, key, descriptor) {
descriptor.value = async function (...args) {
loading.show();
await descriptor.value.apply(this, args);
loading.hide();
};
}

使用我们的装饰器

1
2
3
4
5
6
class Network {
@loadingDecorator
async getList() {
return await list();
}
}

这样,每当一个方法需要加 loading 的时候,给它使用@loadingDecorator装饰器即可。这样即逻辑解藕又能实现比较好的代码复用

经过typescript转码后的代码长这样,感兴趣的同学可以看看

1
2
3
4
5
6
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;
};

什么是装饰器

装饰器是一种特殊的声明,可以作用在类的声明、方法、属性、访问器或者参数上。装饰器的用法是@decoratordecorator是一个函数,会在运行时的时候调用,对类进行一些修改。需要注意的是,在javascript中,装饰器只能用于类,不能作用于普通函数。原因是函数会存在函数提升,设计者为了减少一些复杂性,可以参照一个讨论

如下就是定义一个装饰器函数,并且作用在类上

1
2
3
4
5
6
function sealed(target) {
// do something with 'target' ...
}

@sealed
class A {}

其实就是类似于以下代码

1
A = sealed(A);
  • 装饰器工厂

装饰器本质就是一个函数,所以也可以利用闭包的能力实现更多功能。装饰器工厂就是一个返回函数的函数,运行时将会被调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 例如一个添加颜色的工厂装饰器
function addColor(color: string) {
console.log("run", color);
return function (target) {
if (!target.colorList) {
target.colorList = [];
}
target.colorList.push(color);
};
}

@addColor("red")
class People {}

new People().colorList; // ['red']
  • 多个装饰器组合
    装饰器也是支持多个一起使用的,还是上面 color 例子,添加多个 不同的color的装饰器
1
2
3
4
5
6
7
8
9
@addColor("blue")
@addColor("red")
@addColor("yellow")
class People {}

// log: run blue
// log: run red
// log: run yellow
new People().colorList; // ['yellow','red','blue']

从上面的信息,可以知道。

  • 装饰器定义的执行顺序是从上到下
  • 装饰器运行时装饰 class 的顺序是从下到上

装饰器的基本用法

类装饰器 (Class Decorators)

类装饰器作用于类的构造函数,可用于修改或者替换一个 class 定义

一个装饰器函数签名如下:

1
type decorator = (target: Function) => Function | void;

它接收被装饰的 class 作为target函数的参数,如果装饰器函数有返回值,则使用这个返回值作为新的 class。

  • 无返回值
1
2
3
4
5
6
7
8
9
10
11
// 例如想直接修改一个class,给它新增一个静态方法
function addLog(target) {
target.log = function () {
console.log("hello world");
};
}

@addLog
class People {}

People.log(); // 'hello world'
  • 有返回值

当然,上面有返回值的形式直接返回也行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 例如想继承被装饰的class
function logName(target) {
return class extends target {
log() {
console.log(this.name);
}
};
}

@logName
class People {
name = "hello world";
}

new People().log(); // hello world

类成员装饰器

下面列举的几个都是装饰到类的成员上,所以都可以归为一类

属性装饰器 (Property Decorators)

属性装饰器用于装饰属性,函数签名如下

1
2
3
4
type decorator = (
target: Target | Target.prototype,
propertyKey: string
) => void;

属性装饰器的参数定义如下:

1、第一个参数。如果装饰的是静态方法,则是这个类 Target 本身;如果装饰的是原型方法,则是类的原型对象 Target.prototype

2、第二个参数。这个属性的名称

属性装饰器的返回值是被忽略的,所以如果需要修改属性值。分两种情况

  • 静态属性,可以直接使用Object.getOwnPropertyDescriptor(target, propertyKey)Object.defineProperty(target,propertyKey,{})来获取和修改descriptor
  • 如果是实例属性,则不能直接很方便的进行修改,因为 class 还没有进行实例化。何为定义实例属性,即如通过babel-plugin-proposal-class-properties直接语法定义的属性
1
2
3
class Target {
a = 1;
}

但这样的装饰器也不是没有作用,在 typescript 中可以很方便的收集元类型信息,后面的文章会说到

方法装饰器 (Method Decorators)

方法装饰器就是用来装饰方法,可以用来修改方法的定义。方法装饰器的函数签名如下

1
2
3
4
5
type decorator = (
target: Target | Target.prototype,
propertyKey: string,
descriptor: PropertyDescriptor
) => Function | void;

方法装饰器的参数定义如下:

1、第一个参数。如果装饰的是静态方法,则是这个类Target本身;如果装饰的是原型方法,则是类的原型对象Target.prototype

2、第二个参数。这个方法的名称

3、第三个参数,这个方法的属性描述符,通过descriptor.value可以直接拿到这个方法

如果属性装饰器有返回值,这个返回值讲作为这个方法的属性描述符。对象的属性描述符就是调用Reflect.getOwnPropertyDescriptor(target, propertyKey)的返回值,详细可见

1
2
3
4
5
const obj = { a: 1 };
Reflect.getOwnPropertyDescriptor(obj, "a");
/**
{value: 1, writable: true, enumerable: true, configurable: true}
**/
1
2
3
function log(target, key, descriptor) {
console.log(target, key, descriptor);
}
  • 静态/原型方法装饰器给方法添加 log
1
2
3
4
5
6
7
8
// 静态或者动态方法添加log
function log(target, key, descriptor) {
const origin = descriptor.value;
descriptor.value = function (...args) {
console.log("静态log: ", key);
origin.apply(this, args);
};
}
访问器装饰器 (Accessor Decorators)

参数装饰器 (Parameter Decorators)

参数装饰器的函数签名如下

1
2
3
4
5
type decorator = (
target: Target | Target.prototype,
propertyKey: string,
parameterIndex: number
) => void;

参数装饰器的参数定义如下:

1、第一个参数。如果装饰的是静态方法的参数,则是这个类Target本身;如果装饰的是原型方法的参数,则是类的原型对象Target.prototype

2、第二个参数。参数所处的函数名称

3、第三个参数,该参数位于函数参数列表的位置下标(number)

各种装饰器的执行顺序

如下:

1、先执行实例成员装饰器(非静态的),再执行静态成员装饰器

2、执行成员的装饰器时,先执行参数装饰器,再执行作用于成员的装饰器

3、执行完 1、2 后,执行构造函数的参数装饰器;最后执行作用于 class 的装饰器

typescript 更强大的装饰器

vue-property-decorator中的应用

上面提到的一些用法更多是 javascript 场景中使用装饰器优化我们代码的结构,在typescript中,装饰器还有有一个更强大的功能,就是能在运行时去拿到我们在typescript定义的时候类型信息。

如果用过typescriptvue的同学,一般会用到vue-decorator-property这个库。在Prop我们可以看到文档这样写

If you’d like to set type property of each prop value from its type definition, you can use reflect-metadata.
Set emitDecoratorMetadata to true.
Import reflect-metadata before importing vue-property-decorator (importing reflect-metadata is needed just once.)

1
2
3
4
5
6
7
import "reflect-metadata";
import { Vue, Component, Prop } from "vue-property-decorator";

@Component
export default class MyComponent extends Vue {
@Prop() age!: number;
}

我们就不需要去在Propoptions的 type 再去定义一遍这个属性告诉 vue 了。这个能力正是typescriptemitDecoratorMetadata特性提供的。我们看上面的代码经过 ts 编译后的效果如下,地址

1
2
3
4
5
6
7
8
9
10
11
12
import { __decorate, __metadata } from "tslib";
import "reflect-metadata";
import { Vue, Component, Prop } from "vue-property-decorator";
let MyComponent = class MyComponent extends Vue {};
__decorate(
[Prop(), __metadata("design:type", Number)],
MyComponent.prototype,
"age",
void 0
);
MyComponent = __decorate([Component], MyComponent);
export default MyComponent;

可见我们的类型信息被收集到 metadata 的design:type中,通过reflect-metadata提供的一些方法我们就能在运行时拿到这个类型信息。

可以理解为将每个被装饰的类/属性/方法的类型存放到一个全局的地方,key 为design:type。后续处理的时候可以通过class/method/key拿到这个类型信息,做一些我们想做的事情。

在 node 中的应用

来自深入理解 typescript的例子

如果我们想基于 class 声明编写 http 接口,而不是写很多router.get/router.post这样写法。例如如下:

1
2
3
4
5
6
7
8
9
10
@Controller("/test")
class SomeClass {
@Get("/a")
someGetMethod() {
return "hello world";
}

@Post("/b")
somePostMethod() {}
}

很显然,这里我们是定义了两个接口,分别是/test/atest/b。这里的关键就在于实现ControllerPost/Get装饰器

Controller作用于 class 上,我们定义一个元信息key并使用Reflect.defineMetadata存对应的元信息

1
2
3
4
5
6
7
const PATH_METADATA = Symbol('path');

const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
}
}

再实现一个工厂装饰器,返回Get/Post

1
2
3
4
5
6
7
8
9
10
const PATH_METADATA = Symbol('path');
const METHOD_METADATA = Symbol('method');
const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path,target, key;
Reflect.defineMetadata(METHOD_METADATA, method, target, key);
}
}
const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

createMappingDecorator接收一个参数(表示这是Get还是Post),返回一个装饰器。装饰器调用defineMetadata存了PATH_METADATAMETHOD_METADATA两个 key,value 分别是请求路径和方法。

所以综上装饰后,可以类比一个以下形式的存储结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
[PATH_METADATA]: {
UNDEIFINED: '/test'
GET:{
someGetMethod: '/test'
},
POST:{
somePostMethod: '/test'
}
},
[METHOD_METADATA]: {
GET:{
someGetMethod: '/a'
},
POST:{
somePostMethod:'/b'
}
}
}

取值并映射函数生成route

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
// 取值
function mapRoute(instance: Object) {
const prototype = Object.getPrototypeOf(instance);

// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype)
.filter(item => !isConstructor(item) && isFunction(prototype[item]));
return methodsNames.map(methodName => {
const fn = prototype[methodName];

// 通过metadataKey, target, propertyKey取出定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, instance, methodName);// /a or /b
const method = Reflect.getMetadata(METHOD_METADATA, instance, methodName);// GET or POST
return {
route,
method,
fn,
methodName,
pre
}
})
};

Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

mapRoute(new SomeClass());
/**
* [{
* route: '/a',
* method: 'GET',
* fn: someGetMethod() { ... },
* methodName: 'someGetMethod'
* },{
* route: '/b',
* method: 'POST',
* fn: somePostMethod() { ... },
* methodName: 'somePostMethod'
* }]
*
*/

最后,只需把 route 相关信息绑在对应的http框架上即可

reflect-metadata更多api可以参考

typedi

最后再简单介绍介绍typedi

引用文档的介绍。

typedi是一个 typescript(javascript)的依赖注入工具,可以在 node.js 和浏览器中构造易于测试和良好架构的应用程序。主要有以下特性:

  • 基于属性/构造函数的依赖注入
  • 单例/临时服务
  • 可以支持多个container

官网例子,非常方便实现依赖注入使用

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
import { Container, Service } from 'typedi';

@Service()
class ExampleInjectedService {
printMessage() {
console.log('I am alive!');
}
}

@Service()
class ExampleService {
constructor(
// because we annotated ExampleInjectedService with the @Service()
// decorator TypeDI will automatically inject an instance of
// ExampleInjectedService here when the ExampleService class is requested
// from TypeDI.
private injectedService: ExampleInjectedService
) {}
}

const serviceInstance = Container.get(ExampleService);
// we request an instance of ExampleService from TypeDI

serviceInstance.injectedService.printMessage();
// logs "I am alive!" to the console

最后

码字不易,一键三连的人明年会有好运哦,祝大家新年快乐!!!

参考资料

typescript Decorators

深入理解 typescript

  • Post title:2020的最后一天,不妨了解下装饰器
  • Post author:flytam
  • Create time:2020-12-31 17:42:05
  • Post link:https://blog.flytam.vip/2020的最后一天,不妨了解下装饰器.html
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.