あなたの活字体の使用デコレータを飾るためにどのように?

著者について:
isNealyang教師、仕事の経験、淘宝網のシニアフロントエンドエンジニア、フロントエンドの年、良いJavaScriptが/反応します。
いいえ公共ん:フルフロントエンドスタック機能を備え
ます。https:ナゲッツ列を//juejin.im/user/59be059c5188256c6d77cf2e/postsは
、フロントエンド、クライアント、ノード、面接、キャリアの洞察力と高品質のその他の関連記事:シェア主要な。

 

LAST TIME手書きは一連のある  Typescript 記事、Decorator いつも私の個人的な意見は非常に良いカットプログラムとなっています。私たちは、多くの場合、プログラミング区間と言うことは、いわゆるカットプログラム  AOPプログラミングは簡単な説明であり、考え実行時、指定されたメソッドへの動的なコード・クラス・カットは、AOPプログラミングアイデアは、指定された場所ですAOP そして、我々は精通している  OOP 、のような、単にプログラミングパラダイムAOP 任意の所定のプロトコルを使用するためにどのようなコード言わなかった、あなたはこれを達成するためにどのような方法を使用しなければならないことは、単にパラダイムです。そして、  Decorator それがあるAOP フォーム。

この論文の焦点は、パラダイム、紹介プログラミングを議論することではありません  Typescript+ Decorator 私はプロジェクトに書き込み、いくつかの最近のアプリケーションを含む、以下の図を説明するためにいくつかの知識を。

この記事周Wenxiの先生が転送する許可を受けている、興味を持っている再版他の人には、著者の許可を連絡してください。

著者について:
isNealyang教師、仕事の経験、淘宝網のシニアフロントエンドエンジニア、フロントエンドの年、良いJavaScriptが/反応します。
いいえ公共ん:フルフロントエンドスタック機能を備え
ます。https:ナゲッツ列を//juejin.im/user/59be059c5188256c6d77cf2e/postsは
、フロントエンド、クライアント、ノード、面接、キャリアの洞察力と高品質のその他の関連記事:シェア主要な。

 

入門

デコレータは何ですか

昨年のように見える、公共数:[選択]フルスタックのフロントエンド、およそ共有に存在していた  Decorator の基礎:原則から実際の戦闘にデコレータのあるDecorator 非常に詳細な紹介。

基本的に、それは関数のシンタックスシュガーです。

Decorator された  ES7 新機能を追加し、もちろん、  Typescript 私たちは長い間非常に持っていました。ロングこの前に、とが提案されている  Decorator :非常によく似たデザインパターンの考え方Decoratorパターン

この図はWeaponAccessoryあるDecorator、彼らは基本クラスに機能を追加します。それはあなたのニーズを満たすことができるように。

理解することは、単純な  Decorator、あなたはそれが包装、オブジェクトのパッケージ、メソッド、プロパティであると思うかもしれません。Decoratorのような男、鎧のスーツが、満たす需要に装飾されたが変更されていない人間の性質です。

なぜ使用のデコレータ

なぜ私たちが使用する必要があり  Decorator、実際には、に導入さ  AOP :パラダイムの最も重要な機能の非侵襲的強化。

例えば、私はと呼ばれるページコンテナ、書いて  PageContainer.tsx、スクロールなどの基本的な機能をautoCellイベントは、アンバンドリングを注入しplaceHolder Container 、他の基本的な機能を追加します。

class PageContainer extends Components{
 xxx
}

私はこのコンテナを使用しています。この時間は、マイクロチャネルの共有にアクセスしたいです。または誤差関数は、すべての詳細を明らかにする。しかし、多くの人々ように、このコンテナを使用しました。シェアマイクロチャネルは、必ずしも間違った張私が見てみたいすべての詳細を明らかにしない、共有されません。だから私は確かに変換し、コンテナを強化したいです

分割の機能の点から、これらは、実際に容器の容量が属しています。したがって、非侵襲的な実施形態を高め、Decoratorパターンは非常に良い選択です。私たちが呼んでいるものであること、そして落ちました  Decorator(のため  React か  RaxHOC それは良い解決策である、もちろん、考え方は同じです。)

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

私たちは、追加  Decorator元のコードは、侵襲的ではありません、このようなアプローチを、これはAOP、利益のそれを行うためのコードを出して、主要なビジネスとは何の関係も

关于 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万+

おすすめ

転載: blog.csdn.net/jnshu_it/article/details/104265639