Startseite / Artikel / Framework-unabhängige Frontend-DI mit InversifyJS und einem Composition Root

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.

3230 Wörter

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:

  1. 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.
  2. 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.ts kommt, in dem jede Dienstklasse manuell registriert werden muss.
  3. Lebenszyklusverwaltung. Singleton-Instanzen, vorübergehende Instanzen sowie scoped Auflösungen müssen erstklassig unterstützt werden.
  • Testbarkeit. Der Austausch einer Abhängigkeit durch ein Mock-Objekt sollte in nur einer Zeile möglich sein, ohne dass der Container neu kompiliert werden muss.
  • Einfluss auf das Bundle. Die Betriebskosten sollten gering sein – sowohl in der Rohform als auch nach Minifizierung und Komprimierung mit GZIP.
  • 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:

    1. 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.
    2. Manuelle Injektion funktioniert nur bei kleinen Projekten. In einem großen Unternehmens-Monorepo führt der Composition Root zu Tausenden von manuell geschriebenen Verbindungen.
  • InversifyJS gewann bei der Modularität. Es ist der einzige Kandidat, bei dem ein Modul (ContainerModule) eine Erstklassewert ist, den eine Domänenbibliothek als Einheit erstellen, kapseln und exportieren kann.
  • Die Build-Konfiguration ist wichtig. InversifyJS und TSyringe benötigen in 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.for und 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:

    1. Ein Name für Typ und Wert. TypeScript speichert Typen und Werte in getrennten Namensräumen, sodass AuthFacade sowohl 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 wie AuthFacadeToken erfinden.
  • 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.
  • Die Typumwandlung 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:

    1. Öffentliche API. Die index.ts-Datei der Bibliothek gibt lediglich authContainerModule erneut aus. Klassen wie LoginUseCase oder CoreAuthFacade sind von anderen Paketen aus einfach nicht erreichbar.
    2. Lint-Regeln. Die ESLint-Regel @nx/enforce-module-boundaries von Nx überprüft die Projekt-Tags; ein Projekt mit dem Tag type:core darf 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.
  • Wählen Sie zunächst einen Container für die Modulstruktur aus: Ein Domain, der ein versiegeltes ContainerModule exportiert, sorgt dafür, dass der Einstiegspunkt klein bleibt.
  • Verwenden Sie Symbol.for-Token vom Typ ServiceIdentifier<T>, damit sowohl duplizierte Pakete als auch die Typableitung weiterhin funktionieren.
  • Erfordern Sie Grenzen durch Exporte und Lint-Regeln – nicht nur durch Konventionen.
  • Lassen Sie genau eine Datei, die Composition Root, konkrete Implementierungen berühren; mehrplattformbasierte Builds werden dann zu einer Frage des Austauschs von Bindings.
  • Vergessen Sie nicht: Injektion ist nur das Werkzeug. Das Ziel ist die Inversion – wenn Sie React durch Vue, SQLite durch IndexedDB oder Axios durch Fetch ersetzen können, ohne die Domain-Schicht zu ändern, hat die Strategie ihre Aufgabe erfüllt.
  • Zusätzliche Literatur

  • Auswahl einer React-Folderverstruktur: Sieben Layouts und ihre Bruchpunkte — Vergleichen Sie flache, typbasierte, funktionenbasierte, Atomic Design-, DDD-, Feature-Sliced-Design- sowie Monorepo-Layouts für React und erfahren Sie, ab welcher Größenordnung jedes Layout nicht mehr funktioniert.
  • Umstellung von Angular HttpClient auf das Fetch-Backend mit withFetch() — Erfahren Sie, warum Angulars HttpClient von XMLHttpRequest zu fetch wechselt, wie withFetch() Edge SSR und Streaming ermöglicht und welche Auswirkungen dies auf Ihre Interceptor-Funktionen hat.
  • Wie Angular Dependency Injection Dienste mit inject() auflöst — Erkennen Sie, wie Angular DI Dienste über inject(), providers und hierarchische Injectoren bereitstellt sowie wie Sie Instanzen einschränken und Fehler aufgrund fehlender Provider vermeiden können.
  • SOLID als Frage zum Wandel: Refaktorisieren eines Spring Boot Services — Lernen Sie, jedes SOLID-Prinzip in einem Spring Boot Backend anzuwenden, indem Sie herausfinden, was sich wahrscheinlich ändern wird, und wie man die Überengineering-Probleme vermeidet, die SOLID oft mit sich bringt.