Эта статья — третья часть серии:
- Часть 1: Декораторы методов
- Часть 2: Декораторы свойств и классов
- Часть 3: Декораторы параметров и фабрика декораторов
- Часть 4: Сериализация типов и metadata reflection API
В прошлый раз мы узнали, что такое декораторы и как они реализованы в TypeScript. Мы знаем, как работать с декораторами классов, свойств и методов.
В этой статье мы расскажем про:
- Последний оставшийся тип декораторов — декоратор параметра
- Реализацию фабрики декораторов
- Реализацию конфигурируемых декораторов
Мы будем использовать нижеследующий класс для демонстрации данных концепций:
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
public saySomething(something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
Декораторы параметров
Как мы уже знаем, сигнатура декоратора параметра выглядит следующим образом:
declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) => void;
Использование декоратора под названием
logParameter
будет выглядеть так:class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
При компиляции в JavaScript здесь вызывается метод
__decorate
(о нем мы говорили в первой части). Object.defineProperty(Person.prototype, "saySomething",
__decorate([
__param(0, logParameter)
], Person.prototype, "saySomething", Object.getOwnPropertyDescriptor(Person.prototype, "saySomething")));
return Person;
По аналогии с предыдущими типами декораторов, мы можем предположить, что раз вызывается метод
Object.defineProperty
, метод saySomething
будет заменен результатом вызова функции __decorate
(как в декораторе метода). Это предположение неверно.Если внимательно посмотреть на код выше, можно заметить, что там есть новая функция
__param
. Она была сгенерирована компилятором TypeScript и выглядит следующим образом:var __param = this.__param || function (index, decorator) {
// return a decorator function (wrapper)
return function (target, key) {
// apply decorator (return is ignored)
decorator(target, key, index);
}
};
Функция
__param
возвращает декоратор, который оборачивает декоратор, переданный на вход (с именем decorator
).Можно заметить, что когда декоратор параметра вызывается, его значение игнорируется. Это значит, что при вызове функции
__decorate
, результат ее выполнения не переопределит метод saySomething
.Поэтому декораторы параметров ничего не возвращают.
Оборачивание декоратора в
__param
используется, чтобы сохранить индекс (позицию декорируемого параметра в списке аргументов) в замыкании. class foo {
// foo index === 0
public foo(@logParameter foo: string) : string {
return "bar";
}
// bar index === 1
public foobar(foo: string, @logParameter bar: string) : string {
return "foobar";
}
}
Теперь мы знаем, что декоратор параметра принимает 3 аргумента:
- Прототип декорируемого класса
- Имя метода, содержащего декорируемый параметр
- Индекс декорируемого параметра
Давайте реализуем
logProperty
function logParameter(target: any, key : string, index : number) {
var metadataKey = `log_${key}_parameters`;
if (Array.isArray(target[metadataKey])) {
target[metadataKey].push(index);
}
else {
target[metadataKey] = [index];
}
}
Декоратор параметра, описанный выше, добавляет новое свойство (
metadataKey
) в прототип класса. Это свойство — массив, содержащий индексы декорируемых параметров. Мы можем считать это свойство метаданными.Предполагается, что декоратор параметра не используется для модификации поведения конструктора, метода или свойства. Декораторы параметров должны использоваться только для создания различных метаданных.
Как только метаданные созданы, мы можем использовать другой декоратор для их чтения. К примеру, ниже приведена модифицированная версия декоратора метода из второй части статьи.
Исходная версия выводила в консоль название метода и все ее аргументы при вызове. Новая версия читает метаданные, и на их основе выводит только те аргументы, которые помечены соответствующим декоратором параметра.
class Person {
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@logMethod
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
function logMethod(target: Function, key: string, descriptor: any) {
var originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
var metadataKey = `__log_${key}_parameters`;
var indices = target[metadataKey];
if (Array.isArray(indices)) {
for (var i = 0; i < args.length; i++) {
if (indices.indexOf(i) !== -1) {
var arg = args[i];
var argStr = JSON.stringify(arg) || arg.toString();
console.log(`${key} arg[${i}]: ${argStr}`);
}
}
var result = originalMethod.apply(this, args);
return result;
}
else {
var a = args.map(a => (JSON.stringify(a) || a.toString())).join();
var result = originalMethod.apply(this, args);
var r = JSON.stringify(result);
console.log(`Call: ${key}(${a}) => ${r}`);
return result;
}
}
return descriptor;
}
В следующей части мы узнаем лучший способ работы с метаданными: Metadata Reflection API. Вот небольшой пример того, что мы изучим:
function logParameter(target: any, key: string, index: number) {
var indices = Reflect.getMetadata(`log_${key}_parameters`, target, key) || [];
indices.push(index);
Reflect.defineMetadata(`log_${key}_parameters`, indices, target, key);
}
Фабрика декораторов
Официальный proposal декораторов в TypeScript дает следующее определение фабрики декораторов:
Фабрика декораторов — это функция, которая может принимать любое количество аргументов и возвращает декоратор одного из типов.
Мы уже научились реализовывать и использовать все типы декораторов (класса, метода, свойства и параметра), но кое-что мы можем улучшить. Допустим, у нас есть такой фрагмент кода:
@logClass
class Person {
@logProperty
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@logMethod
public saySomething(@logParameter something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
Он работает, как полагается, но было бы лучше, если бы можно было везде использовать один и тот же декоратор, не заботясь о его типе, как в этом примере:
@log
class Person {
@log
public name: string;
public surname: string;
constructor(name : string, surname : string) {
this.name = name;
this.surname = surname;
}
@log
public saySomething(@log something : string) : string {
return this.name + " " + this.surname + " says: " + something;
}
}
Добиться этого мы можем, обернув декораторы в фабрику. Фабрика может определить тип необходимого декоратора по аргументам, переданным в нее:
function log(...args : any[]) {
switch(args.length) {
case 1:
return logClass.apply(this, args);
case 2:
return logProperty.apply(this, args);
case 3:
if(typeof args[2] === "number") {
return logParameter.apply(this, args);
}
return logMethod.apply(this, args);
default:
throw new Error("Decorators are not valid here!");
}
}
Конфигурируемые декораторы
Последний момент, который бы хотелось обсудить в этой статье, это то, как мы можем передавать аргументы в декоратор при его использовании.
@logClassWithArgs({ when : { name : "remo"} })
class Person {
public name: string;
// ...
}
Мы можем воспользоваться фабрикой декораторов для создания конфигурируемых декораторов:
function logClassWithArgs(filter: Object) {
return (target: Object) => {
// реализация декоратора класса будет тут, декоратор
// будет иметь доступ к параметрам декоратора (filter),
// потому что они хранятся в замыкании
}
}
Ту же идею мы можем применить для остальных типов декораторов.
Заключение
Теперь у нас есть глубокое понимание всех четырех существующих видов декораторов, того как создавать фабрики декораторов и как их параметризовать.
В следующей статье мы узнаем, как использовать Metadata Reflection API.
Поделиться с друзьями
babylon
Не осточертело обёртки над методами писать?