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 в качестве токена для инъекций, по крайней мере на данный момент).