@zirion/ioc · Гайд по миграции
Миграция с других контейнеров
Переход с Awilix, InversifyJS и TSyringe на @zirion/ioc v3: регистрации, время жизни providers и адаптация конструкторов.
Обновлено · · if-else.dev
Начните с одной границы приложения
Выберите один обработчик, воркер или модуль для первого переноса. Перечислите зависимости, их время жизни, порядок инициализации и владельцев ресурсов. Сохраните старый контейнер для остального приложения, пока эта граница не заработает.
В @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. Сохраните прежний контейнер до прохождения этих проверок.
Дальше: основное руководство и запускаемые примеры.