Dependency Injection
Service registration
Services are registered during application initialization through the AppShell.configureServices method. Each service is associated with a constructor key (typically a class) and a factory function that creates the service instance.
Factory functions receive a ReadableGlobalContext as a parameter, allowing services to depend on other services.
Ways to declare a service
You can register services in three ways:
- Using a class constructor and factory function
- Using a
ServiceKeyand factory function - Using an
AsyncServiceKeyand async factory function
export class LoggerService {
public logMessage(message: string) {
// ...
}
}
export class AppConfig implements AppShell {
public configureServices(ctx: WritableGlobalContext) {
// Use the class itself as the key
ctx.registerService(LoggerService, () => new LoggerService());
}
}import {ServiceKey} from "vue-mvvm";
// For values/services that are not classes, create an explicit key
export const ApiBaseUrl = new ServiceKey<string>("ApiBaseUrl");
export class AppConfig implements AppShell {
public configureServices(ctx: WritableGlobalContext) {
ctx.registerService(ApiBaseUrl, () => "https://api.example.com");
}
}import {AsyncServiceKey} from "vue-mvvm";
export interface UserProfile {
id: string;
name: string
}
export const CurrentUserProfile = new AsyncServiceKey<UserProfile>("CurrentUserProfile");
export class AppConfig implements AppShell {
public configureServices(ctx: WritableGlobalContext) {
ctx.registerService(CurrentUserProfile, async () => {
const res = await fetch("/api/me");
if (!res.ok) throw new Error("Failed to load user profile");
return await res.json() as UserProfile;
});
}
}Example registration
export class LoggerService {
public logMessage(message: string) {
// Implementation ...
}
}import {LoggerService} from "@services/logger.service";
export class AppConfig implements AppShell {
public configureServices(ctx: WritableGlobalContext) {
ctx.registerService(LoggerService, () => new LoggerService());
}
}A service can only be registered once with the same key.
Lazy instantiation
Services are retrieved using the getService method, which implements lazy instantiation with singleton semantics.
The getService function follows these steps:
- Check instance cache: First checks for an existing instance.
- Lookup factory: If no instance exists, retrieves the factory function.
- Instantiate: Calls the factory function with a
ReadableGlobalContextto create the instance - Validate: Throws
InvalidServiceInstanceErrorif the factory returns a falsy value - Cache: Stores the instance in the cache
This lazy instantiation pattern ensures services are only created when first requested, improving application startup performance.
Service Mocking
The mockService method enables replacing service implementations with test doubles, facilitating unit testing of ViewModels without real service dependencies.
import {LoggerService} from "@services/logger.service";
import {LoggerServiceMock} from "@mocks/logger.service";
export class AppConfig implements AppShell {
public configureServices(ctx: WritableGlobalContext) {
ctx.registerService(LoggerService, () => new LoggerService());
if (import.meta.env.VITE_TEST) {
ctx.mockService(LoggerService, () => new LoggerServiceMock());
}
}
}Composability & Isolated Contexts
By default, vue-mvvm uses a single global DI context. However, for advanced scenarios or unit testing, you can create isolated DI contexts using the DIContainer class.
This allows you to instantiate multiple independent DI containers, which is particularly useful when running tests in parallel or when you need to ensure that tests do not leak state between each other.
Using DIContainer
The DIContainer class implements the WritableGlobalContext interface and provides all the methods available in the global context.
import { DIContainer, ServiceKey } from "vue-mvvm";
// Create an isolated container
const container = new DIContainer();
class MyService {
constructor(public value: string) {}
}
const MyKey = new ServiceKey<MyService>("MyService");
// Register services in the isolated container
container.registerService(MyKey, () => new MyService("isolated"));
// Retrieve services from the isolated container
const service = container.getService(MyKey);
console.log(service.value); // "isolated"Unit Testing with Isolated Contexts
When testing ViewModels or other components that depend on services, you can create a fresh DIContainer for each test case to ensure complete isolation.