如何用 Decorator 装饰你的 Typescript?

Posted 奇舞精选

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何用 Decorator 装饰你的 Typescript?相关的知识,希望对你有一定的参考价值。


如何用 Decorator 装饰你的 Typescript?

前言

正在着手写 THE LAST TIME 系列的 Typescript 篇,而Decorator 一直是我个人看来一个非常不错的切面方案。所谓的切面方案就是我们常说的切面编程 AOP。一种编程思想,简单直白的解释就是,一种在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是 AOP。AOP 和我们熟悉的 OOP 一样,只是一个编程范式,AOP 没有说什么规定要使用什么代码协议,必须要用什么方式去实现,这只是一个范式。而 Decorator 也就是AOP 的一种形式。

而本文重点不在于讨论编程范式,主要介绍 Typescript+Decorator 下图的一些知识讲解,其中包括最近笔者在写项目的一些应用。

介绍

什么是 Decorator

本质上,它也就是个函数的语法糖。

DecoratorES7 添加的新特性,当然,在 Typescript 很早就有了。早在此之前,就有提出与 Decorator 思想非常相近的设计模式:装饰者模式

如何用 Decorator 装饰你的 Typescript?

上图的WeaponAccessory就是一个Decorator,他们添加额外的功能到基类上。让其能够满足你的需求。

如何用 Decorator 装饰你的 Typescript?

简单的理解 Decorator,可以认为它是一种包装,对 对象,方法,属性的包装。就像 Decorator 侠,一身盔甲,只是装饰,以满足需求,未改变是人类的本质。

为什么要使用 Decorator

为什么要使用 Decorator,其实就是介绍到 AOP 范式的最大特点了:非侵入式增强。

比如笔者正在写的一个页面容器,叫 PageContainer.tsx,基本功能包括滚动、autoCell、事件注入与解绑、placeHolder Container 的添加等基本功能。

class PageContainer extends Components{

 xxx

}

这时候我正使用这个容器,想接入微信分享功能。或者错误兜底功能。但是使用这个容器的人非常多。分享不一定都是微信分享、错误兜底不一定都是张着我想要的样子。所以我必定要对容器进行改造和增强。

从功能点划分,这些的确属于容器的能力。所以在无侵入式的增强方案中,装饰者模式是一个非常好的选择。也就是话落到我们所说的 Decorator。(对于 React 或者 RaxHOC 也是一种很好的方案,当然,其思想是一致的。)

+ @withError

+ @withWxShare

class PageContainer extends Components{

 xxx

}

我们添加 Decorator,这样的做法,对原有代码毫无入侵性,这就是AOP的好处了,把和主业务无关的事情,放到代码外面去做。

关于 Typescript

如何用 Decorator 装饰你的 Typescript?

javascript 毋庸置疑是一门非常好的语言,但是其也有很多的弊端,其中不乏是作者设计之处留下的一些 “bug”。当然,瑕不掩瑜~

话说回来,JavaScript 毕竟是一门弱类型语言,与强类型语言相比,其最大的编程陋习就是可能会造成我们类型思维的缺失(高级词汇,我从极客时间学到的)。而思维方式决定了编程习惯,编程习惯奠定了编程质量,工程质量划定了能力边界,而学习 Typescript,最重要的就是我们类型思维的重塑。

那么其实,Typescript 在我个人理解,并不能算是一个编程语言,它只是 JavaScript 的一层壳。当然,我们完全可以将它作为一门语言去学习。网上有很多推荐 or 不推荐 Typescript 之类的文章这里我们不做任何讨论,学与不学,用或不用,利与弊。各自拿捏~

再说说 typescript,其实对于 ts 相比大家已经不陌生了。更多关于 ts 入门文章和文档也是已经烂大街了。此文不去翻译或者搬运各种 api或者教程章节。只是总结罗列和解惑,笔者在学习 ts 过程中曾疑惑的地方。道不到的地方,欢迎大家评论区积极讨论。

首先推荐下各自 ts 的编译环境:typescriptlang.org

再推荐笔者收藏的两个网站:

  • Typescript 中文网

  • 深入理解 Typescript

  • TypeScript Handbook

  • TypeScript 精通指南

Typescript 中的 Decorator 签名

interface TypedPropertyDescriptor<T> {

    enumerable?: boolean;

    configurable?: boolean;

    writable?: boolean;

    value?: T;

    get?: () => T;

    set?: (value: T) => void;

}


declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;

declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;

如上是 ClassDecoratorPropertyDecorator以及 MethodDecorator 的三个类型签名。

基本配置

由于 DecoratorTypescript 中还是一项实验性的给予支持,所以在 ts 的配置配置文件中,我们指明编译器对 Decorator 的支持。

在命令行或tsconfig.json里启用experimentalDecorators编译器选项:

  • 命令行:

`tsc --target ES5 --experimentalDecorators`

  • tsconfig.json

{

    "compilerOptions": {

        "target": "ES5",

        "experimentalDecorators": true

    }

}

类型

Typescript 中,Decorator 可以修饰五种语句:类、属性、方法、访问器和方法参数。

class definitions

类装饰器应用于构造函数之上,会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

注意,在 Typescript 中的class 关键字只是 JavaScript 构造函数的一个语法糖。由于类装饰器的参数是一个构造函数,其也应该返回一个构造函数。

我们先看一下官网的例子:

function classDecorator<extends { new (...args: any[]): {} }>(

      constructor: T

    ) {

      return class extends constructor {

        newProperty = "new property";

        hello = "override";

      };

    }


    @classDecorator

    class Greeter {

      property = "property";

      hello: string;

      constructor(m: string) {

        this.hello = m;

      }

    }

    const greeter: Greeter = new Greeter("world");

    console.log({ greeter }, greeter.hello);

如何用 Decorator 装饰你的 Typescript?

{ new (...args: any[]): {} }表示一个构造函数,为了看起来清晰一些,我们也可以将其声明到外面:

/**

 *构造函数类型

 *

 * @export

 * @interface Constructable

 */

export interface IConstructable {

    new (...args:any[]):any

}

properties

属性装饰器有两个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  • 成员的key。

descriptor不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。

function setDefaultValue(target: Object, propertyName: string) {

      target[propertyName] = "Nealayng";

    }


    class Person {

      @setDefaultValue

      name: string;

    }


    console.log(new Person().name); // 输出: Nealayng

将上面的代码修改一下,我们给静态成员添加一个 Decorator

function setDefaultValue(target: Object, propertyName: string) {

      console.log(target === Person);


      target[propertyName] = "Nealayng";

    }


    class Person {

      @setDefaultValue

      static displayName = 'PersonClass'


      name: string;


      constructor(name:string){

        this.name = name;

      }

    }


    console.log(Person.prototype);

    console.log(new Person('全栈前端精选').name); // 输出: 全栈前端精选

    console.log(Person.displayName); // 输出: Nealayng

如何用 Decorator 装饰你的 Typescript?

以此可以验证,上面我们说的:Decorator 的第一个参数,对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

methods

方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  • 成员的名字。

  • 成员的属性描述符 descriptor

注意: 如果代码输出目标版本小于ES5,descriptor将会是undefined。

function log( target: Object,

      propertyName: string,

      descriptor: TypedPropertyDescriptor<(...args: any[]) => any>

    ) {

      const method = descriptor.value;

      descriptor.value = function(...args: any[]) {

        // 将参数转为字符串

        const params: string = args.map(=> JSON.stringify(a)).join();


        const result = method!.apply(this, args);


        // 将结果转为字符串

        const resultString: string = JSON.stringify(result);


        console.log(`Call:${propertyName}(${params}) => ${resultString}`);


        return result;

      };

    }


    class Author {

      constructor(private firstName: string, private lastName: string) {}


      @log

      say(message: string): string {

        return `${message} by: ${this.lastName}${this.firstName}`;

      }

    }


    const author:Author = new Author('Yang','Neal');

    author.say('《全站前端精选》');//Call:say("全站前端精选") => "全站前端精选 by: NealYang"

上述的代码比较简单,也就不做过多解释了。其中需要注意的是属性描述符 descriptor 的类型和许多文章写的类型有些不同:propertyDescriptor: PropertyDescriptor

如何用 Decorator 装饰你的 Typescript?

从官方的声明文件可以看出,descriptor 设置为TypedPropertyDescriptor加上泛型约束感觉更加的严谨一些。

如何用 Decorator 装饰你的 Typescript?

当然,官网也是直接声明为类型PropertyDescriptor的。这个,仁者见仁。

accessors

访问器,不过是类声明中属性的读取访问器和写入访问器。访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  • 成员的名字。

  • 成员的属性描述符。


如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。同时 TypeScript 不允许同时装饰一个成员的get和set访问器

function Enumerable( target: any,

      propertyKey: string,

      descriptor: PropertyDescriptor ) {

      //make the method enumerable

      descriptor.enumerable = true;

    }


    class Person {

      _name: string;


      constructor(name: string) {

        this._name = name;

      }


      @Enumerable

      get name() {

        return this._name;

      }

    }


    console.log("-- creating instance --");

    let person = new Person("Diana");

    console.log("-- looping --");

    for (let key in person) {

      console.log(key + " = " + person[key]);

    }

如何用 Decorator 装饰你的 Typescript?

如果上面 get 不添加Enumerable的话,那么 for in 只能出来_name  _name = Diana

parameters

参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。

  • 成员的名字。

  • 参数在函数参数列表中的索引。

参数装饰器只能用来监视一个方法的参数是否被传入。

在下面的示例中,我们将使用参数装饰器@notNull来注册目标参数以进行非空验证,但是由于仅在加载期间调用此装饰器(而不是在调用方法时),因此我们还需要方法装饰器@validate,它将拦截方法调用并执行所需的验证。

function notNull(target: any, propertyKey: string, parameterIndex: number) {

    console.log("param decorator notNull function invoked ");

    Validator.registerNotNull(target, propertyKey, parameterIndex);

}


function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {

    console.log("method decorator validate function invoked ");

    let originalMethod = descriptor.value;

    //wrapping the original method

    descriptor.value = function (...args: any[]) {//wrapper function

        if (!Validator.performValidation(target, propertyKey, args)) {

            console.log("validation failed, method call aborted: " + propertyKey);

            return;

        }

        let result = originalMethod.apply(this, args);

        return result;

    }

}


class Validator {

    private static notNullValidatorMap: Map<any, Map<string, number[]>> = new Map();


    //todo add more validator maps

    static registerNotNull(target: any, methodName: string, paramIndex: number): void {

        let paramMap: Map<string, number[]> = this.notNullValidatorMap.get(target);

        if (!paramMap) {

            paramMap = new Map();

            this.notNullValidatorMap.set(target, paramMap);

        }

        let paramIndexes: number[] = paramMap.get(methodName);

        if (!paramIndexes) {

            paramIndexes = [];

            paramMap.set(methodName, paramIndexes);

        }

        paramIndexes.push(paramIndex);

    }


    static performValidation(target: any, methodName: string, paramValues: any[]): boolean {

        let notNullMethodMap: Map<string, number[]> = this.notNullValidatorMap.get(target);

        if (!notNullMethodMap) {

            return true;

        }

        let paramIndexes: number[] = notNullMethodMap.get(methodName);

        if (!paramIndexes) {

            return true;

        }

        let hasErrors: boolean = false;

        for (const [index, paramValue] of paramValues.entries()) {

            if (paramIndexes.indexOf(index) != -1) {

                if (!paramValue) {

                    console.error("method param at index " + index + " cannot be null");

                    hasErrors = true;

                }

            }

        }

        return !hasErrors;

    }

}


class Task {

    @validate

    run(@notNull name: string): void {

        console.log("running task, name: " + name);

    }

}


console.log("-- creating instance --");

let task: Task = new Task();

console.log("-- calling Task#run(null) --");

task.run(null);

console.log("----------------");

console.log("-- calling Task#run('test') --");

task.run("test");

对应的输出位:

param decorator notNull function invoked

method decorator validate function invoked

-- creating instance --

-- calling Task#run(null) --

method param at index 0 cannot be null

validation failed, method call aborted: run

----------------

-- calling Task#run('test') --

running task, name: test

@validate装饰器把run方法包裹在一个函数里在调用原先的函数前验证函数参数.

装饰器工厂

装饰器工厂真的也就是一个噱头(造名词)而已,其实也是工厂的概念哈,毕竟官方也是这么号称的。在实际项目开发中,我们使用的也还是挺多的

装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。其实说白了,就是一个函数 return 一个 Decorator。非常像 JavaScript 函数柯里化,个人称之为“函数式Decorator”~

如何用 Decorator 装饰你的 Typescript?

import { logClass } from './class-decorator';

import { logMethod } from './method-decorator';

import { logProperty } from './property-decorator';

import { logParameter } from './parameter-decorator';


// 装饰器工厂,根据传入的参数调用相应的装饰器

export function log(...args) {

    switch (args.length) {

        case 3: // 可能是方法装饰器或参数装饰器

            // 如果第三个参数是数字,那么它是索引,所以这是参数装饰器

            if typeof args[2] === "number") {

                return logParameter.apply(this, args);

            }

            return logMethod.apply(this, args);

        case 2: // 属性装饰器

            return logProperty.apply(this, args);

        case 1: // 类装饰器

            return logClass.apply(this, args);

        default: // 参数数目不合法

            throw new Error('Not a valid decorator');

    }

}


@log

class Employee {

    @log

    private name: string;


    constructor(name: string) {

        this.name = name;

    }


    @log

    greet(@log message: string): string {

        return `${this.name} says: ${message}`;

    }

}

加载顺序

一个类中,不同位置声明的装饰器,按照以下规定的顺序应用:

  • 有多个参数装饰器(parameterDecorator)时,从最后一个参数依次向前执行

  • 方法(methodDecorator)和方法参数装饰器(parameterDecorator)中,参数装饰器先执行

  • 类装饰器(classDecorator)总是最后执行。

  • 方法(methodDecorator)和属性装饰器(propertyDecorator),谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。


function ClassDecorator() {

    return function (target) {

        console.log("I am class decorator");

    }

}

function MethodDecorator() {

    return function (target, methodName: string, descriptor: PropertyDescriptor) {

        console.log("I am method decorator");

    }

}

function Param1Decorator() {

    return function (target, methodName: string, paramIndex: number) {

        console.log("I am parameter1 decorator");

    }

}

function Param2Decorator() {

    return function (target, methodName: string, paramIndex: number) {

        console.log("I am parameter2 decorator");

    }

}

function PropertyDecorator() {

    return function (target, propertyName: string) {

        console.log("I am property decorator");

    }

}


@ClassDecorator()

class Hello {

    @PropertyDecorator()

    greeting: string;


    @MethodDecorator()

    greet( @Param1Decorator() p1: string, @Param2Decorator() p2: string) { }

}

输出为:

I am parameter2 decorator

I am parameter1 decorator

I am method decorator

I am property decorator

I am class decorator

实战

由于是业务代码,与技术无关琐碎,只截取部分代码示意,非 Decorator 代码,以截图形式

这应该也是整理这篇文章最开始的原因了。直接说说项目(rax1.0+Decorator)吧。

需求很简单,就是是编写一个页面的容器。

如何用 Decorator 装饰你的 Typescript?

部分项目结构:

pm-detail

├─ constants

│ └─ index.ts //常量

├─ index.css

├─ index.tsx // 入口文件

└─ modules // 模块

       └─ page-container // 容器组件

              ├─ base //容器基础组件

              ├─ decorator // 装饰器

              ├─ index.tsx

              ├─ lib // 工具

              └─ style.ts

重点看下如下几个文件

如何用 Decorator 装饰你的 Typescript?
  • base.tsx

如何用 Decorator 装饰你的 Typescript?

其实是基础功能的封装

在此基础上,我们需要个能滚动的容器

  • scrollbase.tsx

如何用 Decorator 装饰你的 Typescript?

也是基于 Base.tsx 基础上,封装一些滚动容器具有的功能

  • style decorator

import is from './util/is';

import map from './util/map';


const isObject = is(Object);

const isFunction = is(Function);


class Style {

  static factory = (...args) => new Style(...args);


  analyze(styles, props, state) {

    return map(=> {

      if (isFunction(v)) {

        const r = v.call(this.component, props, state);

        return isObject(r) ? this.analyze(r, props, state) : r;

      }

      if (isObject(v)) return this.analyze(v, props, state);

      return v;

    })(styles);

  }


  generateStyles(props, state) {

    const { styles: customStyles } = props;

    const mergedStyles = this.analyze(this.defaultStyles, props, state);

    if (customStyles) {

      Object.keys(customStyles).forEach(key => {

        if (mergedStyles[key]) {

          if (isObject(mergedStyles[key])) {

            Object.assign(mergedStyles[key], customStyles[key]);

          } else {

            mergedStyles[key] = customStyles[key];

          }

        } else {

          mergedStyles[key] = customStyles[key];

        }

      });

    }

    return {

      styles: mergedStyles,

    };

  }


  constructor(defaultStyles = {}, { vary = true } = {}) {

    const manager = this;


    this.defaultStyles = defaultStyles;


    return BaseComponent => {

      const componentWillMount = BaseComponent.prototype.componentWillMount;

      const componentWillUpdate = BaseComponent.prototype.componentWillUpdate;


      BaseComponent.prototype.componentWillMount = function() {

        manager.component = this;

        Object.assign(this, manager.generateStyles(this.props, this.state));

        return componentWillMount && componentWillMount.apply(this, arguments);

      };


      if (vary) {

        BaseComponent.prototype.componentWillUpdate = function(nextProps, nextState) {

          Object.assign(this, manager.generateStyles(nextProps, nextState));

          return componentWillUpdate && componentWillUpdate.apply(this, arguments);

        };

      }


      return BaseComponent;

    };

  }

}


export default Style.factory;

然后我们需要一个错误的兜底功能,但是这个本身应该不属于容器的功能。所以我们封装一个 errorDecorator

  • withError.txs

function withError<extends IConstructable>(Wrapped: T) {

  const willReceiveProps = Wrapped.prototype.componentWillReceiveProps;

  const didMount = Wrapped.prototype.componentDidMount;

  const willUnmount = Wrapped.prototype.componentWillUnmount;


  return class extends Wrapped {

    static displayName: string = `WithError${getDisplayName(Wrapped)}·`;


    static defaultProps: IProps = {

      isOffline: false,

      isError: false,

      errorRefresh: () => {

        window.location.reload(true);

      }

    };


    private state: StateType;

    private eventNamespace: string = "";


    constructor(...args: any[]) {

      super(...args);

      const { isOffline, isError, errorRefresh, tabPanelIndex } = this.props;

      this.state = {

        isOffline,

        isError,

        errorRefresh

      };

      if (tabPanelIndex > -1) {

        this.eventNamespace = `.${tabPanelIndex}`;

      }

    }


    triggerErrorHandler = e => {...};


    componentWillReceiveProps(...args) {

      if (willReceiveProps) {

        willReceiveProps.apply(this, args);

      }

      const [nextProps] = args;

      const { isOffline, isError, errorRefresh } = nextProps;

      this.setState({

        isOffline,

        isError,

        errorRefresh

      });

    }


    componentDidMount(...args) {

      if (didMount) {

        didMount.apply(this, args);

      }

      const { eventNamespace } = this;

      emitter.on(

        EVENTS.TRIGGER_ERROR + eventNamespace,

        this.triggerErrorHandler

      );

    }


    componentWillUnmount(...args) {

      if (willUnmount) {

        willUnmount.apply(this, args);

      }

      const { eventNamespace } = this;

      emitter.off(

        EVENTS.TRIGGER_ERROR + eventNamespace,

        this.triggerErrorHandler

      );

    }


    render() {

      const { isOffline, isError, errorRefresh } = this.state;


      if (isOffline || isError) {

        let errorType = "system";

        if (isOffline) {

          errorType = "offline";

        }

        return <Error errorType={errorType} refresh={errorRefresh} />;

      }


      return super.render();

    }

  };

}

然后我们进行整合导出

import { createElement, PureComponent, RaxNode } from 'rax';

import ScrollBase from "./base/scrollBase";

import withError from "./decorator/withError";


interface IScrollContainerProps {

  spmA:string;

  spmB:string;

  renderHeader?:()=>RaxNode;

  renderFooter?:()=>RaxNode;

  [key:string]:any;

}

@withError

class ScrollContainer extends PureComponent<IScrollContainerProps,{}> {


  render() {

    return <ScrollBase {...this.props} />;

  }

}


export default ScrollContainer;

使用如下:

如何用 Decorator 装饰你的 Typescript?
如何用 Decorator 装饰你的 Typescript?

思维导图

最后附一张,本文思维导图。

参考文献

  • TypeScript装饰器(decorators)

  • 文档

  • TypeScript - Class Decorators


关于奇舞周刊


以上是关于如何用 Decorator 装饰你的 Typescript?的主要内容,如果未能解决你的问题,请参考以下文章

Python 装饰器(Decorator)

关于Python的装饰器 decorator

Java设计模式之——装饰者模式(Decorator pattern)

简单理解 ES7 Decorator

react项目的中使用mobx状态管理安装ES7装饰器(Decorator)语法教程

装饰模式Decorator