Aurelia DI с интерфейсами typescript
Я просмотрел документацию Aurelia DI и посмотрел исходный код и хотел поделиться тем, чего я пытаюсь достичь, чтобы меня можно было сбить, если мне не хватает чего-то очевидного. Я посмотрел образцы здесь для TS с Aurelia, но я не вижу, как он будет работать, а документы отсутствуют.
Я хочу:
dataProvider.js (интерфейс поставщика данных)
export interface DataProvider {
getData(): number;
}
itemDisplayer1.js (класс, который будет потреблять внедренный класс, реализующий интерфейс)
import {inject} from 'aurelia-framework';
import {DataProvider} from './dataProvider';
@inject(DataProvider)
export class itemDisplayer1 {
constructor(public dataProvider: DataProvider) {
this.dataProvider = dataProvider;
this.data = dataProvider.getData();
}
}
itemDisplayer2.js (другой класс, который будет использовать введенный класс, реализующий интерфейс)
import {inject} from 'aurelia-framework';
import {DataProvider} from './dataProvider';
@inject(DataProvider)
export class itemDisplayer2 {
constructor(public dataProvider: DataProvider) {
this.dataProvider = dataProvider;
this.data = dataProvider.getData();
}
}
GoodDataProvider.js
import {DataProvider} from "./dataProvider";
export class GoodDataProvider implements DataProvider {
data = 1;
getData() {
return this.data;
}
}
BetterDataProvider.js
import {DataProvider} from "./dataProvider";
export class BetterDataProvider implements DataProvider {
data = 2;
getData() {
return this.data;
}
}
И затем где-нибудь (?) я хотел бы настроить, что itemDisplayer1 должен быть предоставлен экземпляром GoodDataProvider, а itemDisplayer2 должен быть предоставлен экземпляром BetterDataProvider (1).
Затем возникает проблема контекста DI. Я не уверен, как использовать container.createChild(). Там не так много информации об этом, что я могу найти. Он создает дочерний контейнер и при необходимости передает делегату родительский элемент, но если я создам 2 дочерних контейнера и зарегистрирую один из 2 провайдеров с каждым дочерним контейнером, как бы классы itemDisplayer знали, какой из них использовать (без изменения их определений и инъекции в родительский контейнер и т.д.)?
Примечание. Информация управления жизненным циклом не распространяется на потребителей или поставщиков зависимостей (это часто делается в примерах Aurelia DI и кажется немного произведенным). Я ожидал бы, что это будет определено, когда потребители и поставщики будут связаны - point '(1)' выше.
В заключение, возможно ли это? Это что-то, что есть на картах для ближайшего будущего? Должен ли я пытаться заменить Aurelia DI на пользовательский контейнер, который отвечает моим потребностям?
(Причина, по которой я пытаюсь сделать это, заключается в том, что для оценки js-фреймворков в качестве одного из критериев необходимо, чтобы в среде фреймворков была продемонстрирована зрелая система DI с возможностями управления временем жизни /AOP и т.д.)
Ответы
Ответ 1
from @eisenbergeffect: DI будет получать некоторый внутренний капитальный ремонт, как только мы получим контрольные показатели.
Но в соответствующей заметке он не может работать с интерфейсами, потому что TypeScript компилирует их во время выполнения.
Вам нужно будет придумать уникальные ключи при регистрации различных типов в контейнере DI, а затем указать соответствующий уникальный ключ в инструкции @Inject (xxx). Ключи могут быть любыми, что вам нравится. Обычно люди используют этот тип для уникального ключа (это вызывает некоторую путаницу), но вы можете использовать строки, цифры или что-то еще, что вам нравится.
модульные тесты также информативны: https://github.com/aurelia/dependency-injection/blob/master/test/container.spec.js
Ответ 2
Как сказал Майк, Aurelia еще не поддерживает эту функцию разрешения зависимостей. И интерфейсы скомпилированы, поэтому они не могут использоваться в качестве ключей (например, container.registerInstance(ISomething, new ConcreteSomething());
Однако есть небольшой трюк, который может заставить его выглядеть так, как будто вы используете сам интерфейс в качестве ключа.
foo.ts:
export interface IFoo {
// interface
}
export const IFoo = Symbol();
bar.ts:
import {IFoo} from "./foo.ts";
export class Bar implements IFoo {
// implementation
}
main.ts:
import {IFoo} from "./foo.ts";
import {Bar} from "./bar.ts";
...
container.registerInstance(IFoo, new Bar());
...
Это компилируется отлично, и компилятор знает, когда использовать правильный повторяющийся тип, основанный на контексте, в котором он используется.
Ответ 3
Итак, как заявлено другими, TS компилирует интерфейсы, и в настоящее время нет способа сделать это с чистыми интерфейсами. Однако интересная и часто пропущенная функция TS заключается в том, что она позволяет использовать class
в качестве интерфейса, что позволяет работать с текущим ограничением.
export abstract class DataProvider {
getData(): number;
}
@singleton(DataProvider) // register with an alternative key
export class MyAwesomeDataProvider implements DataProvider {
}
@autoinject
export class DataConsumer {
constructor(dataProvider: DataProvider) {
}
}
В приведенном выше коде мы объявляем абстрактный класс DataProvider
, который гарантирует, что он не скомпилирован TS. Затем мы регистрируем MyAwesomeDataProvider
с альтернативой key
of DataProvider
, которая будет возвращать экземпляр MyAwesomeDataProvider
каждый раз, когда запрашивается a DataProvider
.
Что касается дочерних контейнеров, вы должны сделать container.createChild()
, который возвращает новый экземпляр контейнера, и до тех пор, пока разрешение запускается из этого дочернего контейнера, вы должны получить правильный экземпляр. Единственная проблема - использование декораторов с двумя конфликтующими клавишами. В принципе, метаданные живут на самом классе, поэтому вы не можете регистрировать два экземпляра в DataProvider
, что, несомненно, (из-за того, что я не пробовал самостоятельно) вызывать проблемы, единственный способ сделать это - использовать явная регистрация. Например.
export abstract class DataProvider {
getData(): number;
}
export class MyAwesomeDataProvider implements DataProvider {
}
export class MyMoreAwesomeDataProvider implements DataProvider {
}
child1 = container.createChild();
child1.registerSingleton(DataProvider, MyAwesomeDataProvider);
child2 = container.createChild();
child2.registerSingleton(DataProvider, MyMoreAwesomeDataProvider);
@autoinject
export class DataConsumer {
constructor(dataProvider: DataProvider) {
}
}
child1.get(DataConsumer); // injects MyAwesomeDataProvider
child2.get(DataConsumer); // injects MyMoreAwesomeDataProvider
Ответ 4
Любил идею Фрэнка Гамбино и нашел способ заставить его работать как с @inject
, так и с @autoinject
. Хитрость заключается в использовании специального параметра decorator (поскольку интерфейс зарезервирован в TypeScript, я назвал его @i).
Детали декоратора:
myClass.ts
import { autoinject } from 'aurelia-framework';
import { i } from './i.ts';
import { IFoo } from "./ifoo.ts";
@autoinject
export class MyClass {
constructor(@i(IFoo) foo: IFoo) {
foo.doSomething();
}
}
i.ts:
import "reflect-metadata";
/**
* Declare the interface type of a parameter.
*
* To understand more about how or why it works read here:
* https://www.typescriptlang.org/docs/handbook/decorators.html#metadata
*/
export function i(interfaceSymbol: symbol) {
return function (target: Object, parameterName: string | symbol, parameterIndex: number) {
var paramTypes = Reflect.getMetadata('design:paramtypes', target);
paramTypes[parameterIndex] = interfaceSymbol;
Reflect.defineMetadata('design:paramtypes', paramTypes, target);
}
}
Остальная часть в точности совпадает с ответом Фрэнка Гамбино, но я добавил его для полноты
...
ifoo.ts:
export interface IFoo {
doSomething(): void;
}
export const IFoo = Symbol("IFoo"); // naming the symbol isn't mandatory, but it easier to debug if something goes wrong
some.ts:
import { IFoo } from "./ifoo.ts";
export class Bar implements IFoo {
doSomething(): void {
console.log('it works!');
}
}
main.ts:
import { IFoo } from "./ifoo.ts";
import { Bar } from "./bar.ts";
...
container.registerInstance(IFoo, new Bar());
...
И он может работать с другими контейнерами DI. Чтобы заставить его работать с Angular2 (хотя почему бы вам? Aurelia гораздо более удивительна:) вам просто нужно изменить тип interfaceSymbol
в файле i.ts на any
и вместо Symobl("IFoo")
записать new InjectionToken("IFoo")
(класс InjectionToken - это Angular штука, и, к сожалению, они не поддерживают Symbol в качестве токена для инъекций, по крайней мере на данный момент).