阅读 857

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

github博客地址

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

为什么要用装饰器

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

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

class Network {
  async getList() {
    return await list();
  }
}
复制代码

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

class Network {
  async getList() {
    loading.show();
    const res = await list();
    loading.hide();
    return res;
  }
}
复制代码

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

实现一个loadingDecorator装饰器

function loadingDecorator(target, key, descriptor) {
  descriptor.value = async function (...args) {
    loading.show();
    await descriptor.value.apply(this, args);
    loading.hide();
  };
}
复制代码

使用我们的装饰器

class Network {
  @loadingDecorator
  async getList() {
    return await list();
  }
}
复制代码

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

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

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中,装饰器只能用于类,不能作用于普通函数。原因是函数会存在函数提升,设计者为了减少一些复杂性,可以参照一个讨论

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

function sealed(target) {
  // do something with 'target' ...
}

@sealed
class A {}
复制代码

其实就是类似于以下代码

A = sealed(A);
复制代码
  • 装饰器工厂

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

// 例如一个添加颜色的工厂装饰器
function addColor(color: string) {
  console.log("run", color);
  return function (target) {
    if (!target.colorList) {
      target.prototype.colorList = [];
    }
    target.prototype.colorList.push(color);
  };
}

@addColor("red")
class People {}

new People().colorList; // ['red']
复制代码
  • 多个装饰器组合 装饰器也是支持多个一起使用的,还是上面 color 例子,添加多个 不同的color的装饰器
@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 定义

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

type decorator = (target: Function) => Function | void;
复制代码

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

  • 无返回值
// 例如想直接修改一个class,给它新增一个静态方法
function addLog(target) {
  target.log = function () {
    console.log("hello world");
  };
}

@addLog
class People {}

People.log(); // 'hello world'
复制代码
  • 有返回值

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

// 例如想继承被装饰的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)

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

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直接语法定义的属性
class Target {
  a = 1;
}
复制代码

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

方法装饰器 (Method Decorators)

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

type decorator = (
  target: Target | Target.prototype,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => Function | void;
复制代码

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

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

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

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

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

const obj = { a: 1 };
Reflect.getOwnPropertyDescriptor(obj, "a");
/**
{value: 1, writable: true, enumerable: true, configurable: true}
**/
复制代码
function log(target, key, descriptor) {
  console.log(target, key, descriptor);
}
复制代码
  • 静态/原型方法装饰器给方法添加 log
// 静态或者动态方法添加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)

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

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.)

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 编译后的效果如下,地址

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这样写法。例如如下:

@Controller("/test")
class SomeClass {
  @Get("/a")
  someGetMethod() {
    return "hello world";
  }

  @Post("/b")
  somePostMethod() {}
}
复制代码

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

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

const PATH_METADATA = Symbol('path');

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

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

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 分别是请求路径和方法。

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

{
    [PATH_METADATA]: {
        UNDEIFINED: '/test'
        GET:{
            someGetMethod: '/test'
        },
        POST:{
            somePostMethod: '/test'
        }
    },
    [METHOD_METADATA]: {
        GET:{
            someGetMethod: '/a'
        },
        POST:{
            somePostMethod:'/b'
        }
    }
}
复制代码

取值并映射函数生成route

// 取值
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

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

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

文章分类
前端
文章标签