JSデコレーション(Decorator)シーン実戦

15304 ワード

本文は装飾器(Decorator)の概念と基礎的な使い方を大きく紹介しないで、核心は私たちのチームがどのように装飾器を実際の開発に応用するか、いくつかの高級な使い方の実現を紹介します.
アクセサリーの概要
DecoratorはES 7の新しい文法で、その「装飾器」の呼び方が表すように、いくつかのオブジェクトを装飾包装して包装されたオブジェクトを返すことができ、装飾可能なオブジェクトには、クラス、属性、方法などが含まれています.Decoratorの書き方はJavaの注釈(Annotation)と非常に似ていますが、JSの中の装飾器を「注釈」と呼ばないでください.この2つの原理と実現の機能には違いがあります.Javaでは、注釈は主にあるオブジェクトに注釈を付け、実行時やコンパイル時に、表示されたオブジェクトは、例えば、反射のようなメカニズムによって取得され、いくつかの論理的パッケージを行うことができる.Decoratorの原理と役割はもっと簡単で、オブジェクトを包装し、新しいオブジェクト記述(descriptor)を返します.その役割も非常に簡単で、基本的には包装オブジェクトの宿主、キー値のいくつかの限られた情報を取得します.
Decoratorの詳細については、記事:zhuanlan.を参照してください.zhihu.com/FrontendMag…
簡単に言えば、JSのデコレーションは「デコレーション」の3種類のオブジェクト:クラスのプロパティ/メソッド、アクセサ、クラス自体に使用できますが、いくつかの例を簡単に見てみましょう.
アトリビュート/メソッド用のモディファイヤ
// decorator           ,       
function Decorator(type){
    /**
     *        decorator
     * @target             ,  ,       。       Car      ,   target      Car.prototype
     * @name        key
     * @descriptor           
     */
    return function (target, name, descriptor){
        //                    
        let v = descriptor.initializer && descriptor.initializer.call(this);
        //           ,       descriptor    
        return {
            enumerable: true,
            configurable: true,
            get: function() {
                return v;
            },
            set: function(c) {
                v = c;
            }
        }
    }
}

ここでtargetは、装飾された属性が属するクラスの原型に対応していることに注意してください.Aクラスの属性を装飾し、AクラスがBクラスから継承されている場合は、targetを印刷して、A.prototypeを取得します.その構造はこうです.ここで注意してください.
[image:A 944761 A-E 0 FA-4 C 04-BD 90-BE 179 C 46 B 641-35651-00012238250 C 5/187 FCC 2 A-8 CC 4-46 C 4-B 8 A 3-A 7 FD 5 E 0376 F 6.png]targetを操作する必要がある場合は、この問題を明らかにする必要があるかもしれません.
アクセスオペレータ用の装飾
属性メソッドと同様に、詳しくは説明しません.
class Person {
  @nonenumerable
  get kidCount() { return this.children.length; }
}

function nonenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

クラス向けの装飾
//    mobx   @observer    
/**
 *    react   
 * @param target
 */
function observer(target) {
    target.prototype.componentWillMount = function() {
        targetCWM && targetCWM.call(this);
        ReactMixin.componentWillMount.call(this);
    };
}

その中のtargetはクラス自体です(prototypeではありません)
リアルシーンの適用
ここでは、データ定義レイヤにDecoratorというプロパティを適用し、タイプチェック、フィールドマッピングなどの機能を実装する方法について説明します.
データ定義層(Model)については、アプリケーション内に現れる様々なエンティティデータの定義、すなわちMVVMのM層について、注意して、VM層と区別して、Model自体はデータの管理と流通を提供しないで、あるエンティティ自体の属性と方法を定義するだけで、例えばページに車のモジュールがあるので、CarModelを定義します.車両の色、価格、ブランドなどの情報を記述するために使用されます.
なぜフロントエンドアプリケーション内で明確なModelを定義するのかについては、これまでも知っていた上で、核心的ないくつかの点があります.
  • 保守性の向上データソースのエンティティを固定して正確に説明することは、アプリケーション全体を直列に理解する上で非常に重要です.特に、他の人のコードを再構築したり引き継いだりするときは、ページ(またはモジュール)にどのデータが含まれているのか、これらのデータにはそれぞれどのフィールドがあるのかを正確に知る必要があります.これにより、アプリケーション全体のデータロジックを理解しやすくなります.
  • 確定性を向上させる.インタフェースにいくつかの車両フィールドを追加する場合は、これらのフィールドが定義されているかどうか、サービス側がこれらのフィールドを返すかどうかは不明ですが、modelの明確な定義があれば、どのフィールドがあるかは一目瞭然です.
  • は開発効率を向上させる.このレベルでは、データマッピングやタイプチェックなどを統一して行うことも、今日お話しするポイントです.

  • 我々のチームRN開発フレームワークにおけるModel部分の実装を例にとると、少なくとも3つの基礎的なDecoratorベースの機能を提供しています.タイプチェック、単位変換、フィールドマッピングです.次に、このいくつかの機能が何をしているのかを簡単に紹介し、これらのDecoratorを実現する方法を紹介します.
    まず最終呼び出し時のコードを見てみましょう
    class CarModel extends BaseModel {
        /**
         *   
         * @type {number}
         */
        @observable
        @Check(CheckType.Number)
        @Unit(UnitType.PRICE_UNIT_WY)
        price = 0;
    
        /**
         *    
         * @type {string}
         */
        @observable
        @Check(CheckType.String)
        @ServerName('seller_name')
        sellerName = '';
    }

    3つのカスタムdecoratorが表示されます.
    @Unit,         //        
    @Check,        //        ,
    @ServerName    //          ,                 

    @Unitは比較的特殊な装飾器で、前後端の間で単位を自動的に変換する役割を果たしています.つまり、先端と後端でいくつかの帯単位のデータを交換するとき、各端の注釈と装飾器に基づいて、真実の値を帯単位の値に変換して他端に伝え、他端はフレーム層で自動的に定義された単位に変換されます.これにより,前後の単位の不一致,データ交換時の混乱による問題を解決する.
    @Unitに装飾された属性は、読み書きの際に先端の単位で読み書きされ、JSONに変換されたときに12.3_$のように特殊に処理されます.wyというフォーマットで、この数を表す単位は万元です.@Checkは、フィールドタイプをチェックしたり、フィールドフォーマットをチェックしたり、正規表現などのカスタムチェックをしたりするために使用されることをより理解しやすい.@Server Nameは、同じインタフェース要素に対する前後の名前が異なるなど、マッピングに使用されます.この場合、サービス側の名前に完全に従う必要はありません.フロントエンドで別のプロパティ名を使用して、サービス側のフィールド名に装飾することができます.
    インフラストラクチャの実装
    私たちの目標はこれらのDecoratorを実現することです.以前のDecoratorの科学普及に従って、実際にはこれらの機能を独立して実現するのは簡単です.@Checkを例にとると、パッケージングされたプロパティのdescriptorを書き換え、新しいdescriptorを返し、パッケージングされたプロパティのgetterとsetterを再定義し、setterを呼び出すときにまず入力パラメータのタイプとフォーマットをチェックし、対応する処理を行います.
    /**
     *                    ,         
     * @param type CheckType       
     * @returns {Function}
     * @constructor
     */
    function CheckerDecorator(type){
        return function (target, name, descriptor){
            let v = descriptor.initializer && descriptor.initializer.call(this);
            return {
                enumerable: true,
                configurable: true,
                get: function() {
                    return v;
                },
                set: function(c) {
                    //        c        
                    var cType = typeof(c);
                    // ...
                    v = c;
                }
            }
        }
    }

    非常に簡単で、他のいくつかのDecoratorの実装も似ています.@Unitのような実装はやや複雑かもしれませんが、Decoratorで各属性の寸法の単位を覚えておき、シーケンス化の際に対応する属性の対応する単位を取得して変換すればいいのです.
    インフラストラクチャの問題
    しかし、ここまで来て、問題は実はまだ終わっていません!私たちは確かに利用可能なDecoratorを実現しましたが、これらのDecoratorは重ねて使用できますか?また、業界でよく使われているDecoratorと混用してもいいですか?たとえばmobxの@observableです.つまり、私の最初の例の使い方です.
    @observable
    @Check(CheckType.String)
    @ServerName('seller_name')
    sellerName = '';

    もしあなたが私のさっきの方法で@Checkと@Server Nameを実現したら、あなたは2つの致命的な問題を発見します.
  • という2つの自分で実現したDecoratorはまず重ねて使用できません.
  • 両方のDecoratorは@observableと同時に使用できません.どうしてですか.問題は,属性を書き換えるgetterとsetterの実現原理にある.まず、属性定義getterとsetterを与えるたびに、前回の定義が上書きされます.つまり、この動作は1回しかありません.そして、mobxの実装はgetterとsetterの定義に非常に依存する(私の前の文章を参照してください:どのように自分でmobx-原理解析を実現するか)
  • 実際、Decorator自体が重ねて使用する場合は問題ありません.あなたのパッケージのたびに、属性のdescriptorを上のレイヤのパッケージに返します.最後に関数パッケージ関数の効果で、最終的にはこの属性のdescriptorを返します.
    ステップインプリメンテーション
    では、getterとsetterを定義する実現方法を捨てる必要があります.実はこのような方式以外にも、上述の機能を実現する方法がたくさんあります.核心は一つです.装飾器関数では、あなたが処理しなければならない属性とこの属性に必要な処理の対応関係を記録し、インスタンス化データとシーケンス化データを処理するときに、対応関係を取り出し、関連論理を実行すればいいのです.
    くだらないことを言わないで、私たちは直接この対応関係をクラスの原型に載せる一つの実現方法に行きます.
    function Check (type) {
        return function (target, name, descriptor) {
            let v = descriptor.initializer && descriptor.initializer.call(this);
            /**
             *                          
             */
            if (!target.constructor.__checkers__) {
                //            not enumerable,          。
                Object.defineProperty(target.constructor, "__checkers__", {
                    value: {},
                    enumerable: false,
                    writeable: true,
                    configurable: true
                });
            }
            target.constructor.__checkers__[name] = {
                type: type
            };
            return descriptor
        }
    }

    前に述べた情報では、装飾関数の最初のパラメータtargetはパッケージ属性が属するクラスのプロトタイプであり、babelコンパイル後の結果を見ることができます.そして私のところではなぜ対応関係をtargetにマウントしたのか.constructorでは、私のすべてのModelクラスは、自分が提供したModelベースクラスを継承しているため、targetが手に入れたのはサブクラスの原型ではなく、ベースクラスの原型、targetです.constructorが手に入れたのが最終的なサブクラスです.つまり,開発定義のサブクラスに対応関係をマウントした.
    次に、ベースクラスのコードを見てみましょう.コアは、データのマッピングとシーケンス化の2つの方法を提供します.
    class BaseModel {
        /**
        *                 
        */
        __map (json) {
            let alias = this.constructor.__aliasNames__;
            let units = this.constructor.__unitOriginals__;
            let checkers = this.constructor.__checkers__;
            for (let i in this) {
                if (!this.hasOwnProperty(i)) return;
                //         ,                    realValue
                let realValue = json[i];
                //            
                //         ,    
                if (alias && typeof(alias[i]) !== 'undefined') {
                    // ......
                }
                //           
                if (checkers && checkers[i]) {
                    // ......
                }
                //   ,        
                if (units && units[i]) {
                    // ......
                }
                //   
                this[i] = realValue;
            }
        }
        /**
        *    JSON.stringify         
        */
        toJSON () {
            let result = {};
            let units = this.constructor.__unitOriginals__;
            for (let i in this) {
                if (!this.hasOwnProperty(i)) return;
                if (units && units[i]) {
                    //     ,           
                    result[i] = this[i] + '_$' + units[i];
                } else {
                    result[i] = this[i];
                }
            }
            return result;
        }
    }

    で_map関数では,現在のクラス(this.constructor)上の対応関係をすべて取り出し,データの検証とマッピングを行うが,ここでは理解にかたくないはずである.
    最終的に適用されるコードは、対応するModelクラスがBaseModelから継承されている限り、最終的に使用されるコードを最初に貼り付けます.
    このように実現されたDecoratorは、getter setter関連の機能を一切使用していないためmobxのようなライブラリと完璧に融合し、無限に重ねて使用することができますが、複数の3つのライブラリを使用すると、対応するDecoratorを提供し、getterとsetterを修正してしまうので、仕方がありません!
    まとめ
    Decoratorは原理は非常に簡単ですが、確かに多くの実用的で便利な機能を実現することができ、目測フロントエンド分野では多くのフレームワークやライブラリがこの特性を大規模に使用していますが、これらのライブラリはDecoratorを実現する際に汎用性を考慮し、重ね合わせと共存の問題を考慮することを望んでいます.上のmobxの@observableのように、関係なく重ねることができず、しかも私自身が実現したDecoratorとの順序が乱れず、最外層にいなければならない.属性全体の性質を変えたので、最外層に書かないと、わけのわからない問題が発見される.