Pull to refresh
0

Темная сторона TypeScript — @декораторы на примерах

Reading time 18 min
Views 60K

Декораторы — это невероятно круто. Они позволяют описывать мета информацию прямо в объявлении класса, группируя все в одном месте и избегая дублирования. Ужасно удобно. Однажды попробовав, вы уже никогда не согласитесь писать по-старому.


Однако, несмотря на всю полезность, декораторы в TypeScript (заявлены также на стандарт) не так просты, как хотелось бы. Работа с ними требует навыков джедая, так как необходимо разбираться в объектной модели JavaScript (ну, вы поняли, о чем я), API несколько запутанный и, к тому же, еще не стабильный. В этой статье я немного расскажу об устройстве декораторов и покажу несколько конкретных приемов, как поставить эту темную силу на благо front-end разработки.


Помимо TypeScript, декораторы доступны в Babel. В этой статье рассматривается только реализация в TypeScript.




  Основы


Декорировать в TypeScript можно классы, методы, параметры метода, методы доступа свойства (accessors) и поля.


Почему я использую термин 'поле', а не 'свойство' как в официальной документации

В TypeScript термин "поле" обычно не используется, и поля называют также свойствами (property). Это создает большую путаницу, т.к. разница есть. Если мы объявляем свойство с методами доступа get/set, то в объявлении класса появляется вызов Object.defineProperty и в декораторе доступен дескриптор, а если объявляем просто поле (в терминах C# и Java) — то не появляется ничего, и, соответственно, дескриптор не передается в декоратор. Это определяет сигнатуру декораторов, поэтому я использую термин "поле", чтобы отличать их от свойств с методами доступа.


В общем случае, декоратор — это выражение, предваренное символом "@", которое возвращает функцию определенного вида (разного в каждом случае). Собственно, можно просто объявить такую функцию и использовать ее имя в качестве выражения декоратора:


function MyDecorator(target, propertyKey, descriptor) {
    // ...
}
class MyClass {
    @MyDecorator
    myMethod() {
    }
}

Однако можно использовать любое другое выражение, которое вернет такую функцию. Например, можно объявить другую функцию, которая будет принимать параметрами дополнительную информацию, и возвращать соответствующую лямбду. Тогда в качестве декоратора будем использовать выражение "вызов функции MyAdvancedDecorator".


function MyAdvancedDecorator(info?: string) {
   return (target, propertyKey, descriptor) => {
        // ..
   };
}
class MyClass {
    @MyAdvancedDecorator("advanced info")
    myMethod() {
    }
}

Здесь самый обычный вызов функции, поэтому, даже если мы не передаем параметры, все равно нужно писать скобки "@MyAdvancedDecorator()". Собственно, это два основных способа объявления декораторов.


В процессе компиляции объявление декоратора приводит к появлению вызова нашей функции в определении класса. То есть там, где вызываются Object.defineProperty, заполняется прототип класса и все такое. Как именно это происходит — важно знать, т.к. это объясняет, когда вызывается декоратор, что представляют собой параметры нашей функции, почему они именно такие, а также что и как в декораторе можно сделать. Ниже приведен упрощенный код, в который компилируется наш класс с декоратором:


var __decorateMethod = function (decorators, target, key) {
    var descriptor = Object.getOwnPropertyDescriptor(target, key);
    for (var i = decorators.length - 1; i >= 0; i--) {
        var decorator = decorators[i];
        descriptor = decorator(target, key, descriptor) || descriptor; // Вызов функции декоратора
    }
    Object.defineProperty(target, key, descriptor);
};

// Объявление класса MyClass
var MyClass = (function () {
    function MyClass() {} // Конструктор
    MyClass.prototype.myMethod = function () { }; // метод myMethod

    // Вызов декораторов
    __decorateMethod([
        MyAdvancedDecorator("advanced info") // Вычисление выражения декоратора, и получение функции 
    ], MyClass.prototype, "myMethod");
    return MyClass;
}());


В таблице ниже приведено описание функции для каждого вида декораторов, а также ссылки на примеры в TypeScript Playground, где можно посмотреть, во что точно компилируются декораторы и попробовать их в действии.


Вид декоратора Сигнатура функции
Декоратор класса
Пример в playground

@MyDecorator 
class MyClass {}

function MyDecorator<TFunction extends Function>(target: TFunction): TFunction {
  return target;
}
  • target — конструктор класса
  • returns — конструктор класса или null. Если вернуть конструктор, то он заменит оригинальный. При этом необходимо также настроить прототип в новом конструкторе.

Декоратор метода
Пример в playground

class MyClass {
  @MyDecorator
  myMethod(){}
}

function MyDecorator(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  return descriptor;
}
  • target — прототип класса
  • propertyKey — имя метода (сохраняется при минификации); в текущей реализации тип — string
  • descriptorдескриптор метода*
  • returns — дескриптор метода* или null

Декоратор статического метода
Пример в playground

class MyClass {
  @MyDecorator
  static myMethod(){}
}

function MyDecorator(target: Function, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  return descriptor;
}
  • target — конструктор класса
  • propertyKey — имя метода (сохраняется при минификации); в текущей реализации тип — string
  • descriptorдескриптор метода*
  • returns — дескриптор метода* или null

Декоратор методов доступа
Пример в playground

class MyClass {
  @MyDecorator
  get myProperty(){}
}

Аналогично методу. Декоратор следует применять к первому методу доступа (get или set), в порядке объявления в классе.
Декоратор параметра
Пример в playground

class MyClass {
  myMethod(
    @MyDecorator val){
    }
}

function MyDecorator(target: Object, propertyKey: string | symbol, index: number): void { }
  • target — прототип класса
  • propertyKey — имя метода (сохраняется при минификации); в текущей реализации тип — string
  • index — индекс параметра в списке параметров
  • returns — void


Декоратор поля (свойства)
Пример в playground

class MyClass {
  @MyDecorator
  myField: number;
}

function MyDecorator(target: Object, propertyKey: string | symbol): TypedPropertyDescriptor<any> {
return null;
}
  • target — прототип класса
  • propertyKey — имя поля (сохраняется при минификации); в текущей реализации тип — string
  • returns — null или дескриптор свойства; если вернуть дескриптор, то он будет использован для вызова Object.defineProperty; однако, при подключении библиотеки reflect-metadata этого не происходит (это баг в reflect-metadata)

Декоратор статического поля (свойства)
Пример в playground

class MyClass {
  @MyDecorator
  static myField;
}

function MyDecorator(target: Function, propertyKey: string | symbol): TypedPropertyDescriptor<any> {
return null;
}
  • target — конструктор класса
  • propertyKey — имя поля (сохраняется при минификации); в текущей реализации тип — string
  • returns — null или дескриптор свойства; если вернуть дескриптор, то он будет использован для вызова Object.defineProperty; однако при подключении библиотеки reflect-metadata этого не происходит (это баг в reflect-metadata)

Интерфейсы Декораторы интерфейсов и их членов не поддерживаются.
Объявления типов Декораторы в объявлениях типов (ambient declarations) не поддерживаются.
Функции и переменные вне класса
Декораторы вне класса не поддерживаются.

Интерфейс TypedPropertyDescriptor<T>, фигурирующий в сигнатуре декораторов методов и свойств объявлен следующим образом:


interface TypedPropertyDescriptor<T> {
    enumerable?: boolean;
    configurable?: boolean;
    writable?: boolean;
    value?: T;
    get?: () => T;
    set?: (value: T) => void;
}

Если указать в объявлении декоратора конкретный тип T для TypedPropertyDescriptor, то можно ограничить тип свойств, к которым декоратор применим. Что означают члены этого интерфейса — можно посмотреть здесь. Если коротко, для метода value содержит собственно сам метод, для поля — значение, для свойства — get и set содержат соответствующие методы доступа.


Настройка среды


Поддержка декораторов экспериментальная и может измениться в будущих релизах (в TypeScript 2.0 не изменилась). Поэтому необходимо добавить experimentalDecorators: true в tsconfig.json. Кроме того, декораторы доступны только если target: es5 или выше.


tsconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}


  Важно!!!о target: ES3 и JSFiddle


Важно не забыть указать опцию target — ES5 при работе с декораторами. Если этого не сделать, то код скомпилируется без ошибок, но работать будет по-другому (это баг в компиляторе TypeScript). В частности, декораторам методов и свойств не будет передаваться третий параметр, а их возвращаемое значение будет игнорироваться.

Эти феномены можно наблюдать в JSFiddle (это уже баг в JSFiddle), поэтому в данной статье я не размещаю примеры в JSFiddle.

Тем не менее, есть обходное решение для этих багов. Нужно просто самим получать дескриптор, и самим же его обновлять. Например, вот реализация декоратора @safe, которая работает как с target ES3, так и с ES5.

Для использования информации о типах необходимо также добавить emitDecoratorMetadata: true.


tsconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Для использования класса Reflect необходимо установить дополнительный пакет reflect-metadata:


npm install reflect-metadata --save

И в коде:


import "reflect-metadata";

Однако если вы используете Angular 2, то ваша система сборки уже может содержать в себе реализацию Reflect, и после установки пакета reflect-metadata вы можете получить runtime ошибку Unexpected value 'YourComponent' exported by the module 'YourModule'. В этом случае лучше установить только typings.


typings install dt~reflect-metadata --global --save

Итак, перейдем к практике. Рассмотрим несколько примеров, демонстрирующих возможности декораторов.


safeавтоматическая обработка ошибок внутри функции



Допустим, у нас часто встречаются второстепенные функции, ошибки внутри которых мы хотели бы игнорировать. Писать каждый раз try/catch громоздко, на помощь приходит декоратор:


Реализация декоратора
function safe(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  // Запоминаем исходную функцию
  var originalMethod = descriptor.value;
  // Подменяем ее на нашу обертку
  descriptor.value = function SafeWrapper () {
    try {
      // Вызываем исходный метод
      originalMethod.apply(this, arguments);
    } catch(ex) {
      // Просто выводим в консоль, исполнение кода будет продолжено
      console.error(ex);
    }
  };
  // Обновляем дескриптор
  return descriptor;
}

class MyClass {
    @safe public foo(str: string): boolean {
     return str.length > 0; // если str == null, будет ошибка
  }
}
var test = new MyClass();
console.info("Starting...");
test.foo(null); 
console.info("Continue execution");

Результат выполнения:



Попробовать в действии в Plunker
Посмотреть в Playground


@OnChangeзадание обработчика изменения значения поля



Допустим, при изменении значения поля нужно выполнить какую-то логику. Можно, конечно, определить свойство с get/set методами, и в set поместить нужный код. А можно сократить объем кода, объявив декоратор:


Реализация декоратора
function OnChange<ClassT, T>(callback: (ClassT, T) => void): any {
    return (target: Object, propertyKey: string | symbol) => {
      // Необходимо задействовать существующий дескриптор, если он есть.
      // Это позволит объявять несколько декораторов на одном свойстве.
      var descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) 
        || {configurable: true, enumerable: true};
      // Подменяем или объявляем get и set
      var value: T;
      var originalGet = descriptor.get || (() => value);
      var originalSet = descriptor.set || (val => value = val);
      descriptor.get = originalGet;
      descriptor.set = function(newVal: T) {
        // Внимание, если определяем set через function, 
        // то this - текущий экземпляр класса,
        // если через лямбду, то this - Window!!!
        var currentVal = originalGet.call(this);
        if (newVal != currentVal) {
          // Вызываем статический метод callback с двумя параметрами
          callback.call(target.constructor, this, newVal);
        }
        originalSet.call(this, newVal);
      };
      // Объявляем новое свойство, либо обновляем дескриптор
      Object.defineProperty(target, propertyKey, descriptor);
      return descriptor;
    }
}

Обратите внимание, мы вызываем defineProperty и возвращаем дескриптор из декоратора. Это связано с багом в reflect-metadata, из-за которого для декоратора полей возвращаемое значение игнорируется.


class MyClass {
    @OnChange(MyClass.onFieldChange)
    public mMyField: number = 42;

    static onFieldChange(self: MyClass, newVal: number): void {
      console.info("Changing from " + self.mMyField + " to " + newVal);
    }
}
var test = new MyClass();
test.mMyField = 43;
test.mMyField = 44;

Результат выполнения:



» Попробовать в действии в Plunker
» Посмотреть в Playground
Нам пришлось обработчик объявить как static, т.к. трудно сосласться на экземплярный метод. Вот альтернативный вариант со строковым параметром, и другой с использованием лямбды.


Injectвнедрение зависимостей



Одной из интересных особенностей декораторов является возможность получать информацию о типе декорируемого свойства или параметра (скажем "спасибо" Angular, т.к. сделано было специально для него). Чтобы это заработало, нужно подключить библиотеку reflect-metadata, и включить опцию emitDecoratorMetadata (см. выше). После этого для свойств, которые имеют хотя бы один декоратор, можно вызвать Reflect.getMetadata с ключем "design:type", и получить конструктор соответствующего типа. Ниже простая реализация декоратора @Inject, который использует этот прием для внедрения зависимостей:


Реализация декоратора
// Объявляем декоратор
function Inject(target: Object, propKey: string): any {
    // Получаем конструктор типа свойства 
    // (в примере ниже это будет конструктор класса ILogService)
    var propType = Reflect.getMetadata("design:type", target, propKey);
    // Переопределяем декорируемое свойство
    var descriptor = {
        get: function () {
          // this - текущий объект класса
          var serviceLocator = this.serviceLocator || globalSericeLocator;
          return serviceLocator.getService(propType);  

        }
    };
    Object.defineProperty(target, propKey, descriptor);
    return descriptor;
}

Обратите внимание, мы вызываем defineProperty и возвращаем дескриптор из декоратора. Это связано с багом в reflect-metadata, из-за которого для декоратора полей возвращаемое значение игнорируется.


// Использовать интерфейс, к сожалению, не получится
abstract class ILogService {
    abstract log(msg: string): void;
} 
class Console1LogService extends ILogService {
  log(msg: string) { console.info(msg);  }
}
class Console2LogService extends ILogService {
  log(msg: string) { console.warn(msg); }
}
var globalSericeLocator = new ServiceLocator();
globalSericeLocator.registerService(ILogService, new ConsoleLogService1());
class MyClass {
  @Inject
  private logService: ILogService;
  sayHello() {
    this.logService.log("Hello there");
  }
}
var my = new MyClass();
my.sayHello();
my.serviceLocator = new ServiceLocator();
my.serviceLocator.registerService(ILogService, new ConsoleLogService2());
my.sayHello();

Реализация класса ServiceLocator
class ServiceLocator {
  services: [{interfaceType: Function, instance: Object }] = [] as any;

  registerService(interfaceType: Function, instance: Object) {
    var record = this.services.find(x => x.interfaceType == interfaceType);
    if (!record) {
      record = { interfaceType: interfaceType, instance: instance};
      this.services.push(record);
    } else {
      record.instance = instance;
    }
  }
  getService(interfaceType: Function) {
    return this.services.find(x => x.interfaceType == interfaceType).instance;
  }
}

Как видно, мы просто объявляем поле logService, а декоратор уже самостоятельно определяет его тип, и задает метод доступа, который получает соответствующий экземпляр сервиса. Красиво и удобно. Результат выполнения:



» Попробовать в Plunker
» Посмотреть в Playground


@JsonNameсериализация моделей c преобразованием




Допустим, по каким-то причинам необходимо переименовать некоторые поля объекта при сериализации в JSON. С помощью декоратора мы сможем объявить JSON-имя поля, а после, при сериализации, его прочитать. Технически данный декоратор иллюстрирует работу библиотеки reflect-metadata, а, в частности, функций Reflect.defineMetadata и Reflect.getMetadata.


Реализация декоратора
// Уникальный ключ для наших метаданных
const JsonNameMetadataKey = "Habrahabr_PFight77_JsonName";
// Декоратор
function JsonName(name: string) {
    return (target: Object, propertyKey: string) => {
        // Сохраняем в метаданных переднный name
        Reflect.defineMetadata(JsonNameMetadataKey, name, target, propertyKey);
    }
}
// Функция, работающая в паре с декоратором
function serialize(model: Object): string {
  var result = {};
  var target = Object.getPrototypeOf(model);
  for(var prop in model) {
    // Загружаем сохраненное декоратором значение
    var jsonName = Reflect.getMetadata(JsonNameMetadataKey, target, prop) || prop;    
    result[jsonName] = model[prop];
  }
  return JSON.stringify(result);
}

class Model {
  @JsonName("name")
  public title: string;
}

var model = new Model();
model.title = "Hello there";
var json = serialize(model);
console.info(JSON.stringify(moel));
console.info(json);

Результат выполнения:



» Попробовать в Plunker
» Посмотреть в Playground


Приведенный декоратор обладает тем недостатком, что, если модель содержит в качестве полей объекты других классов, то поля этих классов никак не обрабатываются методом serialize (то есть к ним нельзя применить декоратор @JsonName). Кроме того, здесь не реализовано обратное преобразование — из JSON в клиентскую модель. Оба этих недостатка исправлены в несколько более сложной реализации конвертера серверных моделей, в спойлере ниже.


@ServerModelField - конвертер серверных моделей на декораторах

@ServerModelField — конвертер серверных моделей на декораторах


Постановка задачи следующая. С сервера к нам прилетают некоторые JSON-данные примерно такого вида (похожий JSON шлет один BaaS сервис):


{
    "username":"PFight77",
    "email":"test@gmail.com",
    "doc": {
        "info":"The author of the article"
    }
}

Мы хотим конвертировать эти данные в типизированный объект, переименовывая некоторые поля. Выглядеть в конечном счете все будет так:


class UserAdditionalInfo {
    @ServerModelField("info")
    public mRole: string;
}
class UserInfo {
    @ServerModelField("username")
    private mUserName: string;
    @ServerModelField("email")
    private mEmail: string;
    @ServerModelField("doc")
    private mAdditionalInfo: UserAdditionalInfo;

    public get DisplayName() {
        return mUserName + " " + mAdditionalInfo.mRole;
    }
    public get ID() {
        return mEmail;
    }    
    public static parse(jsonData: string): UserInfo {
        return convertFromServer(JSON.parse(jsonData), UserInfo);
    }
    public serialize(): string {
        var serverData = convertToServer(this);
        return JSON.stringify(serverData);
    }
}

Разберем, как это реализовано.


Во-первых, нам необходимо определить декоратор поля ServerModelField, который будет принимать строковый параметр и сохранять его в метаданных. Кроме того, для разбора JSON нам еще нужно знать, какие поля с нашим декоратором есть в классе вообще. Для этого объявим еще один экземпляр метаданных, общий для всех полей класса, в котором и сохраним имена всех декорированных членов. Здесь мы уже будем не только сохранять метаданные через Relect.defineMetadata, но и получать через Reflect.getMetadata.


// Объявляем уникальные ключи, по которым будем идентифицировать наши метаданные
const ServerNameMetadataKey = "Habrahabr_PFight77_ServerName";
const AvailableFieldsMetadataKey = "Habrahabr_PFight77_AvailableFields";
// Объявляем декоратор
export function ServerModelField(name?: string) {
    return (target: Object, propertyKey: string) => {
        // Сохраняем в метаданных переданный name, либо название самого свойства, если параметр не задан
        Reflect.defineMetadata(ServerNameMetadataKey, name || propertyKey, target, propertyKey);
        // Проверяем, не определены ли уже availableFields другим экземпляром декоратора
        var availableFields = Reflect.getMetadata(AvailableFieldsMetadataKey, target);
        if (!availableFields) {
            // Ok, мы первые, значит создаем новый массив
            availableFields = [];
            // Не передаем 4-й параметр(propertyKey) в defineMetadata, 
            // т.к. метаданные общие для всех полей
            Reflect.defineMetadata(AvailableFieldsMetadataKey, availableFields, target);            
        }
        // Регистрируем текущее поле в метаданных
        availableFields.push(propertyKey);
    }
}

Ну и осталось написать функцию convertFromServer. В ней почти нет ничего особенного, она просто вызывает Reflect.getMetadata и использует полученные метаданные для разбора JSON. Одна особенность — эта функция должна создать экземпляр UserInfo через new, поэтому мы передаем ей помимо JSON-данных еще и класс: convertFromServer(JSON.parse(data), UserInfo). Чтобы понять, как это работает, посмотрите спойлер ниже.


Передача класса параметром
class MyClass {
}
// Объявляем переменную типа "конструктор класса без параметров"
var myType: { new(): any; }; 
// Присваиваем переменной наш класс
myType = MyClass; 
// Эквивалентно new MyClass()
var obj = new myType();

Вторая особенность — это использование данных о типе поля, генерируемых благодаря настройке "emitDecoratorMetadata": true в tsconfig.json. Прием заключается в вызове Reflect.getMetadata с ключом "design:type", который возвращает конструктор соответствующего типа. Например, вызов Reflect.getMetadata("design:type", target, "mAdditionalInfo") вернет конструктор UserAdditionalInfo. Мы будем использовать эту информацию для того, чтобы правильно обрабатывать поля пользовательских типов. Например, класс UserAdditionalInfo также использует декоратор @ServerModelField, поэтому мы должны также использовать эти метаданные для анализа JSON.


Третья особенность заключается в получении соответствующего target, откуда мы будем брать метаданные. Мы используем декораторы полей, поэтому метаданные нужно брать из прототипа класса. Для декораторов статических членов нужно использовать конструктор класса. Получить прототип можно, вызвав Object.getPrototypeOf или же обратившись к свойству prototype конструктора.


Все остальные комментарии в коде:


export function convertFromServer<T>(serverObj: Object, type: { new(): T ;} ): T {
    // Создаем объект, с помощью конструктора, переданного в параметре type
    var clientObj: T = new type();
    // Получаем контейнер с метаданными
    var target = Object.getPrototypeOf(clientObj);
    // Получаем из метаданных, какие декорированные свойства есть в классе
    var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string];
    if (availableNames) {
        // Обрабатываем каждое свойство
        availableNames.forEach(propName => {
            // Получаем из метаданных имя свойства в JSON
            var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName);
            if (serverName) {
                // Получаем значение, переданное сервером
                var serverVal = serverObj[serverName];
                if (serverVal) {
                    var clientVal = null;
                    // Проверяем, используются ли в классе свойства декораторы @ServerModelField
                    // Получаем конструктор класса
                    var propType = Reflect.getMetadata("design:type", target, propName);
                    // Смотрим, есть ли в метаданных класса информация о свойствах
                    var propTypeServerFields =  Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string];
                    if (propTypeServerFields) {
                        // Да, класс использует наш декоратор, обрабатываем свойство рекурсивно
                        clientVal = convertFromServer(serverVal, propType);
                    } else {
                        // Нет, просто копируем значение
                        clientVal = serverVal;
                    }
                    // Записываем результат в конечный объект
                    clientObj[propName] = clientVal;
                }
            }
        });
    } else {
        errorNoPropertiesFound(getTypeName(type));
    }

    return clientObj;
}
function errorNoPropertiesFound<T>(typeName: string) {
    throw new Error("There is no @ServerModelField directives in type '" + typeName + "'. Nothing to convert.");
}

function getTypeName<T>(type: { new(): T ;}) {
     return parseTypeName(type.toString());
}

function parseTypeName(ctorStr: string) {
     var matches = ctorStr.match(/\w+/g);
     if (matches.length > 1) {
         return matches[1];
     } else {
         return "<can not determine type name>";

    }
}

Аналогичный вид имеет обратная функция — convertToServer.


Функция convertToServer
function convertToServer<T>(clientObj: T): Object {
    var serverObj = {};

    var target = Object.getPrototypeOf(clientObj);
    var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string];
    availableNames.forEach(propName=> {        
        var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName);
        if (serverName) {
            var clientVal = clientObj[propName];
            if (clientVal) {
                var serverVal = null;
                var propType = Reflect.getMetadata("design:type", target, propName);
                var propTypeServerFields =  Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string];
                if (clientVal && propTypeServerFields) {
                    serverVal = convertToServer(clientVal);
                } else {
                    serverVal = clientVal;
                }
                serverObj[serverName] = serverVal;
            }
        }
    });

    if (!availableNames) {
        errorNoPropertiesFound(parseTypeName(clientObj.constructor.toString()));
    }

    return serverObj;
}

Работу декоратора @ServerModelField в действии можно посмотреть в plunker.


Controller, Actionсервисы для взаимодействия с сервером



В ASP.NET сервер, как правило, состоит из контроллеров, которые содержат методы. Соответственно, url методов выглядит обычно, как /ControllerName/ActionName. В клиентском коде хорошей практикой будет сделать единую точку, через которую будут происходить все запросы к серверу вообще, и к каждому контроллеру в частности. Это позволит упросить рефакторинг, облегчит внедрение общей логики обработки ошибок и т.п.


С помощью декораторов можно красиво объявлять классы TypeScript, которые будут соответствовать контроллерам на сервере. Объявление методов при этом мы постараемся максимально упростить, так чтобы они содержали только одну строчку, а url будем формировать на основе информации из декораторов.


Реализация декоратора
var ControllerNameMetadataKey = "Habr_PFight77_ControllerName";
// Первый декоратор. 
// К сожалению, нет надежного способа узнать 
// имя класса (устойчивого к минификации),
// поэтому имя класса придется передавать вручную.
function Controller(name: string) {
    return (target: Function) {
    Reflect.defineMetadata(ControllerNameMetadataKey, name, target.prototype);
  };
}
// Второй декоратор, применяемый к методам
function Action(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>): TypedPropertyDescriptor<any> {
  // Запоминаем исходную функцию
  var originalMethod = descriptor.value;
  // Подменяем ее на нашу обертку
  descriptor.value = function ActionWrapper () {
        // Получаем url, сохраненное декоратором Controller 
      var controllerName = Reflect.getMetadata(ControllerNameMetadataKey, target);
      // Формируем url вида /ControllerName/ActionName
      var url = "/" + controllerName + "/" + propertyKey;
      // Передаем url последним параметром
      [].push.call(arguments, url);
      // Вызываем исходный метод с дополнительным параметром
      originalMethod.apply(this, arguments);
  };
  // Обновляем дескриптор
  return descriptor;
}
// Функция, упрощающая объявление методов
function post(data: any, args: IArguments): any {
  // Получаем url, переданный декоратором @Action
  var url = args[args.length - 1];
  return $.ajax({ url: url, data: data, method: "POST" });
}

@Controller("Account")
class AccountController {
  @Action
  public Login(data: any): any {
    return post(data, arguments);
  }
}
var Account = new AccountController();
Account.Login({ username: "user", password: "111"});

Результат выполнения:




» Попробовать в Plunker
» Посмотреть в Playground

Можно также добавить декораторы параметров так, чтобы сигнатура метода в TypeScript полностью повторяла сигнатуру серверного метода. С помощью декораторов можно сохранять имя каждого параметра и при выполнении запроса формировать на основе этих данных соответствующий JSON. К сожалению, получить имя параметра в коде декораторы не позволяют, поэтому придется передавать имя в декоратор вручную (так же, как в декоратор Controller).


Заключение


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


Как было продемонстрировано в статье, в нашем распоряжении следующий ряд приемов:


  1. Модификация дескриптора метода или свойства. В частности, можно подменить метод оберткой, задать дескриптор для поля, с объявлением методов доступа и т.д. В целом, из декоратора можно произвести любую трансформацию прототипа класса.


  2. Сохранение и использование метаданных при помощи класса Reflect. Мы можем передать в декоратор любое значение, также нам доступно имя свойства или метода, устойчивое к минифкации.


  3. Получение информации о типе при помощи вызова Reflect.getMetada с ключом "design:type".

Использование этих приемов может быть самым разнообразным, в зависимости от конкретных нужд. Например, в Легком Клиенте 8 мы активно используем декораторы для объявления сервисов взаимодействия с сервером. Наша реализация чуть сложнее представленной в статье (мы используем декораторы параметров), но в целом построена по тому же принципу. Кроме того, мы думаем еще задействовать несколько декораторов для объявления публичного API наших ReactJS компонентов, а также автоматизировать привязку обработчиков событий к this.


На этом пока все. Пишите впечатления в комментариях, делитесь своим опытом использования декораторов.


UPD. Как заметил whileTrue, в ES7 декораторы не вошли. Будем надеяться, что хотя бы в ES8 попадут.


Tags:
Hubs:
+56
Comments 33
Comments Comments 33

Articles

Information

Website
www.docsvision.com
Registered
Founded
Employees
51–100 employees
Location
Россия