Боевая сцена JS Decorator (Декоратор)

JavaScript Babel ECMAScript 6
Боевая сцена JS Decorator (Декоратор)

В этой статье мы не будем подробно знакомить с концепцией и базовым использованием декоратора (Decorator), а познакомим вас с тем, как наша команда применяет декоратор к фактической разработке и реализации некоторых расширенных возможностей.


Введение в декораторы

Декоратор — это новый синтаксис ES7. Как следует из его имени «декоратор», он может украшать и обертывать некоторые объекты и возвращать обернутый объект. Объекты, которые можно декорировать, включают в себя: классы, свойства, методы и т. д. . То, как написан декоратор, очень похоже на аннотацию (Annotation) в Java, но вы не должны называть декоратор в JS «аннотацией», потому что принципы и функции у них все же разные. пометить объект, а затем во время выполнения или во время компиляции вы можете получить помеченный объект с помощью такого механизма, как отражение, и выполнить над ним некоторую логическую упаковку. Принцип и функция Декоратора проще, то есть обернуть объект, а потом вернуть новое описание объекта (дескриптор).

Подробное введение в Decorator см. в статье:Колонка Call.com/frontend маг…

Проще говоря, декораторы JS можно использовать для «декорирования» трех типов объектов: свойств/методов класса, методов доступа и самого класса, просто взгляните на несколько примеров.

Декораторы для свойств/методов

// 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;
            }
        }
    }
}

Обратите внимание, что цель здесь соответствует прототипу класса, к которому принадлежит оформленное свойство.Если нужно декорировать свойство класса А, а класс А наследуется от класса В, то вы печатаете цель, и вы получаете А. прототип, который Структура такая, мы должны обратить внимание здесь:

[image:A944761A-E0FA-4C04-BD90-BE179C46B641-35651-00001223828250C5/187FCC2A-8CC4-46C4-B8A3-A7FD5E0376F6.png]
Если вам нужно манипулировать целями, вам может понадобиться это выяснить.

украшение для оператора доступа

Подобно методу свойств, он не будет подробно описываться.

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);
    };
}

где цель — это сам класс (а не его прототип)


реальное приложение

Сегодня мы хотим представить, как применить функцию декоратора к уровню определения данных для реализации некоторых функций, таких как проверка типов и сопоставление полей.

Что касается уровня определения данных (модели), то на самом деле это определение различных данных объектов, которые появляются в приложении, то есть уровень M в MVVM.Обратите внимание, что он отличается от уровня VM.Сама модель не предоставляет данные управления и обращения, и отвечает только за определение атрибутов и методов самой сущности.Например, на странице есть модуль автомобиля.Мы определяем CarModel, который используется для описания цвета, цены, марки и другой информации транспортного средства.

Что касается того, почему четко определенная модель должна быть определена во внешнем приложении, я уже обсуждал это на Zhihu ранее.Основные моменты таковы:

  • Улучшить ремонтопригодность. Сделать фиксированное и точное описание сущности источника данных, что очень важно для последовательного понимания всего приложения, особенно при рефакторинге или заимствовании чужого кода, нужно точно знать, что страница (или модуль) будет Какие данные включены, и какие поля есть в этих данных, так легче понять логику данных всего приложения.
  • Улучшить уверенность. Если вы хотите добавить несколько полей транспортного средства в свой интерфейс, вы не знаете, были ли эти поля определены ранее, и будет ли сервер возвращать эти поля, вам, возможно, придется запросить его (и иметь разрешение на получение всех полей), чтобы знаю, но если есть четкое определение модели, то с первого взгляда будет понятно, какие там поля.
  • Повысить эффективность разработки. Равномерно выполните сопоставление данных и проверку типов в этом слое, что также является предметом сегодняшнего разговора.

Взяв в качестве примера реализацию части модели в структуре разработки RN нашей команды, мы предоставляем как минимум три основные функции на основе декоратора: проверку типов, преобразование единиц измерения и сопоставление полей. Далее я кратко расскажу, что делают эти функции, а затем расскажу, как реализовать эти декораторы.

Давайте посмотрим на код, когда будет сделан последний вызов

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 = '';
}

Вы можете видеть, что у нас есть три пользовательских декоратора:

@Unit,         // 单位转换装饰器
@Check,        // 类型检查装饰器,
@ServerName    // 数据字段映射装饰器,当前后端定义的字段名不一致的时候用

@Unit — это специальный декоратор, его функция заключается в автоматическом преобразовании единиц измерения между фронтендом и бэкендом, то есть когда фронтенд и бэкенд обмениваются некоторыми данными с единицами, они будут основаны на аннотациях и декораторах каждый конец.Реальное значение преобразуется в значение с единицей и передается на другой конец, а затем другой конец будет автоматически преобразован в его определенную единицу на уровне структуры, чтобы решить проблему несогласованности передней и задней части конечные единицы и путаница при обмене данными.

Атрибуты, оформленные @Unit, считываются и записываются в соответствии с интерфейсной единицей, а затем при преобразовании в JSON они будут специально обработаны в формат типа 12.3_$wy, что указывает на то, что единицей этого числа является десять тысяч юаней. .
@Check легче понять, он используется для проверки типа поля, или проверки формата поля, или некоторых пользовательских проверок, таких как регулярные выражения и т. д.
@ServerName используется для сопоставления. Например, имена front-end и back-end для одного и того же элемента интерфейса различаются. На данный момент нет необходимости решать на основе имени сервера. Можно использовать другой атрибут name во фронтенде, а потом оформить его как поле name сервера. .

Базовая реализация

Наша цель — реализовать эти декораторы.Согласно предыдущей популяризации декораторов, реализовать эти функции самостоятельно на самом деле очень просто.
Взяв за пример @Check, мы переписываем дескриптор обернутого свойства, возвращаем новый дескриптор, переопределяем геттер и сеттер обернутого свойства, а затем проверяем тип и формат входящих параметров при вызове сеттера и делаем некоторые соответствующую обработку.

/**
 * 此注解如果赋值的时候匹配到的类型有问题,会在控制台显示警告
 * @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;
            }
        }
    }
}

Очень просто, реализация нескольких других декораторов аналогична. Это может быть немного сложно реализовать, как @Unit, но если вы помните единицу измерения каждой метки атрибута в декораторе, вы можете получить соответствующий атрибут во время сериализации. затем преобразоваться.

Основные проблемы реализации

Однако и здесь проблема еще не исчерпана!
Мы реализуем декоратор, который работает, но могут ли эти декораторы складываться? Кроме того, можно ли смешивать его с некоторыми декораторами, обычно используемыми в отрасли? Например @observable в mobx. Это использование моего первого примера выше:

@observable
@Check(CheckType.String)
@ServerName('seller_name')
sellerName = '';

Если вы реализуете @Check и @ServerName так, как это сделал я, вы обнаружите две фатальные проблемы:

  • Эти два самореализующихся декоратора нельзя использовать вместе.
  • Ни один декоратор нельзя использовать с @observable.
    Зачем? Проблема заключается в принципе реализации Getter и Setter, которую мы переписываем свойство. Прежде всего, каждый раз, когда вы определяете Getter и Setter для свойства, он перезаписывает предыдущее определение, то есть это действие может быть сделано только один раз. Затем реализация MOBX очень зависит от определения Getter и Benters (см. Мое предыдущая статья:Как реализовать мобкс самостоятельно — принципиальный разбор)

На самом деле, сам Декоратор не представляет проблемы при наложении, потому что каждый раз, когда вы выполняете перенос, он будет возвращать дескриптор атрибута в оболочку предыдущего слоя, и, наконец, это эффект функции пакета функций пакета функций, и окончательный результат по-прежнему остается этим свойством.descriptor.

Расширенная реализация

Затем нам нужно избавиться от реализации определения геттеров и сеттеров. На самом деле, помимо этого метода, есть много способов достижения вышеперечисленных функций.Ядро - это одна точка.В функции декоратора запишите соответствующую связь между атрибутами, которые вам нужно обработать, и обработкой, которую вы должны сделать для этого. атрибут, а затем обработайте соответствующую связь.При создании экземпляра данных и сериализации данных извлеките соответствующую связь и выполните соответствующую логику.

Без лишних слов перейдем непосредственно к методу реализации, который монтирует это соответствие прототипу класса.

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
    }
}

Обратите внимание, что я упомянул информацию ранее, первый целевой параметр функции украшения — это прототип класса, которому принадлежит свойство-обертка, что можно увидеть, посмотрев на результат после компиляции babel. Тогда зачем я монтирую здесь соответствующее отношение к target.constructor, ведь все мои классы Model наследуются от предоставленного мной базового класса Model (BaseModel), а target получает не прототип подкласса, а прототип подкласса. Прототип базового класса, который получает target.constructor, является окончательным подклассом. То есть я смонтировал соответствующее отношение к подклассу, определенному разработкой.

Далее посмотрите на код базового класса.Ядро предоставляет два метода, которые являются методами отображения данных и сериализации.

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.

Декоратор, реализованный таким образом, поскольку он не использует никаких функций, связанных с геттером-сеттером, может быть идеально интегрирован с такими библиотеками, как mobx, и может использоваться в бесконечных стеках, но если вы используете несколько сторонних библиотек, все они предоставляют соответствующий Decorator, а потом модифицировать геттер и сеттер, то никак!


Суммировать

Хотя принцип Decorator очень прост, он действительно может реализовать множество практичных и удобных функций.По оценкам, многие фреймворки и библиотеки в области внешнего интерфейса будут использовать эту функцию в больших масштабах, но я также надеюсь, что эти библиотеки будут учитывайте универсальность при реализации проблемы сосуществования Decorator. Как и @observable у mobx выше, он не может быть наложен, и порядок реализованного мной Декоратора не может быть хаотичным.Он должен быть в самом внешнем слое, потому что он меняет природу всего атрибута.Когда он не прописан в самый внешний слой, он обнаружит некоторые необъяснимые проблемы.