Добрый день.
Понравилась атрибутная регистрация компонентов в angular2 и захотелось сделать подобное в проекте с knockoutjs.
Компоненты в нокауте появились довольно давно. Тем не менее, отсутствие встроенной поддержки dependency injection, как и необходимость отдельной регистрации компонент несколько раздражала.
В данной статье я не хочу рассказывать про component loader'ы и как их использовать, чтобы добавить поддержку DI. Скажу лишь, что в итоге модифицировал этот пакет.
Использование:
params это те параметры, которые были переданы компоненту через разметку.
Проблема здесь в том, что регистрация компонент не слишком удобна. Легко опечататься и легко забыть зарегистрировать сервис; также хотелось сделать зависимости более явными.
Для реализации задумки нужно понять как работают декораторы в typescript. Если вкратце, то это просто некая функция или фабрика, которая будет вызвана в некоторый момент времени (в какой из можно прочитать в документации).
Декоратор регистрации компонента:
Как видно, декоратор не делает ничего особенного. Вся магия в функции getFactory:
Здесь с помощью Reflect.getMetadata(«design:paramtypes», target) мы вытащили информацию о типах принимаемых аргументов в конструкторе компонента (для того, чтобы это заработало, нужно включить опцию в транспайлере typeScript'a — об этом ниже) и затем просто собрали фабрику для IoC из type.name.
Теперь немного подробнее об injectParamateres. Что если мы хотим инжектировать не какой-то инстанс класса, а просто Object, например, конфигурацию приложения или params переданный компоненту? В ангулар2 для этого используется декоратор Inject, применяемый к параметрам конструктора:
Вот его реализация:
И напоследок декоратор регистрации сервиса:
Поскольку декораторы ещё не вошли в стандарт, для их использования в файле tsconfig.json нужно включить experimentalDecorators и emitDecoratorMetadata.
Также, так как при регистрации зависимостей мы полагаемся на имена функций-конструкторов, то важно включить опцию keep_fnames в настройках UglifyJS.
Исходный код можно найти здесь.
Понравилась атрибутная регистрация компонентов в angular2 и захотелось сделать подобное в проекте с knockoutjs.
@Component({ selector: "setup-add-edit-street-name", template: require("text!./AddEditStreetName.tmpl.html"), directives: [BeatSelector] }) export class AddEditStreetNameComponent extends AddEditModalBaseComponent<StreetNameViewModel> { constructor(@Inject("params") params, streetNameService: StreetNameService) { super(params, streetNameService); } location = ko.observable() }
Компоненты в нокауте появились довольно давно. Тем не менее, отсутствие встроенной поддержки dependency injection, как и необходимость отдельной регистрации компонент несколько раздражала.
Dependency Injection
В данной статье я не хочу рассказывать про component loader'ы и как их использовать, чтобы добавить поддержку DI. Скажу лишь, что в итоге модифицировал этот пакет.
Использование:
// регистрации фабрики kontainer.registerFactory('taskService', ['http', 'fileUploadService', (http, fileUploadService) => new TaskService(http, fileUploadService)); // регистрация самого компонента ko.components.register('task-component', { viewModel: ['params', 'taskService', (service) => new TaskComponent(params, service) ], template: '<p data-bind="text: name"></p>' });
params это те параметры, которые были переданы компоненту через разметку.
Проблема здесь в том, что регистрация компонент не слишком удобна. Легко опечататься и легко забыть зарегистрировать сервис; также хотелось сделать зависимости более явными.
Решение
Для реализации задумки нужно понять как работают декораторы в typescript. Если вкратце, то это просто некая функция или фабрика, которая будет вызвана в некоторый момент времени (в какой из можно прочитать в документации).
Декоратор регистрации компонента:
export interface ComponentParams { selector: string; template?: string; templateUrl?: string; directives?: Function[]; } export function Component(options: ComponentParams) { return (target: { new (...args) }) => { if (!ko.components.isRegistered(options.selector)) { if (!options.template && !options.templateUrl) { throw Error(`Component ${target.name} must have template`); } const factory = getFactory(target); const config = { template: options.template || { require: options.templateUrl }, viewModel: factory }; ko.components.register(options.selector, config); } }; }
Как видно, декоратор не делает ничего особенного. Вся магия в функции getFactory:
interface InjectParam { index: number; dependency: string; } const injectMetadataKey = Symbol("inject"); function getFactory(target: { new (...args) }) { const deps = Reflect.getMetadata("design:paramtypes", target).map(type => type.name); const injectParameters: InjectParam[] = Reflect.getOwnMetadata(injectMetadataKey, target) || []; for (const param of injectParameters) { deps[param.index] = param.dependency; } const factory = (...args) => new target(...args); return [...deps, factory]; }
Здесь с помощью Reflect.getMetadata(«design:paramtypes», target) мы вытащили информацию о типах принимаемых аргументов в конструкторе компонента (для того, чтобы это заработало, нужно включить опцию в транспайлере typeScript'a — об этом ниже) и затем просто собрали фабрику для IoC из type.name.
Теперь немного подробнее об injectParamateres. Что если мы хотим инжектировать не какой-то инстанс класса, а просто Object, например, конфигурацию приложения или params переданный компоненту? В ангулар2 для этого используется декоратор Inject, применяемый к параметрам конструктора:
constructor(@Inject("params") params, streetNameService: StreetNameService) { super(params, streetNameService); }
Вот его реализация:
interface InjectParam { index: number; dependency: string; } const injectMetadataKey = Symbol("inject"); export function Inject(token: string) { return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { const existingInjectParameters: InjectParam[] = Reflect.getOwnMetadata(injectMetadataKey, target, propertyKey) || []; existingInjectParameters.push({ index: parameterIndex, dependency: token }); Reflect.defineMetadata(injectMetadataKey, existingInjectParameters, target, propertyKey); }; }
И напоследок декоратор регистрации сервиса:
export function Injectable() { return (target: { new (...args) }) => { if (!kontainer.isRegistered(target.name)) { const factory = getFactory(target); kontainer.registerFactory(target.name, factory); } }; } // использование @Injectable() export class StreetNameService { constructor(config: AppConfig, @Inject("ApiClientFactory") apiClientFactory: ApiClientFactory) { this._apiClient = apiClientFactory(config.endpoints.streetName); } // ... }
Как это всё завести?
Поскольку декораторы ещё не вошли в стандарт, для их использования в файле tsconfig.json нужно включить experimentalDecorators и emitDecoratorMetadata.
Также, так как при регистрации зависимостей мы полагаемся на имена функций-конструкторов, то важно включить опцию keep_fnames в настройках UglifyJS.
Исходный код можно найти здесь.