#typescript #decorator
#typescript #декоратор
Вопрос:
Я работаю над библиотекой, которая собирается реализовать некоторые пользовательские настройки маршрутизации веб-запросов. И я хочу иметь возможность реализовать эту функциональность, используя декораторы typescirpt, подобные этому.
@Controller({ path: '/api' })
class TestController {
@Route('get', '/')
get() {
return 'banana';
}
}
Проблема, с которой я сталкиваюсь, заключается в том, что, похоже, я не могу связать «дочерний» декоратор метода с «родительским» декоратором класса.
У меня есть несколько довольно простых фабрик декораторов, которые вы можете увидеть здесь:
export function Controller(params?: IControllerParams) {
const func: ClassDecorator = (target) => {
registerController(target, params || {});
logger.info(`Registered controller: ${target.name}`);
console.dir(target); // [Function: TestController]
};
return func;
}
export function Route(verb: Verb, path: string) {
const func: MethodDecorator = (target, key) => {
registerRoute(verb, path, key, target);
logger.info(`Registered route: ${path} for verb: ${verb}`);
console.dir(target); // TestController {}
};
return func;
}
Теперь проблема в том, что целевые типы, возвращаемые каждым из экземпляров декоратора, немного отличаются, что означает, что я не могу их сравнивать.
Метод class возвращает сигнатуру функции для моего класса, а метод method возвращает сигнатуру именованного объекта.
Я чего-то не понимаю? Я видел, как другие библиотеки делают такую связь, поэтому я знаю, что это должно быть возможно!
Комментарии:
1. Я не совсем понимаю проблему, не могли бы вы, пожалуйста, пояснить?
2. Мне нужно было иметь возможность получить связь между обоими декораторами, поскольку каждый из них содержит информацию (путь), которую необходимо объединить, чтобы моя маршрутизация работала. Я не мог понять, как создать эти две ссылки, пока не сделал то, что сказал в своем ответе
Ответ №1:
Я действительно сталкивался с этой точной проблемой раньше, и есть значительные сложности.
Во-первых, вы можете очень легко потерять доступ к значению «this», поэтому вы должны быть осторожны. Альтернативой является обработка каждой функции как статического, чистого метода, который просто определен в объекте. Второй — это порядок, в котором оцениваются декораторы, который, как вы, вероятно, уже знаете, выворачивается наизнанку.
Принимая во внимание оба этих фактора, вот что я сделал. Я использовал этот код с Meteor, который сделал что-то очень похожее на то, что вы делаете.
Преамбула
Серверный модуль был просто классом с серией обработчиков методов. ОН ЖЕ контроллер, и этот код был создан для Meteor.
Код
/**
* This is horribly ugly code that I hate reading myself,
* but it is very straightforward. It defines a getter
* property called __modulle, and returns the data that
* we care about in a format that is readable for a future
* registry/bootstrapping system
*/
function boltModuleProperty(proto: any) {
Object.defineProperty(proto, '__module', {
get: function () {
let obj: IModuleDetails = {};
for (let key in this.__moduleFunctions)
obj[`${this.__moduleName}.${key}`] = this.__moduleFunctions[key];
return obj;
}
})
}
/**
* This is evaluated at the very end.
*
* Collect all the methods and publications, registering
* them with Meteor so they become available via the
* default Meteor Methods and Subscriptions.
*/
export function ServerModule (moduleName?: string) {
return function (target: any) {
boltModuleProperty(target.prototype);
// Use either a passed-in name, or the class' name
target.prototype.__moduleName = moduleName || target.name;
}
}
/**
* Take the name of the method to be exposed for Meteor,
* and save it to the object's prototype for later. We
* we do this so we can access each method for future
* registration with Meteor's 'method' function
*/
export function ServerMethod (name: string = null) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
let fnName = name || descriptor.value.name;
// ensure we actually get the real prototype
let proto = target.prototype ? target.prototype : target.constructor.prototype
if (!proto.__moduleFunctions) proto.__moduleFunctions = {};
proto.__moduleFunctions[fnName] = descriptor.value;
}
}
Объяснение
Вы определяете дополнительную информацию о классе в формате, который вы способны прочитать и понять. Каждый метод / свойство, которые вы используете внутри класса, должны хранить информацию о себе и НЕ выполнять НИКАКИХ действий. Декоратор никогда не должен вызывать какой-либо внешний побочный эффект когда-либо. Я подчеркиваю это как важный момент только потому, что вы не хотите потерять представление о том, как все происходит в вашей кодовой базе.
Теперь, когда у нас есть некоторый код для просмотра, мы должны обойти эту надоедливую регистрацию и не потерять доступ к некоторому потенциально связанному коду. У нас есть все, что нам нужно, через недавно созданное __module
свойство класса, но оно пока не отображается через typescript.
Здесь два варианта:
let myInstance: IServerModule amp; MyClass = new MyClass();
// or
let myInstance: any = new MyClass();
Настройка
Как бы вы ни обращались к регистрации метода (express.get и т.д.), Вы хотите иметь что-то, что принимает ссылку на класс, сохраняет ее в реестре (буквально просто массив в каком-нибудь загрузочном файле, аналогичном модулям Angular) и регистрирует все в этом файле загрузки / модуля.
Получите доступ к __module
свойству, прочитайте сохраненную вами информацию и зарегистрируйте ее по мере необходимости. Таким образом, вы достигаете разделения задач, у вас есть четкое понимание того, что создается в вашем приложении, и вы можете использовать свои декораторы именно так, как вы считаете необходимым.
Комментарии:
1. Спасибо за подробный ответ! Я сам пришел к аналогичным выводам. Но приятно видеть за этим какой-то реальный код. Я немного разочарован тем, что эта функция существует (учитывая ее экспериментальность), но вам нужно преодолеть ТАК МНОГО препятствий, чтобы сделать ее полезной. В конечном итоге я мог бы использовать пакет reflect-metadata, который теоретически выполняет за вас большую часть основной работы. Я думаю, что это работает очень похоже на ваше решение.
Ответ №2:
И, конечно, я работаю над проблемой вскоре после публикации. Мне просто нужно сравнить прототип целевого класса ‘parent’ с целевым методом ‘child’, и они совпадут.