На этой странице

@zirion/ioc · Гайд по миграции

Миграция с других контейнеров

Переход с Awilix, InversifyJS и TSyringe на @zirion/ioc v3: регистрации, время жизни providers и адаптация конструкторов.

Обновлено · ·

Начните с одной границы приложения

Выберите один обработчик, воркер или модуль для первого переноса. Перечислите зависимости, их время жизни, порядок инициализации и владельцев ресурсов. Сохраните старый контейнер для остального приложения, пока эта граница не заработает.

В @zirion/ioc один provider регистрируется под одним selector. Зависимости задаются аргументами конструктора в inject или массивом для valueFactory. Асинхронное разрешение использует await; getOrFailSync() подходит для полностью синхронного графа. По умолчанию используется transient, поэтому общие ресурсы нужно пометить явно.

Прежняя возможность Подход в @zirion/ioc
Константа или готовый экземпляр valueFactory: () => instance, обычно SINGLETON
Внедрение через конструктор add(Service, { inject: [Dependency] })
Декораторы и внедрение свойств Перенос в конструктор или адаптирующую фабрику
Общий экземпляр scope: InjectScope.SINGLETON
Экземпляр на запрос scope: InjectScope.REQUEST с явным объектом context
Новый экземпляр TRANSIENT по умолчанию
Несколько providers под одним token Отдельные selectors и фабрика массива
Закрытие ресурсов Код завершения приложения

Примеры ниже показывают независимые регистрации. Замените демонстрационные классы и значения аналогами из приложения.

Переход с Awilix

.singleton() соответствует SINGLETON, .transient() подходит scope по умолчанию, а .scoped() можно заменить REQUEST, если один context обозначает один запрос приложения. Конструктор в режиме Awilix PROXY получает cradle-объект; сохраните эту форму через адаптер:

import { Container, InjectScope } from '@zirion/ioc';

class Database {
  users = ['Ada'];
}

class UserService {
  constructor(private readonly cradle: { db: Database }) {}

  list() {
    return this.cradle.db.users;
  }
}

const container = new Container()
  .add('db', {
    scope: InjectScope.SINGLETON,
    valueFactory: () => new Database(),
  })
  .add(UserService, {
    inject: ['db'],
    scope: InjectScope.REQUEST,
    valueFactory: ([db]) => new UserService({ db }),
  });

const context = { requestId: 'req-1' };
console.log((await container.getOrFail(UserService, context)).list());

Новые сервисы могут принимать прямые аргументы вместо cradle. Для Awilix CLASSIC перенесите зависимости по именам параметров в явный inject. Request context заменяет использование createScope() для кеширования, но не создаёт дочерний контейнер и не позволяет переопределять регистрации. Для .disposer() и закрытия scopes Awilix понадобится явный код освобождения ресурсов.

Исходные режимы внедрения и время жизни описаны в документации Awilix.

Переход с InversifyJS

Сохраните tokens-классы, строки и symbols. Замените декораторы упорядоченными элементами inject. Здесь symbol обозначает интерфейс без runtime-метаданных:

import { Container, InjectScope } from '@zirion/ioc';

interface Mailer {
  send(message: string): void;
}

const MAILER = Symbol('MAILER');

class ConsoleMailer implements Mailer {
  send(message: string) {
    console.log(message);
  }
}

class Notifications {
  constructor(private readonly mailer: Mailer) {}

  welcome() {
    this.mailer.send('Welcome');
  }
}

const container = new Container()
  .add(MAILER, {
    scope: InjectScope.SINGLETON,
    valueFactory: () => new ConsoleMailer(),
  })
  .add(Notifications, { inject: [MAILER] });

container.getOrFailSync(Notifications).welcome();

inSingletonScope() соответствует SINGLETON, inTransientScope() соответствует TRANSIENT. Inversify inRequestScope() кеширует внутри одного дерева разрешения; @zirion/ioc REQUEST также разделяет отдельные разрешения с одним context. Выберите нужное время жизни до переноса.

Для getAsync() используйте await getOrFail(), для синхронного графа используйте getOrFailSync(). Именованные, tagged и множественные bindings требуют отдельных selectors или фабрики выбора; прямого аналога getAll() нет.

Исходное поведение scopes и разрешения: Inversify binding guide и Container API.

Переход с TSyringe

Замените @injectable() и @inject(TOKEN) явными регистрациями. Здесь CLOCK обозначает зависимость-интерфейс:

import { Container, InjectScope } from '@zirion/ioc';

interface Clock {
  now(): Date;
}

const CLOCK = Symbol('CLOCK');

class Report {
  constructor(private readonly clock: Clock) {}

  createdAt() {
    return this.clock.now().toISOString();
  }
}

const container = new Container()
  .add(CLOCK, {
    scope: InjectScope.SINGLETON,
    valueFactory: () => ({ now: () => new Date() }),
  })
  .add(Report, { inject: [CLOCK] });

console.log(container.getOrFailSync(Report).createdAt());

Lifecycle.Transient соответствует TRANSIENT, singleton-регистрации соответствуют SINGLETON. Для ContainerScoped подходит REQUEST, если приложение передаёт context для каждой границы, прежде владевшей дочерним контейнером. ResolutionScoped действует только в одной цепочке разрешения и не равен context, общему между отдельными вызовами.

Interceptors, @injectAll, переопределение дочерних регистраций и dispose() требуют адаптеров или остаются в прежнем контейнере на время переноса. Уберите reflect-metadata и настройки компиляции декораторов после переноса всех зависимых модулей.

Исходные lifecycle и disposal API описаны в документации TSyringe.

Соедините старый и новый контейнер

Заранее получите прежний сервис и зарегистрируйте тот же объект в новом контейнере. Так обе стороны используют одного владельца ресурса вместо случайного создания двух singleton:

import { Container, InjectScope } from '@zirion/ioc';

declare const legacy: {
  resolve(token: string): { send(message: string): void };
};

const existingMailer = legacy.resolve('mailer');
const container = new Container().add('mailer', {
  scope: InjectScope.SINGLETON,
  valueFactory: () => existingMailer,
});

const mailer = container.getOrFailSync('mailer');
mailer.send('Hello');

Для ленивого создания используйте valueFactory: () => legacy.resolve('mailer'). Если прежний поиск асинхронный, используйте await container.getOrFail('mailer'). Избегайте мостов в обе стороны, образующих скрытый цикл через обращения к контейнерам: проверка inject не видит эти вызовы.

Если старому API нужны синхронные экземпляры, а перенесённый граф асинхронный, получите сервис при запуске и передайте готовый экземпляр старому коду. Не пытайтесь блокировать промис синхронным accessor.

Явно замените multi-binding

Зарегистрируйте реализации под отдельными selectors и соберите группу самостоятельно:

const container = new Container()
  .add('email', { valueFactory: () => ({ name: 'email' }) })
  .add('sms', { valueFactory: () => ({ name: 'sms' }) })
  .add('channels', {
    inject: ['email', 'sms'],
    valueFactory: (channels) => channels,
  });

const channels = container.getOrFailSync('channels');
console.log(channels.map((channel: { name: string }) => channel.name));

На selector приходится одна регистрация, переопределения нет. Для теста или другой конфигурации создавайте новый контейнер.

Проверьте перенесённую границу

Получите перенесённую точку входа и проверьте идентичность singleton, request и transient. Проверьте асинхронную инициализацию и ошибки. Отдельно проверьте закрытие ресурсов: finalize() выполняет startup hooks и не заменяет disposal. Сохраните прежний контейнер до прохождения этих проверок.

Дальше: основное руководство и запускаемые примеры.

Наверх