How to decorate your Typescript use Decorator?

About the Author:
isNealyang teacher, years of work experience, Taobao senior front-end engineer, front-end, good JavaScript / react.
No public: Full featured front-end stack
Nuggets column: https: //juejin.im/user/59be059c5188256c6d77cf2e/posts
major share: front-end, client, Node, interview, career insights and other related articles of high quality.

 

THE LAST TIME handwriting is the series of  Typescript articles, which Decorator has always been my personal opinion a very good cut program. The so-called cut programs that we often say that the programming section  AOP. A programming thought, straightforward explanation is A run-time, dynamic code class cut into the specified method, AOP programming idea is the specified location . AOP And we are familiar with  OOP , like, just a programming paradigm , AOP did not say what code to use any prescribed protocol, you must use what way to achieve this is just a paradigm. And  Decorator that is AOP a form.

The focus of this paper is not to discuss programming paradigms, introduces  Typescript+ Decorator some knowledge to explain the figure below, including some recent applications I write in the project.

This article Zhou Wenxi teacher has received authorization to forward, reprint other people who are interested, please contact the author's permission.

About the Author:
isNealyang teacher, years of work experience, Taobao senior front-end engineer, front-end, good JavaScript / react.
No public: Full featured front-end stack
Nuggets column: https: //juejin.im/user/59be059c5188256c6d77cf2e/posts
major share: front-end, client, Node, interview, career insights and other related articles of high quality.

 

Introduction

What is the Decorator

Looks like last year, when the public number: [selection] full stack front end, there had to share about  Decorator the basics of: Decorator from principle to actual combat , there are of Decorator very detailed introduction.

Essentially, it is syntactic sugar for function.

Decorator Is  ES7 adding a new feature, of course,  Typescript we have long had very. Long before this, there is proposed with the  Decorator idea of very similar design pattern: the decorator pattern .

The figure WeaponAccessoryis a Decorator, they add extra functionality to the base class. So that it could meet your needs.

Simple to understand  Decorator, you may think it is a packaging, packaging of objects, methods, properties. Like Decorator Man, a suit of armor, but decorated to meet demand has not changed is the nature of human beings.

Why use Decorator

Why should we use  Decorator, in fact, introduced to the  AOP most important feature of the paradigm: non-invasive enhancement.

For example, I'm writing a page container, called  PageContainer.tsxbasic functions including scrolling, autoCellevent injected with unbundling, placeHolder Container adding other basic functions.

class PageContainer extends Components{
 xxx
}

This time I am using this container, want access to micro-channel sharing. Or error function reveal all the details. But using this container so many people. Share micro-channel are not necessarily shared, not always reveal all the details wrong Zhang I want to look. So I certainly want to transform and enhance container .

From a functional point of division, these are indeed the capacity of the container belongs. Therefore, non-invasive enhancing the embodiment, the decorator pattern is a very good choice. That is what we call, then fell  Decorator. (For  React Or  Rax, HOC it is a good solution, of course, the idea is the same.)

+ @withError
+ @withWxShare
class PageContainer extends Components{
 xxx
}

We add  Decorator, such an approach, the original code is no invasive, this is the AOPbenefit of, the main business and nothing to do, put out the code to do it .

关于 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 的三个类型签名。

基本配置

由于 Decorator 在 Typescript 中还是一项实验性的给予支持,所以在 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<T 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);

{ 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 的第一个参数,对于静态成员来说是类的构造函数,对于实例成员是类的原型对象

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(a => 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

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

当然,官网也是直接声明为类型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]);
    }

如果上面 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”~

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)吧。

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

部分项目结构:

pm-detail
├─ constants
│    └─ index.ts  //常量
├─ index.css
├─ index.tsx  // 入口文件
└─ modules  // 模块
       └─ page-container  // 容器组件
              ├─ base   //容器基础组件
              ├─ decorator  // 装饰器
              ├─ index.tsx
              ├─ lib  // 工具
              └─ style.ts

重点看下如下几个文件

  • base.tsx

其实是基础功能的封装

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

  • scrollbase.tsx

也是基于 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(v => {
      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<T 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;

使用如下:

思维导图

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

 

本文已经获得周文熙老师授权转发,其他人若有兴趣转载,请直接联系作者授权。

作者简介:
isNealyang老师,多年工作经验,淘宝高级前端工程师 ,前端,擅长JavaScript/react。
公众号:全栈前端精选
掘金专栏:https://juejin.im/user/59be059c5188256c6d77cf2e/posts
主要分享:前端、客户端、Node、面试、职场感悟等相关高质量文章。

作者:isNealyang 
链接:https://mp.weixin.qq.com/s/PFgc8xD7gT40-9qXNTpk7A
来源:公众号:全栈前端精选

发布了750 篇原创文章 · 获赞 1052 · 访问量 58万+

Guess you like

Origin blog.csdn.net/jnshu_it/article/details/104265639