Angular2-like реєстрація компонентів і залежностей для knockoutjs

Добрий день.

Сподобалася атрибутная реєстрація компонентів у 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.вами()
}

Компоненти в нокауті з'явилися досить давно. Тим не менше, відсутність вбудованої підтримки 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.

Вихідний код можна знайти на тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.