@zirion/ioc · Гайд по миграции
Миграция с v2 на v3
Обновление @zirion/ioc с v2 до v3: время жизни providers, типизация TypeScript, фабрики и lifecycle hooks.
Обновлено · · if-else.dev
Что меняется в первую очередь
Регистрация по-прежнему использует add(), inject и valueFactory; асинхронные get() и getOrFail() по-прежнему возвращают промисы. Главное несовместимое изменение: время жизни по умолчанию. Provider без явного scope становится transient в v3 вместо singleton в v2.
Сначала сохраните поведение приложения. Внедряйте transient и синхронное разрешение после того, как обновление заработает.
| Область | v2 | v3 |
|---|---|---|
| Scope без указания | SINGLETON |
TRANSIENT |
| Разрешение | Асинхронные get / getOrFail |
Асинхронный API и getSync / getOrFailSync |
| Типы фабричных значений | Ограниченный вывод | Вывод зарегистрированных selectors и результатов фабрик |
Повторы в inject |
Схлопываются | Порядок и повторения сохраняются |
Асинхронный onInitialized() |
Промис не ожидается | Промис ожидается |
| Falsy-результаты singleton | Могли создаваться повторно | Кешируются, включая null и undefined |
| Конкурентное кешируемое разрешение | Могло создать несколько значений | Разделяет выполняющуюся singleton/request-работу |
Циклический граф inject |
Нет ошибки цикла | CIRCULAR_DEPENDENCY |
| Финализация классов | Могла затронуть request-providers | Создаются только singleton-классы |
Обновите пакет и среду выполнения
Используйте Node.js 20 или новее в разработке, CI и production. Установите новую мажорную версию своим пакетным менеджером:
npm install @zirion/ioc@^3.0.0
# или: yarn add @zirion/ioc@^3.0.0
# или: pnpm add @zirion/ioc@^3.0.0
Запускайте проверку типов и тесты приложения после каждого шага. Пока не убирайте await из существующих вызовов разрешения.
Сохраните общие экземпляры
Проверьте каждую регистрацию без scope. Пулы БД, кеши, общее состояние, загрузчики настроек и сервисы со startup hooks часто требуют явного singleton.
До обновления, в v2:
import { Container } from '@zirion/ioc';
class Store {
users: string[] = [];
}
const container = new Container().add(Store);
const first = await container.getOrFail(Store);
const second = await container.getOrFail(Store);
console.log(first === second); // true в v2
После обновления, с сохранением поведения в v3:
import { Container, InjectScope } from '@zirion/ioc';
class Store {
users: string[] = [];
}
const container = new Container()
.add(Store, { scope: InjectScope.SINGLETON });
const first = await container.getOrFail(Store);
const second = await container.getOrFail(Store);
console.log(first === second); // true в v3
Без явного scope каждое разрешение создаёт отдельный Store; изменения одного экземпляра не видны в другом. Фабрика подключения к БД также выполняется при каждом transient-разрешении; для общего пула выберите singleton.
Сохраните явные регистрации REQUEST. Создавайте один context на запрос и передавайте этот объект во все разрешения запроса. Не переносите данные запроса в singleton. Transient может передать context request-зависимостям, но размещение под singleton не делает request-зависимость безопасной.
Сохраняйте тип, возвращённый add()
V3 знает зарегистрированные selectors и выводит результаты фабрик. Такой прежний паттерн меняет контейнер во время выполнения, но оставляет переменную с типом пустого контейнера:
const container = new Container();
container.add('port', { valueFactory: () => 3000 });
// container.getOrFail('port'); // Ошибка TypeScript в v3
Сохраняйте возвращённый тип:
const container = new Container()
.add('port', { valueFactory: () => 3000 });
const port = await container.getOrFail('port'); // number
При модульной регистрации возвращайте цепочку из каждого composition helper и сохраняйте результат. Не задавайте результату голый тип Container: так теряется информация о зарегистрированных selectors. Если другой функции нужен тип готового контейнера, используйте ReturnType<typeof createContainer>.
Типы не доказывают соответствие inject конструктору и наличие всех зависимостей. Сохраните интеграционные тесты, разрешающие главные сервисы.
Проверьте инициализацию и финализацию
Асинхронный onInitialized() теперь завершается до получения экземпляра. Ошибка hook отклоняет разрешение; следующий вызов может повторить создание. Если раньше hook запускал работу без ожидания, разрешение теперь может стать дольше или выявить ошибку запуска. Обязательную работу оставьте инициализацией; независимые фоновые задачи запускайте явно в коде приложения.
build() / finalize() создаёт только singleton-классы с onFinalized(). Явно задайте SINGLETON классам, которым нужен startup hook. У объектов-селекторов с onFinalized() hook по-прежнему вызывается. Request и transient-классы при сборке не создаются.
build() не разрешает все зарегистрированные графы и не закрывает ресурсы. Серверы, подключения и клиенты по-прежнему закрывайте при завершении приложения.
Проверьте фабрики и массивы inject
- Независимые асинхронные зависимости выполняются параллельно. Если фабрике B нужен результат A, зарегистрируйте A в
injectфабрики B; порядок регистраций не задаёт порядок инициализации. - Массив
injectкопируется приadd(). Подготовьте его до регистрации: последующие изменения больше не меняют граф. - Повторы остаются отдельными аргументами конструктора. Повторный singleton передаёт тот же объект дважды, transient создаёт два значения. Проверьте код, рассчитывавший на схлопывание повторов в v2.
- Обычная функция, возвращённая фабрикой, остаётся значением; возвращённый класс инстанцируется. При важности этого различия явно возвращайте нужный объект.
- Кешируемые фабрики сохраняют все успешные значения, включая
false,0,'',null,undefinedиNaN.getOrFail()проверяет регистрацию, а не отсутствиеnull. - Конкурентные singleton и request-разрешения разделяют одну выполняющуюся операцию. После ошибки следующий вызов повторит создание. Инициализация должна допускать повторную попытку.
- Циклический граф
injectтеперь бросаетCIRCULAR_DEPENDENCY. Выделите общую логику или разделите настройку и выполнение вместо скрытых обращений к контейнеру из фабрики.
Внедряйте синхронные вызовы осознанно
Новые синхронные методы необязательны. Переписывать существующие асинхронные вызовы не нужно.
const container = new Container()
.add('port', { valueFactory: () => 3000 });
const port = container.getOrFailSync('port');
console.log(port); // 3000, не Promise
Синхронное разрешение подходит только для графа, где все фабрики, зависимости и hooks инициализации синхронны. Иначе возникает ASYNC_RESOLUTION_REQUIRED. Вызов может запустить асинхронную фабрику до обнаружения промиса, поэтому это не безобидная проверка возможностей. Сохраните await для графов с возможной асинхронной инициализацией.
Проверьте поведение после обновления
- Проверьте идентичность singleton и общее состояние между отдельными разрешениями.
- Убедитесь, что один context переиспользует request-значения, а разные context изолированы.
- Проверьте свежие значения transient, для которых нужно независимое создание, включая повторные внедрения.
- Разрешите кешируемые providers конкурентно и проверьте один вызов фабрики.
- Проверьте ошибки фабрик и асинхронных hooks, затем успешную повторную попытку.
- Проверьте startup hooks явных singleton-классов и закрытие ресурсов при завершении.
- Запустите TypeScript-проверку и smoke test своего входа ESM или CommonJS.
Дальше: руководство v3 и запускаемые примеры. Руководство v2 доступно для сравнения.