Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
flytam opened this issue Dec 31, 2020 · 0 comments
Open

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

flytam opened this issue Dec 31, 2020 · 0 comments
Labels

Comments

@flytam
Copy link
Owner

flytam commented Dec 31, 2020

装饰器目前还处于提案阶段,要在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.colorList = [];
    }
    target.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

@flytam flytam added the 原生 label Dec 31, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant