装饰器的探讨

142 阅读7分钟

写在前面

最近了解学习NestJS框架,其中有一个特色,就是使用装饰器,将一些对类、方法、访问器、参数的额外的自定义操作写入装饰工厂函数,主流程语义化引用装饰器表达式即可,其简洁、优雅、可读性高,可实现依赖注入(DI)。

我们知道ES6和Typescript开始引入类的概念,当然我们也知道JS基于原型链实现继承的,类只是它的语法糖。但不妨碍与其他语言一样,基于它编写面向对象编程(OOP)的编程范式的代码及特性。

我们了解类的基本的概念,开始聊聊装饰器 tc39/proposal-decorators。目前装饰器处于ECMA规范提案阶段3,小伙伴一开始可能跟我一样,提案阶段属于实验功能,后面会不会有可能破坏性变更,这样的思维想法很合理正常,但也不用多虑,越来越多有优秀的框架和库开始使用,哪怕有破坏性变更, 可以参考或自行适配。还有阶段3,一般而言是较为成熟的阶段。

那就一起开始了解装饰器知识。

装饰器是一种扩展 JavaScript 类的提议。装饰器也是面向切面编程(AOP)的一种实现。

何为AOP呢? 面向切面编程是程序设计中非常经典的思想,它通过预编译或动态代理的方式来为已经编写完成的模块添加新的功能,从而避免了对源代码的修改, 也让开发者可以更方便地将业务逻辑功能和诸如日志记录、事务处理、性能统计、行为埋点等系统级的功能拆分开来,从而提升代码的复用性和可维护性。 就如axios库提供的interceptors拦截器机制;也像express和koa框架中所使用的中间件模型,用户自定义的处理函数依次添加到拦截器数组中,并在请求发送前或者响应返回后的特定“时间切面”上依次执行。

上述引述提到,OOP和AOP编程范式,网上看了这句描述不错。

OOP(面向对象编程)的关注点是梳理实体关系,它所要解决的问题是如何将具体的需求划分为相对独立且封装性良好的类,让它们具有自己的行为。 AOP(面向切面编程)的关注点则是剥离通用功能,让很多类共享一个行为,这样当它变化时只需要修改这个行为即可,它可以让开发者在实现类的特性时更加关注其本身的任务,而不是苦恼于将它归属于哪个类。

这些编程思想与范式,及实现方式值得我们继续了解与深入学习实践,在更多业务场景的解决方案才能游刃有余的去运用。

下面开始上图学习装饰器啦~

装饰器

装饰器.drawio.png

装饰器的引入

装饰器是一种尚未完全被 JavaScript 规范认可的语言特性。目前可以在JS内引入,或Ts引入。

JS环境引入,借助Babel,使用提案最新功能

我们知道Babel的是JS的编译器(将ECMA最新版本或者特性转化指定的版本),这就帮助我们可以在JS中使用。

官网搜索此插件 @babel/plugin-proposal-decorators,可以帮助我们书写装饰器啦 配置文件如下:

{
  "plugins": ["@babel/plugin-proposal-decorators"]
}

Ts环境引入 tsconfig.json配置项:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

装饰器的分类

上述图,能够直观的表达出几种装饰器装饰类的类型。

通过提案的中的Decorator类型声明如下:

type Decorator = (value: Input, context: {
  kind: string;
  name: string | symbol;
  access: {
    get?(): unknown;
    set?(value: unknown): void;
  };
  private?: boolean;
  static?: boolean;
  addInitializer?(initializer: () => void): void;
}) => Output | void;

其中kind装饰值的类型,可用于断言装饰器是否使用正确,及不同类型的值具有不同的类型。类型有:

  • class
  • method
  • getter
  • setter
  • field
  • accessor

具体装饰类型的声明或者用法,可通过Ecma TC39的装饰器的提案,去查阅使用。

装饰器的练习

装饰器是一种特殊的声明式语法。相比Jquery命令式的过程操作,声明式注重结果,具体到写法我们三大框架中也不陌生。将具体操作封装在框架底层或者表达式内。装饰器使用形式@expression的表达式,expression是运行时使用有关装饰声明的调用函数。

类(class)装饰器, 表达式中可调用函数参数1个,即:类的构造函数

方法(method)装饰器, 表达式中可调用函数参数3个,即:[

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的key
  3. 成员的属性描述符。 ]

访问器(getter、setter)装饰器,表达式中可调用函数参数3个,即:[

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的key
  3. 成员的属性描述符。 ]

属性装饰器(field), 表达式中可调用函数参数2个,即:[

  1. 静态成员的类的构造函数,或者实例成员的类的原型。
  2. 成员的key ]

参数装饰器(field),表达式中可调用函数参数3个,即:[

  1. 静态成员的类的构造函数,或者实例成员的类的原型
  2. 成员的姓名
  3. 函数参数列表中参数的序号索引 ]
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");

@describe('Animals that can fly')
class Bird {
    @foodOpt
    private eatFood: string
    private _feathersColor: string = 'black'

    constructor (public age: number, food: string = '') {
        this.eatFood = food
    }

    @enumerable(false)
    get feathersColor () {
        // 监听实例的此属性,便于额外的逻辑处理
        console.log('实例调用了获取羽毛颜色的操作')
        //...

        return this._feathersColor
    }

    @noticeElseSystem
    @enumerable(false)
    set feathersClor (color: string) {
        this._feathersColor = color
    }

    @validateFly(false)
    canFly () {
        return "I'm finally free to fly"
    }

    @enumerable(false)
    getBirdFood () {
        return `The ${this.age}-year-old eats ${this.eatFood}`
    }
    @validate
    setBirdFood (@required food: string) {
        this.eatFood = food
    }
}


const a = new Bird(1, 'worm')

// 类装饰器,对构造函数及构造函数的原型对象进行操作
console.log(Bird.des)
console.log(a.sayHello())

// 方法装饰器
console.log(a.canFly())
console.log(Object.getOwnPropertyNames(a)) // ["age", "_feathersColor", "eatFood"] 
console.log(Object.getOwnPropertyNames(Bird.prototype)) // ["constructor", "feathersColor", "feathersClor", "canFly", "getBirdFood", "sayHello"] 
console.log(Object.keys(Bird.prototype)) //  [ "sayHello"] 
console.log('getBirdFood' in a) // true

// 访问器装饰器
console.log(a.feathersColor)
a.feathersClor = 'red'
console.log(a.feathersColor)

// 属性装饰器
console.log(a.getBirdFood(), '[1]')
a.setBirdFood('big worms')
console.log(a.getBirdFood(), '[2]')


// 类装饰器工厂函数
// 类装饰器应用于类的构造函数,可用于观察、修改或替换类定义。
function describe (des: string) {
    return function (constructor: Function) {
        constructor.des = 'Animals that can fly'
        constructor.prototype.sayHello = () => console.log('The sky is beautiful')

    }
}

// 方法装饰器
// 装饰器应用于方法的属性描述符,可用于观察、修改或替换方法定义
function validateFly(bol: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        if (!bol) target.canFly = function () {
            return "sorroy, I can't do it"
        }
    }
}
// 访问器与方法装饰器参数一致
function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

// 访问器装饰器
function noticeElseSystem (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const set = descriptor.set!;

    descriptor.set = function (val) {
        console.log('实例调用了设置羽毛颜色的操作')
        //...

        set.call(this, val)
    }
}

// 属性装饰器
function foodOpt (target: any, propertyKey: string)  {
  let _val = target[propertyKey]

  // 属性读取访问器
  const getter = () => {
      console.log(`Get: ${propertyKey} => ${_val}`);
      return _val;
  };

  // 属性写入访问器
  const setter = (newVal:any) => {
      console.log(`Set: ${propertyKey} => ${newVal}`);
      _val = newVal;
  };

  // 删除属性
  if (delete target[propertyKey]) {
      // 创建新属性及其读取访问器、写入访问器
      Object.defineProperty(target, propertyKey, {
          get: getter,
          set: setter,
          enumerable: true,
          configurable: true
      });
  }
}

// 参数装饰器

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
  let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
 
function validate(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  let method = descriptor.value!;
 
  descriptor.value = function () {
    let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
    if (requiredParameters) {
      for (let parameterIndex of requiredParameters) {
        if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
          throw new Error("Missing required argument.");
        }
      }
    }
    return method.apply(this, arguments);
  };
}

元数据

随着装饰器发展,通过声明式语法拓展类及成员的能力。而元数据则补充了对生成数据层面的不足。

font1.jpg

font2.jpg

Vue中练习

vue开发中,目前我们熟知的API风格,Options(选项式) 和 Composition(组合式)API。

社区也有提供类(class)风格编写Vue组件,里面有大量的装饰器及元数据的应用。这个库就是Vue Class Component, 同时搭配vue-property-decorator,它提供了一些好用装饰器。

20220929-163758.jpg

如果我们项目中没有历史包袱,或者公司前端采用微前端架构,某部分业务也可以考虑使用这种风格进行开发,实践中可以学习,当然最快的啦。

拓展链接