Framework-unabhängige Frontend-DI mit InversifyJS und einem Composition Root
Wie man einen TypeScript DI-Container auswählt, jedes Domain-Bereich als ContainerModule verpackt, alles in einer Composition Root verbindet und diese mit React, Vue und Angular verknüpft.
Große Frontend-Codebasen versagen selten aufgrund eines einzelnen fehlerhaften Komponenten; sie versagen, weil Geschäftsregeln stillschweigend mit React-Hooks, HTTP-Clientn und Speicher-APIs verflochten werden, bis nichts mehr eigenständig getestet oder wiederverwendet werden kann. Dependency Injection ist das Mechanismus auf niedrigem Niveau, der diese Regeln voneinander trennt – allerdings nur, wenn die Verbindungen absichtlich gestaltet werden. Diese Anleitung führt durch den Auswahl eines Containers für ein mehrdomäniges TypeScript-Monorepo, die Definition typsicherer Token, das Abschließen jeder Domäne hinter einem einzigen Modul, den Aufbau des Graphen in einer Composition Root sowie die Offenlegung gegenüber React, Vue und Angular durch Brücken, die jeweils aus etwa einem Dutzend Zeilen bestehen.
Warum die Isolierung bereits vor dem Modellieren der Domäne entschieden werden muss
Klassische Ratschläge des Domain-Driven Design besagen, dass Implementierungsdetails auf später verschoben werden sollten: Zuerst werden Modellentitäten und Anwendungsfallmodelle erstellt, die Tools werden anschließend ausgewählt. In der Praxis profitiert ein Team, das eine umfassende architektonische Veränderung einleitet, davon, bereits zu Beginn eine technische Frage zu klären – genauer gesagt, wie die Modulisolation tatsächlich durchgesetzt werden soll. Ohne eine vereinbarte Mechanismuslösung werden die ersten Domänen ad hoc miteinander verbunden, und die geltenden Konventionen können sich nie wieder etablieren.
Das zugrundeliegende Prinzip ist das Prinzip der Abhängigkeitsumkehr: Hochlevel-Politiken sollten von Abstraktionen abhängen, und konkrete Details sollten von denselben Abstraktionen abhängen. Abhängigkeitsinjektion ist die praktische Technik, die dieses Prinzip umsetzt. Klassen fordern abstrakte Token anstelle konkreter Implementierungen, und etwas außerhalb von ihnen entscheidet, worauf sich diese Token beziehen. In DDD und Clean Architecture ist dies keine optionalen Verfeinerungen; das Domänenmodell darf nicht wissen, mit welchem UI-Framework, HTTP-Client oder Persistenzlayer es arbeitet.
Die meisten Frontend-Teams greifen zuerst auf React Context (oder provide/inject in Vue) zurück. Das scheint ausreichend zu sein: Man deklariert einen Wert in der Wurzel und kann ihn überall darunter abrufen. Die Einschränkungen zeigen sich jedoch schnell. Es gibt keine Lebenszyklusverwaltung, kein Konzept von Factories oder Scopes, und jeder Verbraucher des Wertes ist nun mit dem React-Runtime verbunden, sodass die Logik ohne diesen nicht ausgeführt oder getestet werden kann. Der Weg von diesem Ausgangspunkt zu einer richtigen Composition Root wird im weiteren Verlauf dieses Leitfadens behandelt. Das Schreiben eines maßgeschneiderten Containers lohnt sich in der Regel selten, daher besteht die erste Aufgabe darin, die vorhandenen IoC-Optionen ehrlich zu bewerten.
Kriterien für einen Frontend-DI-Container
Backend-Frameworks wie NestJS, Spring und .NET bieten Dependency Injection als standardmäßig gelöste Funktion mit. Frontend-Code unterliegt zusätzlichen Einschränkungen: Laufzeitkosten, Bundle-Größenbeschränkungen sowie die Notwendigkeit, mit verschiedenen UI-Frameworks zu arbeiten. Fünf Kriterien bestimmen das Bewertungsziel:
- Framework-agnostizität. Der Container muss in reinem TypeScript laufen, ohne Laufzeitabhängigkeiten zu React, Vue oder Angular, und im Browser, in Node.js für SSR sowie in React Native funktionieren.
- Modularität. Ein Domänenbereich sollte in der Lage sein, ein vorkonfiguriertes Modul mit eingebauten Bindungen bereitzustellen, sodass die Anwendung dieses Modul nur lädt. Dadurch wird verhindert, dass es zu einem 500 Zeilen langen
main.tskommt, in dem jede Dienstklasse manuell registriert werden muss. - Lebenszyklusverwaltung. Singleton-Instanzen, vorübergehende Instanzen sowie scoped Auflösungen müssen erstklassig unterstützt werden.
Die Kandidaten
InversifyJS
InversifyJS ist seit Langem die Standardwahl für große TypeScript-Projekte und stellt den umfassendsten IoC-Container im Ökosystem dar. Er ist framework-unabhängig und deklariert Abhängigkeiten mithilfe von Decoratoren. Sein ContainerModule gruppiert Bindungen in logische Einheiten und unterstützt inSingletonScope, inTransientScope sowie inRequestScope, Untercontainer, das Laden und Entladen von Modulen zur Laufzeit sowie Aktivierungs-Hooks. Der Preis ist das Gewicht: Er war die am stärksten belastete Option, und es wäre irreführend, aktuelle Versionen als leicht zu bezeichnen.
TSyringe
TSyringe, das von Microsoft gepflegt wird, tauscht Breite gegen Bequemlichkeit ein. Es löst Konstruktorabhängigkeiten automatisch aus den TypeScript-Metadaten, bietet eine umfangreiche Sammlung an Dekoratoren und deckt die üblichen Lebenszyklen (Singleton, Transient, ResolutionScoped, ContainerScoped) ab. Für eine mittelgroße Anwendung stellt es eine attraktive, unkomplizierte Einrichtung dar. Zwei Aspekte sprechen in einer Mehrdomänenarchitektur dagegen: Es gibt kein Konzept erstklassiger Module, sodass die Komposition manuell mit Registrierungsfunktionen oder createChildContainer() erstellt werden muss, und es ist auf den reflect-metadata-Polyfill angewiesen, was einen erheblichen Zusatzgewicht verursacht.
Awilix
Awilix vermeidet Dekoratoren völlig. Es nutzt die ES6 Proxy API, um Abhängigkeiten mit den Namen der Konstruktorparameter oder mit Schlüsseln des Argument-Objekts einer Factory abzustimmen. Dadurch eignet es sich ideal für Teams, die keine Dekoratoren verwenden möchten, und es war der kleinste eigenständige Container, der bewertet wurde. Es unterstützt Lebenszyklen von SINGLETON, SCOPED und TRANSIENT; Module werden mithilfe von benutzerdefinierten Registrierungsfunktionen organisiert.
Eine frontend-spezifische Fallstrick verdient Aufmerksamkeit. Im CLASSIC-Injection-Modus liest Awilix die Namen der Konstruktorargumente als Strings, und Minifier-Tools ändern diese Argumentnamen um, wodurch die Auflösung in der Produktion fehlschlägt. Nur der Standardmodus PROXY übersteht aggressive Umbenennungen.
Angulars eingebauter Injector
Angular verfügt über eines der leistungsstärksten hierarchischen DI-Systeme in der Frontend-Entwicklung, mit tokenbasiertem Einbinden, begrenzten Providern sowie integriertem Lazy Loading. Wenn es ohne den Angular-Runtime verwendet werden könnte, wäre es ein ernsthafter Konkurrent für die Domain-Schicht. Das ist jedoch nicht möglich – und das ist das Problem.
React Context
Context ist eigentlich kein DI-Container; es handelt sich dabei um eine Methode, um das Prop-Drilling zu vermeiden. Ein Provider befindet sich in der Nähe des Wurzels, und Komponenten lesen ihn mit useContext(). Jede Änderung des bereitgestellten Wertes führt dazu, dass alle Verbraucher neu gerendert werden, und es gibt weder ein Lebenszyklusmanagement noch Factory-Bindings oder Scope-Isolation. Sein einziger wirklicher Vorteil ist, dass er keinen zusätzlichen Platz im Bundle beansprucht.
Vue provide und inject
Vues Mechanismus leitet Werte den Komponentenbaum entlang und teilt die architektonischen Einschränkungen des Contexts: Er ist keine praktische Grundlage für ein Netzwerk aus Domänen-Diensten. Dennoch bietet er einen ergonomischen Vorteil, da Abhängigkeiten global über ein Plugin registriert werden können, anstatt den Baum in verschachtelte Provider einzuhüllen. Angulos moderne inject()-Funktion stützt sich auf eine ähnliche Idee eines aktiven Injektionskontexts.
Einfache Konstruktorinjektion
Der Ansatz ohne zusätzliche Bibliotheken leitet jede Abhängigkeit explizit über Konstruktoren weiter. Er ist vollständig typsicher und verursacht keinen Laufzeit-Overhead. Der Nachteil liegt in der Skalierbarkeit: Sobald eine Anwendung etwa 10 bis 15 Domänen-Dienste hat, wird das Manuell-Verschalten des Netzwerks am Eingangspunkt zu einer großen, brüchigen Datei, die niemand mehr anfassen möchte.
Was der Vergleich zeigt
Die Größen der Bündel wurden ermittelt, indem ein Eingabefail erstellt wurde, das die öffentliche API jedes Pakets importiert, mit esbuild --bundle --minify --platform=browser gebündelt und das Ergebnis mit gzip -9 komprimiert wurde, wodurch die eigenen Overhead-Kosten des Containers vom Anwendungscode getrennt werden. Daraus ergaben sich vier Schlussfolgerungen:
- Framework-DI gehört zur UI und nicht zum Domain-Bereich. Die Verwendung von
useContext()oder Angulars@Injectable()in Domain-Bibliotheken verbindet Geschäftsregeln mit einem bestimmten Framework und erfüllt somit nicht das erste Kriterium. - Manuelle Injektion funktioniert nur bei kleinen Projekten. In einem großen Unternehmens-Monorepo führt der Composition Root zu Tausenden von manuell geschriebenen Verbindungen.
ContainerModule) eine Erstklassewert ist, den eine Domänenbibliothek als Einheit erstellen, kapseln und exportieren kann.tsconfig.json die Einstellungen experimentalDecorators: true, emitDecoratorMetadata: true sowie target: ES2022. InversifyJS v8 benötigt keinen separaten reflect-metadata-Polyfill mehr. Bei mit esbuild- oder SWC-basierten Werkzeugen (Vite, Next.js, Bun) muss überprüft werden, ob der Transformierungsprozess veraltete Dekoratormetadaten unterstützt – in der Regel über ein Plugin, da esbuild selbst keine Dekoratormetadaten ausgibt.Vier Regeln, die verhindern, dass der Container zu einem Service Locator wird
Ein Container für sich allein isoliert nichts; wird unvorsichtig verwendet, wird er zu einer globalen Sammlung von Diensten, auf die jede Datei zugreifen kann. Die Architektur stützt sich daher auf vier Konventionen:
- Absstrakte DI-Token, die mit
Symbol.forund einer$-Eigenschaft erstellt werden. - Jede Domänenbibliothek exportiert nur ihr Container-Modul, niemals ihre konkreten Klassen.
- Eine einzige Composition Root erstellt das Anwendungsgraphen.
- Ein dünner Brückenelement pro UI-Framework stellt den Container den Komponenten zur Verfügung.
Typsichere Token mit Symbol.for und $
Ein Container benötigt Laufzeit-Identifikatoren, um Abstraktionen auf Implementierungen abzubilden. TypeScript-Interfaces verschwinden zur Kompilierzeit, sodass sie diese Rolle nicht direkt übernehmen können. Das hier verwendete Muster verknüpft jedes Interface in einem @my-app/*-contracts-Paket mit einer Konstante desselben Namens, deren $-Eigenschaft einen Symbol enthält.
// libs/auth/contracts/src/lib/interfaces/auth.facade.ts
import type { ServiceIdentifier } from 'inversify';
export interface AuthFacade {
getAccessToken(): Promise<string | null>;
login(): Promise<void>;
logout(): Promise<void>;
}
export const AuthFacade = {
$: Symbol.for('AuthFacade') as ServiceIdentifier<AuthFacade>,
};
// libs/auth/contracts/src/lib/interfaces/pin.facade.ts
export interface PinFacade {
setupPin(pin: string): Promise<void>;
verifyPin(sub: string, pin: string): Promise<PinVerifyResult>;
changePin(sub: string, currentPin: string, newPin: string): Promise<PinVerifyResult>;
}
export const PinFacade = {
$: Symbol.for('PinFacade') as ServiceIdentifier<PinFacade>,
};
Dieser Codeausschnitt beinhaltet drei Entscheidungen:
- Ein Name für Typ und Wert. TypeScript speichert Typen und Werte in getrennten Namensräumen, sodass
AuthFacadesowohl das Interface als auch den Token-Träger sein kann. Der Compiler wählt aus dem Kontext die richtige Bedeutung aus, und niemand muss Namen wieAuthFacadeTokenerfinden.
Symbol.for anstelle von Symbol(). Jeder Aufruf von Symbol() gibt einen völlig neuen Wert zurück, während Symbol.for() nach dem Schlüssel im globalen Symbolregister der Laufzeit sucht. Wenn ein Packages aus einem Kontrakt in zwei Bundles dupliziert wird – was in einem Monorepo oder mit Module Federation vorkommen kann – erzeugen beide Kopien weiterhin denselben Token, und die Abfragen funktionieren weiterhin, anstatt mysteriös zu versagen. Der Nachteil ist, dass Registry-Schlüssel globale Zeichenketten sind und daher in der gesamten Anwendung eindeutig sein sollten.ServiceIdentifier<AuthFacade>. Durch das Angben von $ als Inversifys ServiceIdentifier<T> wird der Token bereits zur Kompilierzeit mit seiner Schnittstelle verknüpft, sodass die Auflösungssysteme den richtigen Typ ohne explizite Generics ableiten können.Der folgende Auszug zeigt beide Seiten: eine Klasse, die die Fassade durch Konstruktorinjektion erhält, sowie einen direkten container.get-Aufruf, dessen Rückgabetyp aus dem Token abgeleitet wird.
import { inject, injectable } from 'inversify';
import { AuthFacade } from '@my-app/auth-contracts';
@injectable()
export class LoginPageComponent {
// Strongly-typed injection via token
constructor(@inject(AuthFacade.$) private readonly auth: AuthFacade) {}
}
// Resolution site automatically infers return type as AuthFacade
const facade = container.get(AuthFacade.$); // AuthFacade
Der Verbraucher bezieht sich nur auf den Vertrag. Er hat keine Ahnung, was sich innerhalb von @my-app/auth-core befindet – und genau das ist der Sinn.
Ein Domänenbereich, ein ContainerModule
Jede Kernbibliothek eines Domänenbereichs stellt nur eine Sache bereit: ihr DI-Modul. Anwendungsfall, Entitäten, Ports und Repositorien bleiben privat. Innerhalb des Moduls sind interne Anwendungsfälle an sich selbst gebunden, während die öffentliche Fassade an ihr Vertrags-Token gebunden ist.
// libs/auth/core/src/lib/auth-container.module.ts
import { ContainerModule, type ContainerModuleLoadOptions } from 'inversify';
import { AuthFacade } from '@my-app/auth-contracts';
import { CoreAuthFacade } from './facades/core-auth.facade';
import { LoginUseCase } from './use-cases/login.use-case';
import { LogoutUseCase } from './use-cases/logout.use-case';
import { GetAccessTokenUseCase } from './use-cases/get-access-token.use-case';
export const authContainerModule = new ContainerModule((options: ContainerModuleLoadOptions) => {
// Private use cases (registered to self within domain)
options.bind(LoginUseCase).toSelf().inSingletonScope();
options.bind(GetAccessTokenUseCase).toSelf().inSingletonScope();
options.bind(LogoutUseCase).toSelf().inSingletonScope();
// Public contract implementation bound to token
options.bind(AuthFacade.$).to(CoreAuthFacade).inSingletonScope();
});
// libs/auth/core/src/index.ts - ONLY the ContainerModule is re-exported!
export * from './lib/auth-container.module';
Die Kapselung wird zweimal durchgesetzt:
- Öffentliche API. Die
index.ts-Datei der Bibliothek gibt lediglichauthContainerModuleerneut aus. Klassen wieLoginUseCaseoderCoreAuthFacadesind von anderen Paketen aus einfach nicht erreichbar. - Lint-Regeln. Die ESLint-Regel
@nx/enforce-module-boundariesvon Nx überprüft die Projekt-Tags; ein Projekt mit dem Tagtype:coredarf beispielsweise kein Projekt mit demselben Tag aus einem anderen Bereich importieren. Weitere Informationen zur Deklaration von Tags und Einschränkungen finden Sie in der Dokumentation zu den Nx-Modulgrenzen.
Die Exportgrenze verhindert versehentliche Importe; die Lint-Regel stoppt absichtliche Umwege über komplexe Pfade. Gemeinsam sorgen sie dafür, dass die Architektur von CI-Systemen überprüft wird und nicht nur von den Überprüfern im Gedächtnis behalten werden muss.
Der Composition Root
Die Composition Root ist der einzige Ort, an dem das Abhängigkeitsdiagramm erstellt wird – typischerweise apps/my-app/src/composition-root.ts oder eine Funktion, die von main.ts aufgerufen wird. Es gilt eine Regel dafür:
Die Composition Root ist der einzige Ort im Codebase, an dem konkrete Implementierungen, Infrastrukturadapter sowie Kernmodule des Domänenbereichs importiert werden dürfen.
Die untenstehende Funktion bindet zunächst die globale Infrastruktur (einen auf Axios basierenden HTTP-Client sowie einen Logger, der über eine Factory erstellt wird, damit er konfigurierte Transporte empfangen kann), und lädt anschließend die Domänenmodule.
// apps/my-app/src/composition-root.ts
import { Container } from 'inversify';
import { HttpClientPort, LoggerPort } from '@my-app/shared-contracts';
import { AxiosHttpClient, BrowserLogger, ConsoleLogTransport, FileLogTransport } from '@my-app/shared-infrastructure';
import { authContainerModule } from '@my-app/auth-core';
import { paymentsContainerModule } from '@my-app/payments-core';
export const initAppContainer = (): Container => {
const container = new Container();
// 1. Bind global infrastructure adapters
container.bind(HttpClientPort.$).to(AxiosHttpClient).inSingletonScope();
container.bind(LoggerPort.$).toDynamicValue(() => new BrowserLogger({
transports: [
new ConsoleLogTransport(),
new FileLogTransport(),
],
})).inSingletonScope();
// 2. Load domain modules
container.load(authContainerModule);
container.load(paymentsContainerModule);
return container;
};
Zur Vollständigkeit: Der BrowserLogger-Adapter ist eine gewöhnliche Klasse, die LoggerPort implementiert und jede Nachricht an ihre jeweiligen Transporte weiterleitet. Beachten Sie, dass er keine Dekoratoren enthält; da er innerhalb von toDynamicValue erstellt wird, muss der Container seinen Konstruktor niemals überprüfen.
// libs/shared/infrastructure/src/lib/logging/browser-logger.ts
import type { LoggerPort } from '@my-app/shared-contracts';
import type { LogTransport } from './interfaces/log-transport.interface';
export interface BrowserLoggerOptions {
transports: LogTransport[];
}
export class BrowserLogger implements LoggerPort {
constructor(private readonly options: BrowserLoggerOptions) {}
info(message: string, ...args: unknown[]): void {
this.options.transports.forEach(t => t.write('INFO', message, args));
}
}
Mehrere Anwendungen aus einem Domain-Kern
Weil die Infrastruktur im Composition Root ausgewählt wird, können verschiedene Anwendungen dieselben Domain-Pakete mit unterschiedlichen Adaptern sowie einer anderen Auswahl an Modulen wiederverwenden. Eine React Native-Anwendung könnte eine durch AsyncStorage unterstützte Speicherung nutzen, während eine Web-Admin-Anwendung IndexedDB verwendet und stattdessen ein Audit-Domain-Laden statt eines Zahlungs-Moduls.
// apps/mobile/src/composition-root.ts — React Native target
container.bind(StoragePort.$).to(AsyncStorageAdapter).inSingletonScope();
container.bind(LoggerPort.$).to(RnLogger).inSingletonScope();
container.load(authContainerModule);
container.load(paymentsContainerModule);
// apps/admin/src/composition-root.ts - Web Admin target
container.bind(StoragePort.$).to(IndexedDbAdapter).inSingletonScope();
container.bind(LoggerPort.$).to(BrowserLogger).inSingletonScope();
container.load(authContainerModule);
container.load(auditContainerModule); // Payments domain excluded entirely
@my-app/auth-core ist byte für byte identisch in den mobilen, administrativen und Desktop-Versionen. Es erfährt niemals, ob die Speicherung über AsyncStorage oder IndexedDB erfolgt.
Erlaubnis für einen Domain, eine andere zu verwenden
Nehmen wir an, payments-core benötigt ein Zugriffstoken, das von auth-core verwaltet wird. Die Importierung von @my-app/auth-core ist aufgrund der Grenzregeln verboten. Stattdessen verlässt sich der Zahlungsfall auf das Vertrags-Token der Authentifizierungsdomain, das sich im contracts-Paket befindet und erlaubt ist.
// libs/payments/core/src/lib/use-cases/create-payment.use-case.ts
import { inject, injectable } from 'inversify';
// Import from contracts, NOT core — permitted by boundary rules
import { AuthFacade } from '@my-app/auth-contracts';
import { PaymentsGatewayPort } from '../ports/payments-gateway.port';
@injectable()
export class CreatePaymentUseCase {
constructor(
@inject(AuthFacade.$) private readonly auth: AuthFacade,
@inject(PaymentsGatewayPort.$) private readonly gateway: PaymentsGatewayPort,
) {}
async execute(amount: number): Promise<void> {
const token = await this.auth.getAccessToken();
return this.gateway.processPayment(amount, token);
}
}
paymentsContainerModule bindet AuthFacade.$ nicht; es verwendet ihn lediglich. Die Bindung erfolgt, wenn der Composition Root beide Module in denselben Container lädt. Die praktische Folge ist: Wenn eine Anwendung Zahlungen ohne Authentifizierung lädt, scheitert die Auflösung zur Laufzeit. Deshalb lohnt es sich, für jedes Anwendungsziel einen Smoke-Test durchzuführen, bei dem jeder öffentliche Token einmal aufgelöst wird.
Vermittlung zwischen Container und UI-Frameworks
Da der Container nichts über Bibliotheken für die Benutzeroberfläche weiß, erhält jedes Framework einen kleinen Adapter mit etwa 10 bis 15 Zeilen Code.
React
Der Container wird beim Start einmal erstellt und nie ersetzt, sodass sich der Kontextwert niemals ändert. Dadurch wird die mit Context üblicherweise verbundene Wiederrendern-Kaskade umgangen: Die Nutzer erhalten einen stabilen Referenzwert. useInjection merkt sich die Abfrage pro Container und Token ab und wirft einen klaren Fehler, wenn der Anbieter fehlt.
// libs/shared/react-di/src/lib/di-context.tsx
import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
import type { Container, ServiceIdentifier } from 'inversify';
export interface DIProviderProps {
container: Container;
children: ReactNode;
}
const DIContext = createContext<Container | null>(null);
export const DIProvider = ({ container, children }: DIProviderProps) => (
<DIContext.Provider value={container}>
{children}
</DIContext.Provider>
);
export const useInjection = <T,>(token: ServiceIdentifier<T>): T => {
const container = useContext(DIContext);
return useMemo(() => {
if (!container) {
throw new Error('useInjection must be used within a DIProvider');
}
return container.get(token);
}, [container, token]);
};
Vue
Durch Vue’s Plugin-System sowie einen typisierten InjectionKey wird der Adapter noch kürzer. Das Plugin stellt den Container auf Anwendungsebene bereit, und ein Composable löst daraus Tokens auf.
// libs/shared/vue-di/src/lib/di-plugin.ts
import type { App, InjectionKey } from 'vue';
import { inject } from 'vue';
import type { Container, ServiceIdentifier } from 'inversify';
export const DI_CONTAINER: InjectionKey<Container> = Symbol('DI_CONTAINER');
export const diPlugin = {
install(app: App, { container }: { container: Container }) {
app.provide(DI_CONTAINER, container);
},
};
export const useInjection = <T>(token: ServiceIdentifier<T>): T => {
const container = inject(DI_CONTAINER);
if (!container) {
throw new Error('diPlugin is not installed');
}
return container.get(token);
};
Angular
Angels eigener hierarchischer Injector kann über Factory-Anbieter auf den Domain-Container verweisen. Durch die Übermittlung des Containers über einen InjectionToken sowie die Erstellung eines neuen Containers bei jedem Bootstrap wird verhindert, dass gleichzeitige serverseitig gerenderte Anfragen denselben Zustand teilen.
// apps/angular-app/src/app/domain.providers.ts
import { InjectionToken, type Provider } from '@angular/core';
import type { Container } from 'inversify';
import { AuthFacade } from '@my-app/auth-contracts';
import { initAppContainer } from './composition-root';
export const DI_CONTAINER = new InjectionToken<Container>('DI_CONTAINER');
export const AUTH_FACADE = new InjectionToken<AuthFacade>('AUTH_FACADE');
export const provideDomainContainer = (container: Container): Provider[] => [
{
provide: DI_CONTAINER,
useValue: container,
},
{
provide: AUTH_FACADE,
useFactory: (c: Container) => c.get(AuthFacade.$),
deps: [DI_CONTAINER],
},
];
// main.ts - fresh container instance created per bootstrap
bootstrapApplication(AppComponent, {
providers: [...provideDomainContainer(initAppContainer())],
});
Jeder zusätzliche Angular-Komponenten-Fassade benötigt seinen eigenen InjectionToken sowie einen Factory-Provider, wodurch die Liste mit der Verbreitung der öffentlichen API anwächst; die Erstellung aus einem kleinen Mapping ist eine Option, sobald die Liste sehr lang wird.
Kosten und wann man darauf verzichten sollte
Die Einrichtung fügt etwa 21 KB im Gzip-Format hinzu und erfordert emitDecoratorMetadata während des Builds. In einem Unternehmensmonorepo mit zehn oder mehr Domänen wird dieser Aufwand schnell wieder ausgeglichen. Bei einer Anwendung mit zwei oder drei Bildschirmen ist die Isolierung ein unnötiger Overhead; einfache Konstruktorinjektion oder sogar Singleton-Einträge auf Modulebene sind besser geeignet.
Haupterkenntnisse
- Bewahren Sie die DI-Mechanismen des Frameworks (Context, provide/inject, Angular Providers) am Rand der Benutzeroberfläche auf; der Kern der Domäne sollte nur von Vertrags-Token abhängen.
ContainerModule exportiert, sorgt dafür, dass der Einstiegspunkt klein bleibt.Symbol.for-Token vom Typ ServiceIdentifier<T>, damit sowohl duplizierte Pakete als auch die Typableitung weiterhin funktionieren.Zusätzliche Literatur
- Behebung der React-Prop-Überlastung mit Composition und Slots — Erfahren Sie, warum konfigurationsintensive React-Props zu Wartungsaufwänden führen, und wie Inversion of Control, Composition sowie Slots wirklich wiederverwendbare Komponenten ermöglichen.
- Einbinden von MCP-Tools in eine React-Chat-Oberfläche mit integrierter menschlicher Freigabe — Erfahren Sie, wie das Model Context Protocol in eine React-Anwendung passt: warum der Backend-Server MCP hosten sollte, wie ein Tool-Server funktioniert und wie Tool-Aufrufe in der Benutzeroberfläche gestreamt und freigegeben werden können.