DI frontend niezależny od frameworka z InversifyJS i korzeniem kompozycji
Jak wybrać kontener DI dla TypeScript, zapakować każdą domenę jako ContainerModule, połączyć wszystko w jednym Composition Root i stworzyć most łączący go z React, Vue oraz Angular.
Złożone bazy kodu frontendu rzadko zawodzą z powodu jednego uszkodzonego komponentu; zawodzą, ponieważ reguły biznesowe stopniowo splatają się z hookami React, klientami HTTP oraz API do przechowywania danych, aż nic nie może być testowane ani ponownie wykorzystywane samodzielnie. Injekcja zależności to mechanizm na niskim poziomie, który utrzymuje te reguły oddzielone, ale tylko wtedy, gdy ich połączenia są celowo zaprojektowane. Ten przewodnik opisuje, jak wybrać kontener dla monorepo typu TypeScript obejmującego kilka domen, jak zdefiniować bezpieczne pod względem typów tokeny, jak zamknąć każdą domenę za pomocą pojedynczego modułu, jak zbudować strukturę w Composition Root oraz jak udostępnić ją frameworkom React, Vue i Angular za pomocą mostów składających się po dziesięć linii kodu każdy.
Dlaczego izolacja musi zostać ustalona przed sformułowaniem modelu domeny
Klasyczne zasady projektowania napędzanego domenami mówią, że szczegóły implementacji powinny zostać odłożone na później: najpierw modele entytetów i przypadki użycia, a narzędzia wybiera się później. W praktyce zespół rozpoczynający dużą zmianę architektoniczną odnosi korzyści z rozstrzygnięcia wcześniej jednego pytania technicznego, a mianowicie tego, w jaki sposób faktycznie będzie zapewniona izolacja modułów. Bez uzgodnionego mechanizmu pierwsze domeny są łączone w sposób ad hoc, a konwencje nigdy nie wracają do normy.
Zasada leżąca u podstaw to zasada odwrócenia zależności: polityka wysokiego poziomu powinna polegać na abstrakcjach, a konkretne szczegóły powinny polegać na tych samych abstrakcjach. iniekcja zależności to praktyczna technika, która ją realizuje. Klasy proszą o abstrakcyjne tokeny zamiast konkretnych implementacji, a coś poza nimi decyduje o tym, na co te tokeny się odnoszą. W DDD i Clean Architecture nie jest to opcjonalne ulepszenie – domena nie powinna wiedzieć, z jakim frameworkiem interfejsu użytkownika, klientem HTTP czy warstwą persistencji pracuje.
Większość zespołów front-endowych najpierw sięga po React Context (lub provide/inject w Vue). Wydaje się to wystarczające: deklaruje się wartość w pobliżu korzenia, a następnie odczytuje ją wszędzie niżej. Ograniczenia pojawiają się szybko – nie ma zarządzania cyklem życia, brakuje koncepcji fabryk czy zakresów, a każdy użytkownik tej wartości jest teraz powiązany z silnikiem React, więc logika nie może działać ani być testowana bez niego. Droga od tego punktu wyjścia do prawdziwego korzenia kompozycji jest tematem reszty tego przewodnika. Tworzenie specjalistycznego kontenera rzadko się opłaca, więc pierwszym krokiem jest uczciwa ocena dostępnych opcji IoC.
Kryteria dla kontenera DI w front-endzie
Frameworki backendowe takie jak NestJS, Spring i .NET dostarczają mechanizm DI jako rozwiązane, standardowe rozwiązanie. Kod frontendowy ma dodatkowe ograniczenia: koszt wykonywania, limity rozmiaru plików oraz konieczność pracy z różnymi frameworkami interfejsu użytkownika. Pięć kryteriów określa cel oceny:
- Agnostyczność wobec frameworków. Kontener musi działać z zwykłym TypeScriptem, bez żadnych zależności od React, Vue czy Angular, i musi funkcjonować w przeglądarce, w Node.js do SSR oraz w React Native.
- Modularność. Domena powinna móc dostarczyć uprzednio skonfigurowany moduł z zamkniętymi w nim powiązaniami, tak aby aplikacja pobierała go wyłącznie. To zapobiega powstawaniu 500-linijnego pliku
main.ts, w którym każda usługa jest ręcznie rejestrowana. - Zarządzanie cyklem życia. Singletoni, instancje tymczasowe oraz rozwiązywanie problemów w określonych kontekstach muszą być obsługiwane na najwyższym poziomie.
Kandydaci
InversifyJS
InversifyJS od dawna jest standardowym wyborem dla dużych projektów typu TypeScript i stanowi najpełniejszy kontener IoC w tym ekosystemie. Jest niezależny od frameworków i deklaruje zależności za pomocą dekoratorów. Jego ContainerModule grupuje powiązania w logiczne jednostki, a także obsługuje tryby inSingletonScope, inTransientScope i inRequestScope, kontenery potomne, ładowanie i usuwanie modułów w czasie wykonywania oraz haki aktywacyjne. Cena idzie w parze z wagą: był to najcięższy w użyciu wybór, a określanie aktualnych wersji jako lekkich byłoby mylące.
TSyringe
TSyringe, utrzymywany przez Microsoft, oferuje szeroki zakres funkcji w zamian za prostotę użycia. Automatycznie rozwiązuje zależności konstruktorów na podstawie metadanych TypeScript, dostarcza bogaty zestaw dekoratorów oraz obsługuje typowe tryby życia (Singleton, Transient, ResolutionScoped, ContainerScoped). Dla aplikacji średniej wielkości stanowi atrakcyjne rozwiązanie o niskich wymaganiach konfiguracyjnych. W architekturze wielodomenowej występują dwa minusy: brakuje koncepcji modułów pierwszej klasy, więc kompozycja musi być budowana ręcznie za pomocą funkcji rejestratora lub createChildContainer(), a ponadto narzędzie to polega na polyfillu reflect-metadata, co powoduje znaczną dodatkową wagę.
Awilix
Awilix całkowicie unika dekoratorów. Wykorzystuje API ES6 Proxy do dopasowywania zależności do nazw parametrów konstruktora lub do kluczy obiektu argumentów fabryki. Dzięki temu doskonale pasuje do zespołów, które nie chcą używać dekoratorów, a był to najmniejszy samodzielny kontener przeanalizowany. Obsługuje tryby życia SINGLETON, SCOPED i TRANSIENT; moduły są organizowane za pomocą dostosowanych funkcji rejestracji.
Jedna specyficzna dla frontendu pułapka wymaga uwagi. W trybie iniekcji CLASSIC Awilix odczytuje nazwy parametrów konstruktora jako łańcuchy znaków, a narzędzia minifikujące zmieniają nazwy tych parametrów, przez co rozwiązanie przestaje działać w środowisku produkcyjnym. Tylko domyślny tryb PROXY przetrwa agresywne modyfikacje.
Wbudowany injector w Angular
Angular posiada jeden z najpotężniejszych hierarchicznych systemów DI w rozwoju frontendu, obejmujący iniekcję opartą na tokenach, dostawców o określonym zasięgu oraz wbudowane ładowanie opóźnione. Gdyby można go było używać bez środowiska runtime Angular, byłby poważnym kandydatem do realizacji warstwy domenowej. Niestety nie jest to możliwe, i to stanowi problem.
React Context
Context w rzeczywistości nie jest kontenerem DI; to sposób na uniknięcie problemu przenoszenia właściwości po łańcuchu komponentów. Dostawca znajduje się blisko korzenia, a komponenty odczytują jego wartości za pomocą useContext(). Każda zmiana dostarczonej wartości powoduje ponowne renderowanie wszystkich komponentów, które z niej korzystają, przy czym brakuje mechanizmów zarządzania cyklem życia komponentów, wiązania fabrycznego ani izolacji zasięgu. Jego jedyną rzeczywistą zaletą jest to, że nie dodaje żadnych dodatkowych rozmiarów do pliku bundle.
Vue provide i inject
Mechanizm Vue przekazuje wartości w dół drzewa komponentów i dzieli się ograniczeniami architektonicznymi Context: nie stanowi praktycznej podstawy dla grafu usług domenowych. Ma jednak jedną zaletę ergonomiczną, ponieważ zależności mogą być rejestrowane globalnie za pomocą pluginu, zamiast otaczać drzewo nawarstwionymi dostawcami. Współczesna funkcja inject() w Angularze opiera się na podobnej koncepcji aktywnego kontekstu iniekcji.
Zwykła iniekcja przez konstruktory
Podejście zero-library przekazuje każdą zależność wyraźnie przez konstruktory. Jest w pełni bezpieczne pod względem typów i nie generuje żadnego obciążenia w czasie wykonywania. Wadą jest skalowalność: gdy aplikacja ma około 10 do 15 usług domenowych, ręczne połączenie grafu w punkcie wejścia zamienia się w duży, kruchy plik, którego wszyscy boją się edytować.
Co pokazuje to porównanie
Rozmiary plików pakietowych porównano poprzez stworzenie pliku wejściowego, który importuje publiczną API każdego pakietu za pomocą esbuild --bundle --minify --platform=browser, a następnie skompresowanie uzyskanego wyniku za pomocą gzip -9, co pozwala oddzielić własne obciążenia kontenera od kodu aplikacji. Wynikły cztery wnioski:
- Framework DI należy do interfejsu użytkownika, a nie do domeny biznesowej. Umieszczenie funkcji
useContext()lub anotacji@Injectable()z Angulara w bibliotekach domenowych łączy reguły biznesowe z jednym frameworkiem, co sprawia, że nie spełnia się pierwszy kryterium. - Ręczna iniekcja sprawdza się tylko w małych projektach. W dużym monorepo przedsiębiorstwa element Composition Root zamienia się w tysiące linii ręcznie napisanego kodu łączącego komponenty.
ContainerModule) stanowi wartość pierwszej klasy, którą biblioteka domenowa może stworzyć, zamknąć wewnątrz niej i wyeksportować jako jedną całość.tsconfig.json ustawień experimentalDecorators: true, emitDecoratorMetadata: true oraz target: ES2022. InversifyJS v8 nie wymaga już oddzielnego polyfilla reflect-metadata. W przypadku narzędzi opartych na esbuild lub SWC (Vite, Next.js, Bun) należy upewnić się, że krok transformacji obsługuje metadane dekoratorów z wersji starszych, zazwyczaj za pomocą pluginu, ponieważ esbuild sam w sobie nie wyświetla metadanych dekoratorów.Cztery zasady zapobiegające przekształceniu kontenera w lokalizator usług
Sam kontener nie izoluje niczego; jeśli jest używany lekkomyślnie, staje się globalną kolekcją usług, do której może uzyskać dostęp każdy plik. Dlatego architektura opiera się na czterech konwencjach:
- Abstrakcyjne tokeny DI utworzone za pomocą
Symbol.fororaz właściwości$. - Każda biblioteka domenowa eksportuje tylko swój moduł kontenera, nigdy swoje konkretne klasy.
- Jedyny punkt początkowy kompozycji buduje graf aplikacji.
- Cienki most dla każdego frameworku UI umożliwia dostęp do kontenera z komponentów.
Tokeny bezpieczne pod względem typu za pomocą Symbol.for i $
Kontener potrzebuje identyfikatorów w czasie wykonywania, aby mapować abstrakcje na implementacje. Interfejsy w TypeScript znikają w czasie kompilacji, więc nie mogą bezpośrednio pełnić tej roli. Tutaj używany wzorzec łączy każdy interfejs w pakiecie @my-app/*-contracts z stałą o tym samym nazwie, której właściwość $ przechowuje obiekt typu Symbol.
// 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>,
};
W tym fragmencie zawarte są trzy decyzje:
- Jeden nazwa dla typu i wartości. TypeScript przechowuje typy i wartości w oddzielnych przestrzeniach nazw, więc
AuthFacademoże być zarówno interfejsem, jak i nośnikiem tokena. Kompilator wybiera odpowiednie znaczenie na podstawie kontekstu, więc nikt nie musi wymyślać nazw takich jakAuthFacadeToken.
Symbol.for zamiast Symbol(). Każde wezwanie Symbol() zwraca zupełnie nową wartość, podczas gdy Symbol.for() wyszukuje klucz w globalnym rejestrze symboli jądra aplikacji. Jeśli pakiet kontraktów pojawi się dwukrotnie w dwóch bundlach, co może zdarzyć się w monorepo lub przy użyciu Module Federation, obie kopie nadal tworzą ten sam token, a wyszukiwania działają prawidłowo, zamiast nieoczekiwanie zawodzić. Z drugiej strony klucze rejestru są globalnymi łańcuchami, więc powinny być unikalne w całej aplikacji.ServiceIdentifier<AuthFacade>. Określenie typu $ jako Inversify’ego ServiceIdentifier<T> łączy token z jego interfejsem w czasie kompilacji, dzięki czemu mechanizmy rozwiązywania typów mogą wywnioskować właściwy typ bez konieczności użycia wyraźnych generyków.Następny fragment pokazuje obie strony: klasę otrzymującą fasadę poprzez iniekcję konstruktora oraz bezpośredni wywołanie container.get, którego typ zwracany jest wywnioskowany na podstawie tokena.
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
Konsument odwołuje się wyłącznie do kontraktu. Nie ma pojęcia, co znajduje się wewnątrz @my-app/auth-core, a właśnie o to chodzi.
Jeden domen, jeden ContainerModule
Baza krytyczna każdej domeny udostępnia jedną rzecz: swój moduł DI. Przypady użycia, entity, porty i repozytoria pozostają prywatne. Wewnątrz modułu wewnętrzne przypadki użycia są powiązane ze sobą, natomiast publiczna fasada jest powiązana z tokenem swojego kontraktu.
// 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';
Encapsulacja jest egzekwowana dwa razy:
- Publiczna API. Plik
index.tsbiblioteki ponownie eksportuje jedynieauthContainerModule. Klasy takie jakLoginUseCaseczyCoreAuthFacadesą po prostu niedostępne z innych pakietów. - Zasady lintingu. Zasada ESLinta
@nx/enforce-module-boundariesz Nx sprawdza tagi projektu, więc na przykład projekt o tagutype:corenie może importować projektu o tym samym tagutype:corez innego domeny. Szczegóły dotyczące deklarowania tagów i ograniczeń znajdują się w dokumentacji dotyczącej granic modułów w Nx.
Granice eksportu zapobiegają przypadkowym importom, natomiast zasady lintingu uniemożliwiają celowe użycie skrótów poprzez złożone ścieżki. Razem sprawiają, że architektura jest weryfikowana przez systemy CI, a nie musi być pamiętana przez recenzentów.
Korzeń kompozycji
Korzeń kompozycji to miejsce, w którym tworzony jest graf zależności, zazwyczaj apps/my-app/src/composition-root.ts lub funkcja wywoływana z main.ts. Rządzi nim jedna zasada:
Korzeń kompozycji to jedyne miejsce w bazie kodu, gdzie dozwolone jest importowanie konkretnych implementacji, adapterów infrastruktury oraz modułów jądra domeny.
Funkcja poniżej najpierw łączy globalną infrastrukturę (klient HTTP oparty na Axios oraz logger stworzony za pomocą fabryki, aby mógł przyjmować skonfigurowane środki transportu), a następnie ładuje moduły domeny.
// 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;
};
Dla pełności informacji, adapter BrowserLogger to zwykła klasa implementująca interfejs LoggerPort, która przekazuje każdą wiadomość do odpowiednich mechanizmów transmisji. Należy zauważyć, że nie zawiera żadnych dekoratorów; ponieważ jest tworzona wewnątrz funkcji toDynamicValue, kontener nie musi w ogóle sprawdzać jej konstruktora.
// 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));
}
}
Kilka aplikacji z jednego jądra domeny
Ponieważ infrastruktura jest wybierana w Composition Root, różne aplikacje mogą ponownie używać tych samych pakietów domeny z różnymi adapterami oraz innym wyborem modułów. Aplikacja React Native może korzystać z pamięci przechowującej opartej na AsyncStorage, podczas gdy aplikacja administracyjna dla przeglądarki wykorzystuje IndexedDB i ładowa moduły związane z audytem zamiast z płatnościami.
// 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 jest identyczny bit po bicie we wersjach mobilnych, administracyjnych i na komputerze. Nigdy nie dowiaduje się, czy magazynem danych jest AsyncStorage, czy IndexedDB.
Zezwalanie jednej domenie na korzystanie z drugiej
Załóżmy, że payments-core potrzebuje tokenu dostępu zarządzanego przez auth-core. Importowanie @my-app/auth-core jest zabronione zgodnie z regułami dotyczącymi granic. Zamiast tego przypadek użycia płatności polega na tokenie umowy domeny autoryzacyjnej, który znajduje się w pakiecie contracts i jest dozwolony.
// 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 nie wiąże się z AuthFacade.$; jedynie go wykorzystuje. Wiązanie zostaje nawiązane, gdy korzeń kompozycji ładuje oba moduły do tego samego kontenera. Praktyczne konsekwencje: jeśli aplikacja ładuje funkcje płatności bez autoryzacji, rozwiązanie zawodzi w czasie wykonywania, dlatego warto przeprowadzić test sprawdzający dla każdego docelowego oprogramowania, aby upewnić się, że każdy publiczny token zostanie rozwiązany przynajmniej raz.
Łączenie kontenera z frameworkami UI
Ponieważ kontener nie wie nic o żadnej bibliotece UI, każdy framework otrzymuje mały adapter składający się z około 10 do 15 linii kodu.
React
Container jest tworzony tylko raz podczas uruchamiania i nigdy nie jest zastępowany, więc wartość kontekstu nigdy się nie zmienia. Dzięki temu unika się łańcucha ponownego renderowania zwykle związanego z Context: konsumentzy otrzymują stabilną referencję. Funkcja useInjection zapamiętuje wyniki wyszukiwania dla każdego kontenera i tokenu oraz rzuca wyraźny błąd, jeśli dostawca jest nieobecny.
// 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
System pluginów Vue w połączeniu z typowanym obiektem InjectionKey sprawia, że adapter jest jeszcze krótszy. Plugin dostarcza kontener na poziomie aplikacji, a komponenty komposable wykorzystują go do rozwiązywania tokenów.
// 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
Własny hierarchiczny injector Angular może delegować zadania do kontenera domeny za pośrednictwem dostawców typu factory. Przekazywanie kontenera za pomocą InjectionToken oraz tworzenie nowego kontenera przy każdym uruchomieniu zapobiega dzieleniu się stanem pomiędzy równoczesnymi żądaniami renderowanymi na serwerze.
// 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())],
});
Każdy dodatkowy komponent Angular wymagający fasady musi mieć własny InjectionToken oraz dostawcę fabryki, więc lista ta rośnie wraz z publiczną API; generowanie jej na podstawie małego mapy jest opcją, gdy lista staje się długa.
Koszty i kiedy to pominąć
Ustawienie to dodaje około 21 KB po stłumieniu i wymaga użycia emitDecoratorMetadata podczas budowania. W przypadku korporacyjnego monorepo z dziesięcioma lub więcej domenami ten koszt szybko się zwraca. W aplikacji z dwoma lub trzema ekranami izolacja stanowi zbędny obciążenie; zwykła iniekcja konstruktora lub nawet singletoni na poziomie modułu będą lepsze rozwiązanie.
Główne wnioski
- Zachowaj mechanizmy DI frameworku (Context, provide/inject, dostawcy Angular) na granicy interfejsu użytkownika; rdzeń domeny powinien polegać wyłącznie na tokenach kontraktu.
ContainerModule, sprawia, że punkt wejścia pozostaje mały.Symbol.for o typie ServiceIdentifier<T>, aby zarówno duplikowane pakiety, jak i inferencja typów nadal funkcjonowały.Literatura pokrewna
- Naprawianie przeładowania propów w React za pomocą kompozycji i slotów — Dowiedz się, dlaczego propy w React wymagające intensywnej konfiguracji powodują problemy z utrzymaniem oprogramowania, oraz jak inwersja kontroli, kompozycja i sloty umożliwiają tworzenie naprawdę wielokrotnie używalnych komponentów.
- Włączanie narzędzi MCP do interfejsu chatowego w React z wbudowaną aprobatą człowieka — Dowiedz się, jak Model Context Protocol pasuje do aplikacji React: dlaczego backend powinien hostować MCP, jak działa serwer narzędzi oraz jak przesyłać i aprobowywać wywołania narzędzi w interfejsie użytkownika.