Контейнер зависимостей
@vue-modeler/di — это контейнер зависимостей на основе shared composable.
Контейнер решает проблему управления жизненным циклом моделей и сервисов:
- Упрощает совместное использование моделей и сервисов между компонентами
- Отделяет бизнес-логику от представления
- Позволяет реализовать принципы MVVM, DDD, SOLID
- Может использоваться как service locator через
dc.resolve()внеsetup
Основные возможности
- ⚡ Ленивая загрузка: создает зависимости только когда они нужны
- 🗑️ Автоматическое удаление: удаляет неиспользуемые зависимости
- 🔧 Поддержка destructor: автоматически вызывает метод
destructorпри очистке - 💾 Постоянные экземпляры: позволяет создавать долгоживущие сервисы
- 🧭 Service locator: разрешает зависимости во время выполнения через контейнер
TIP
Контейнер зависимостей хранит зависимости, НО не поддерживает автоматического внедрения (autowire). Разработчик самостоятельно внедряет зависимости в отдельном модуле или слое.
Как это работает?
Контейнер работает по принципу "создай по требованию, удали когда не нужно":
- Регистрация фабрики — вы регистрируете фабрику для создания экземпляра, получаете shared composable
- Создание экземпляра — экземпляр создается только при первом обращении
- Переиспользование — при повторных обращениях возвращается существующий экземпляр
- Отслеживание ссылок — контейнер считает, сколько компонентов используют экземпляр
- Очистка — когда счетчик использования становится 0, экземпляр удаляется
Регистрация фабрики
provider регистрирует фабрику зависимости и создает shared composable, который будет использоваться в компонентах.
Фабрика — простая функция, которая может возвращать любое синхронное значение, кроме null, undefined или Promise (см. Фабрики должны быть синхронными).
Контейнер хранит то, что вернула фабрика. Никаких дополнительных действий не производит. Зависимости не внедряет.
import { provider } from '@vue-modeler/di';
const useDependency = provider(() => {
// ваша фабрика по созданию экземпляра
return {
// экземпляр с методами и данными
};
});
// так тоже можно
const useSymbol = provider(() => new Symbol('dependency'));
const useNumber = provider(() => 10);
const useTrue = provider(() => true);
// передаем зависимости в конструктор
const useObject = provider(() => new SomeModel(
useDependency(),
useSymbol(),
useNumber(),
useTrue()
));Использование в компонентах
Пример использования провайдера внутри шаблона компонента:
<template>
<div>{{ model.state }}</div>
</template>
<script setup lang="ts">
import { useDependency } from '@/providers/myDependency';
const model = useObject(); // получаем экземпляр
</script>Постоянные экземпляры
Бывают случаи, когда нужно создать экземпляр, который будет оставаться в памяти приложения после использования. Например, сервисы уровня приложения, кэши или менеджеры состояния.
Для этого нужно передать опцию persistentInstance: true в функцию provider.
const usePersistentService = provider(
() => new MyService(),
{ persistentInstance: true } // дополнительные опции
);Основные особенности постоянных экземпляров:
- Сохраняются в контейнере даже после освобождения всей области видимости
- Сохраняют своё состояние между перезагрузками компонентов
- Вложенные провайдеры становятся постоянными автоматически, если находятся внутри постоянного провайдера
- Полезны для сервисов уровня приложения, кэшей и менеджеров состояния
Например, вот как выглядит использование вложенных провайдеров:
// вложенный провайдер становится постоянным вместе с основным
const useNestedService = provider(() => new NestedService());
const usePersistentService = provider(
() => new MainService(useNestedService()),
{ persistentInstance: true }
);WARNING
На клиенте используйте постоянные экземпляры осторожно, поскольку они не будут удаляться автоматически.
Для SSR постоянные экземпляры безопасны, так как при каждом запросе создается новый экземпляр контейнера, а старый удаляется вместе с содержимым.
Доступ к контейнеру внутри фабрики
Фабрика получает объект, в свойстве dc которого лежит активный контейнер. Используйте его, когда экземпляру нужно разрешать другие зависимости во время выполнения (вне setup):
import { provider, type DependencyContainer } from '@vue-modeler/di';
const useApi = provider(({ dc }) => new ApiClient(dc));dc — это публичный DependencyContainer. Вложенные провайдеры, вызванные синхронно внутри фабрики, автоматически наследуют тот же контейнер.
Разрешение зависимостей вне setup
useDependency() можно вызывать только синхронно в setup компонента или внутри фабрики другого провайдера. Для кода времени выполнения — обработчиков событий, watch, кода после await, тестов или SSR — разрешайте зависимость через ссылку на контейнер:
const model = dc.resolve(useDependency);resolve() возвращает (или создает) экземпляр в этом контейнере. В отличие от useDependency() в setup, он не привязывает экземпляр к области видимости Vue, поэтому экземпляр живёт столько же, сколько и контейнер.
Переопределение фабрики
У каждого провайдера есть метод redefine(factory), который подменяет фабрику до первого разрешения — удобно для тестов и SSR-моков. Замена получает тот же аргумент { dc } плюс prevFactory, поэтому можно обернуть предыдущую реализацию:
const useService = provider(({ dc }) => new RealService(dc));
useService.redefine(({ dc, prevFactory }) => {
const previous = prevFactory?.({ dc });
return new MockService(previous);
});Переопределение после того, как экземпляр уже создан в целевом контейнере, выбросит ошибку Provider was redefined after instance creation. Разрешайте моки из отдельного контейнера, чтобы держать их изолированными.
Фабрики должны быть синхронными
Фабрика должна отрабатывать синхронно и возвращать экземпляр напрямую — никогда не Promise. async-фабрики, await в теле фабрики или возврат Promise не поддерживаются и выбрасывают ошибку при регистрации. Асинхронную работу выполняйте на созданном экземпляре, а не в фабрике.
// Так нельзя
const useModel = provider(async ({ dc }) => new MyModel(dc));
// Правильно — асинхронная работа живёт на экземпляре
const useModel = provider(({ dc }) => new MyModel(dc));