Введэнне залежнасцей у монорепозітарыі на TypeScript: InversifyJS для React і Angular
Введэнне залежнасцей у монорепозітарыі на TypeScript: выберыце контейнер, задаце токены з абэранцэю типаў, складзіце Composition Root і з’яўяце з’ёднанне з React, Vue і Angular.
Крыніцы: Frontend Dependency Injection: From Context API to Composition Root
Большыя кодавыя базы фронтэнду рэдка калі злучваюцца з-за аднаго неякіснага компонента; яны злучваюцца тады, калі бізнес-правіла таямніча пачынаюць пераплетацца з React hooks, HTTP-кліентамі і API для зберагчыка, пакуль нельгае нічога працаваць або перыякорваць самастаі. Введэнне залежнасцей — это механізм нізкага рэвэлю, які дазваляе залічваць гэтыя правіла окремымі, але толькі якщо структура падключэння спроектавана намеравана. У гэтым кяліку показана, як выбраць контейнер для манорепа на TypeScript з калькама домэнаў, як задаць токены з абэрнуццю типаў, як захаваць кожны домэн за адним модулем, як склікці граф у Composition Root і як выклікць яго ў React, Vue і Angular чераз мосты па дзесяткі ліній кожны.
Чаму ізоляцыю трэба адначасова з модэлюванням домэна
Класычныя рэкамендацыі па дызайне, адасвечанаму домэнамі, кажу, што деталі рэалізацыі трэба адкладзіць: спачатку ствараюцься энтытэты модэлю і кейсы выкарыстоўвання, а інструменты выбіраюцца пазней. У практыцы команда, якая пачынае большую архітэктурную змяну, выгадвае, якшто з самага пачатку раз’ясніць адну тэхнічную прыбліжнасць — а самае гэта, як на самай працэ практыкувана ізоляцыя модуляў. Без дагаворанага механізма першыя калькі домэнаў ствараюцца спадарожнім спосабам, і стандарты так і не вяртаюцца да нормы.
Асновным прынцыпам являецца прынцып інверсіі залежнасцяў: высокага рангу стратэгіі павінны залежаць ад абстракцый, а конкрэтныя деталі — ад тых сабе абстракцый. Ін’екцыя залежнасцяў — это практычны метод, які яе рэалізуе. Класы запрашваюць абстрактныя токены заместо конкрэтных рэалізацый, і што-то званае зза ўсімі класамі вяршыць тое, на шта будуць перакладваны гэтыя токены. У падходах DDD і Clean Architecture гэта не проста дапаможны элемент; домэн не павінен знаваць, з якім фрэймворкам UI, кліентам HTTP чыям слоем зберагання він працюе.
Большасць команд фронтэнду спачатку выбіраюць React Context (альбо provide/inject у Vue). Гэта здаецца дастатнім: задаецца значэнне блізу корні, і яго можна чытаць будзь-дзе нижэй. Але абмежэнні стаюць зрозумелымі быстра. Не існуе керавання жыцёвым циклам, няма панявання пра фабрыкі чыста дыяпазоны, і кожны корыстувальнік значэння тепер прыўязаны да рантайму React, таму логіка не можа выконвацца чыста буць працавана без яго. Шлях ад гэтай выхіднай точкі да правильнага Composition Root адбываецца самэй у рэште гэтага кяліку. Стварэнне спецыяльнага контейнера рэдкасцю вартае тыяго, таму першым крокам є чыста адказна ацэнка існуючых варыянтаў IoC.
Крэтарыі для контейнера для впрыску залежнасцэў у фронтэндзе
Фрэймворкі бэкэнду, такія як NestJS, Spring і .NET, выходзяць з рэштаваным падходам да інжыніерыі залежнасцей як да стандартнай праблемы. Код фронтэнду мае дадзейныя абмежэнні: вартасць выканання, ліміты розмеру пакетаў і патрэба працаваць з разнымі фрэймворкамі UI. Пяць крэтэріяў дапамагаюць застаўляць цялеспрабавнасць ацэнкі:
- Агностыцызм фрэймворка. Контэйнер павінен работаць у чыстым TypeScript без дадзейных залежнасцей ад React, Vue чыра Angular, і павінен функцыонаваць у браузеры, у Node.js для SSR і ў React Native.
- Модулярнасць. Домэн павінен магчымаць выдачу празконфігураванага модуля з усімі неабходнымі звязкамі ўнутры, каб аплікацыя загружала яго толькі. Гэта запобегае наявнасці 500-радковага
main.ts, у яком кожны сервіс рэгіструецца вручную. - Калітэкс-менеджамент. Сінглтаны, транзыентныя экземпляры і рэшэнняяць у межах аплікацыі павінны быць падтрымваныя на першым роўні.
Кандыдаты
InversifyJS
InversifyJS даўно ўжо є стандартным выборам для большых проектаў на TypeScript і ўсёбяжным кантэйнерам моделі IoC у гэтым екасістэме. Ён незалежны ад фрэймворка і выказвае залежнасці за дапамогою дэкоратараў. Його ContainerModule групавае зв’язкі ў логічныя елементы, і ён падтрымлівае режымы inSingletonScope, inTransientScope і inRequestScope, дзецячыя кантэйнеры, завантажэнне і выключэнне модуляў пад час роботы, а таксама хукі актывацыі. Недзея — ў вазе: ён быў найважэлівейшым з усіх варыянтав, а называць сучасныя версіі лёгкімі было б вадзічна.
TSyringe
TSyringe, якій адмініструе Microsoft, ставіць шыроту на другое месца рады ўдобства. Ў ёй автаматычна выкарыстоўваюцца залежнасці канстрактараў з метададаў TypeScript, ёсць шырокі набор декоратараў, і ён падтрымлівае звычныя періяды жыцця (Singleton, Transient, ResolutionScoped, ContainerScoped). Для середняпаўерховага прыемлівага застосоўвання гэта ўдобны спосаб наладкі. Аднак у архітэктуре з калькама домэнаў ёсць два недзеяння: няма концэпцыі модуля першага класу, таму складанне праблем трэба ствараць вручную за дапамою функцый-рэгістратораў або createChildContainer(), і ён залежыць ад паліфіла reflect-metadata, які дадае значны вагу.
Awilix
Awilix абсалютна не выкарыстоўвае декоратары. Ён выкарыстоўвае API ES6 Proxy, каб паўязаць залежнасці з іменамі параметраў канстрактара або з ключамі об’екта аргументаў фабрыкі. Чыніцца, гэта ідеальна падходзіць для команд, якія не хочуць декоратарыв, і гэты прыстрой быў найменшым самостойным контейнерам, які быў перакананы. Ён падтрымлівае режымы жыццёвага цыклу SINGLETON, SCOPED і TRANSIENT; модулі організуюцца за дапамою спецыяльных функцый рэгістрацыі.
Є адна проблема, спецыфічная для фронтэнду, якая заслуговае на увагу. У режыме ін’екцыі CLASSIC Awilix чытае імены параметраў канстрактара як строкі, а зменшувачы коду перэіменовуюць гэтыя параметры, таму процес развязкі залежнасцей ламаецца у продакшэне. Толькі стандартны режым PROXY вытрымае агрэсыўную перэіменавання.
Вбудованы ін’ектар Angular
Angular располагае аднам з найболей супэльных іерархічных систем DI у розрабоце фронтэнду, якія включае в сябе вбрыск на адваротах, прадастчыкі з абмежаннем дыячнасці і можлівасць пасляўзлагання. Якбы яго можна было выкарыстоўваць без рантайму Angular, ён стаў бы серйозным кандыдатам для ролі слоя домэну. Але гэта немагчыма, і гэта ёсць проблема.
React Context
Context на самай працоўцы не ёсць контэйнером DI; гэта спосаб ухіліцы ад неабходнасці передачы значэнняў праз множыцю прыемнікаў. Прадастчык размешчаецца блізка к корню, а компоненты чытаюць яго за дапамою useContext(). Будзь-яя змена значэння, якое прадаецца, прыводзіць да паўтарнага атрыбутавання кожнага прыемніка, пры чым няма жыцёвага цыклу, няма прыўязкі праз фабрыку і няма ізоляціі дыячнасці. Яго ўзельныя прынтэльныя перавагі — гэта тое, што ён не дадае нічога дапаможнага у пакете з кодам.
Vue provide і inject
Механізм Vue перадае значэнні па дрэве компанентаў і выкарыстоўвае архітэктурныя ліміты Context: гэта не ўсходжліва база для графа домэнных служб. У яго ёсць адна практычная перавага — залежнасці можна зарэўнаваць у глобальным масштабе за дапамогою плагіна, замест таго каб заглушаць дрэва компанентаў за дапамогою вялікіх канструкцый provider. Сучасная функцыя inject() у Angular таксама апыраецца на аднаковую ідею актыўнага контэкста інжэкцыі.
Інжэкцыя через канструктары
Падход з нульовым наборам бібліятак працюе за прынцыпам явнага перадачы кожной залежнасці через канструктары. Гэты падход ўсё абавязкова типавы і не мае додатковых накладных витрачэнняяў пад час выканання. Недзея — масштабаванне: калі ў прыкладнай програме ўжо є ад 10 да 15 домэнных служб, ручная наладка графа ў пункце запуску ператвараецца на вялікі, нестабільны файл, які всі бояцца чыпляць.
Што паказвае парабярэнне
Размахі бандлів былы парабянены праз’еднаннем файлу-вхіду, які імпортуе публічны API кожнага пакета, за дапамою esbuild --bundle --minify --platform=browser, і стисненнем рэзультата за дапамою gzip -9; гэта дазволяе аддзервацыяваць сабеўскі наклад на контейнер ад коду прыемлі. Выйшлі чатыры вывары:
- Фрэймворк DI належыць да UI, а не да домэны. Выварка
useContext()або@Injectable()у Angular у бібліятэкі домэны спаўівае правіла бізнесу з адним фрэймворкам і не падтрымлівае першы крэтэріян. - Ручная інжэкцыя працуе толькі для малых проектаў. У вялікім корпоратыўным монорепо Composition Root ператвараецца на тыячы ліній ручна напісанага кэрэнтавання.
ContainerModule) ўжо ўнічыжна ўважаецца цэннасцю першага класу, якую бібліятэка домэна можа стварыць, інкапсуляваць і экспортаваць як адну елементарную частку.tsconfig.json неабходны параметры experimentalDecorators: true, emitDecoratorMetadata: true і target: ES2022. У версіі InversifyJS v8 больш не трэба окалічнага паліфіла reflect-metadata. Калі вы викорыстоўваеце інструменты на базе esbuild або SWC (Vite, Next.js, Bun), пераканайцеся, што крок трансформацыі падтрымлівае старыя метаданы декоратараў, зазвычай за дапамогою плагіна, адтуды што esbuild сама па сабе не выдае метаданы декоратараў.Чатыры правілы, якія не дазволяюць контейнеру стаць локаторам служб
Сам контэйнер не ізалюе нічога; якщо яго викорыстоўваць без адказнасці, ён стае глобальным «мешкам» служб, да якога можа дастынуць будзь-які файл. Таму архітектура апыраецца на чатыры прыемы:
- Абстрактныя токены DI, створаныя з
Symbol.forі атрыбутам$. - Кожная бібліятэка домэны вывозіць толькі свой модуль контэйнера, а не своі конкрэтныя класы.
- Одна-едынственная точка складання (Composition Root) складае граф аплікацыі.
- Слабы мост для кожнай парадыгмы UI адкрывае доступ да контэйнера для компонентаў.
Безпека типа за дапамой токенаў з Symbol.for і $
Контейнеру патрэбны ідэнтыфікатары часа выканання, каб перадаць абстракцыі ў імплементацыі. Інтэрфейсы TypeScript зникаюць у часе кампілявання, таму яны не можуць безпасоваць гэтую ролю безпосередна. У гэтым падходзе кожны інтэрфейс у пакете @my-app/*-contracts супараджуецца з константай таго ж назвы, чыя атрыбут $ мае значэнне типу 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>,
};
У гэтым абзацы знаходзяцца тры рашэнні:
- Адна назва для типу і значэння. TypeScript зберагае типы і значэнні ў разных прасторах імен, таму
AuthFacadeможа выступаць як інтерфейс, так і хранільнік токена. Кампайлер выбірае правильны значэння на аднойчынку з контекстам, і нікому не трэба выдумваць такія назвы, якAuthFacadeToken.
Symbol.for у змене на Symbol(). Кожны вызов Symbol() вяртае абсалютна новую значэнне, тады калі Symbol.for() шукае ключ у глобальным реестре симвалоў часу выканання. Якщо пакет contracts павторыцца ў двух бандлах, як гэта можа здарыцца ў манорепа або з Module Federation, обе копіі все раве ствараюць той самы токен, і пошукі продовжаюць работаць, а не збіваюцца незрозумела. Недзея гэтага ў тым, што ключы реестра ўсё ж глобальныя строкі, таму яны павінны быць унікальныя ў всім прыемліцэ.ServiceIdentifier<AuthFacade>. Указванне $ як Inversify-а ServiceIdentifier<T> прыяўляе токен да яго інтэфейса ў часе компілявання, тады месцы разв’язкі можаюць выважыць правы тип без явных гэнерыкаў.Наступны ўрывак паказвае обе стороны: клас, які прымае фасад за дапамою інжэкцыі через канстрактар, і прымітны вызов container.get, чыя тип адпаведнасці вяртаецца выважваецца на адварыяце.
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
Корыстнік абсалютна час апіляець толькі на контракт. Ён не мае жадных падстав разумець, што знаходзіцца ўнутрь @my-app/auth-core, і самэ гэта якраз і є метай.
Адна домэна, адны ContainerModule
Кожная базовая біблія домэны адкрывае толькі адну рэч: свой модуль DI. Кейсы викорыстання, ентытэты, порты і рэпазітары застаюцца прыватнымі. Унутрь модуля внутршнія кейсы викорыстання прыўязаны да сабе, тады як публічны фасад — да токена свайго контракта.
// 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';
Прыемкаванне выканана два разы:
- Публічны API. Файл
index.tsбібліятэкі зноў выэкспортуе толькіauthContainerModule. Класы на кшталтLoginUseCaseчыста не ўсунены для доступу з іншых пакетаў. - Правіла лінтавання. Правіло ESLint
@nx/enforce-module-boundariesз Nx пераглядае тагі проекта, таму, напрыклад, проект з тагамtype:coreне можа імпортуваць проект з тагамtype:coreз іншай домэны. Дакладнейша інфармацыя пра тое, як задаюцца тагі і абмежэнні, даўнаецца ў дасягліве пра межы модуляў Nx.
Межа экспорту запобегае нехталежным імпортам; правіла лінтавання — намеровым апштучкам через дзейнавыя шляхі. Разам яны ператвараюць архітэктуру на тое, што пераканальваецца за дапамогою CI, а не на тое, што трэба памяцаць рэвізорам.
Коранскі элемент складання
Корант складання — гэта тое месца, дзе ствараецца граф завіснасця, зазвычай apps/my-app/src/composition-root.ts або функцыя, яка запускаецца з main.ts. Яго кантролюе адна правіла:
Корант складання — гэта едынственна частка кодбазы, дзе дазволена імпортацыя конкрэтных рэалізацый, адаптараў інфраструктуры і основных модуляў домэны.
Функцыя, паказаная нижэй, спачатку прыўязывае глобальную інфраструктуру (HTTP-кліента на базе Axios і логгера, створанага з фабрыкі, каб ён мог узьмляць настаўленыя способы передачы дадзенаў), а пасля завантажвае модулі домэны.
// 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;
};
Для цяласообразнасці, адаптар BrowserLogger — это звычны клас, які рэалізуе LoggerPort і перадае кожную паведамленне да адпрацоўкічых прыемнікаў. Заўважыце, што ён не мае жадных дэкоратараў; адтаколькі ён ствараецца ўнутры toDynamicValue, контейнер ніколі не патрабуе пераглядаць яго канстрактар.
// 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));
}
}
Кальколькая аплікацыя з аднаго домэннага ядра
Паколькі інфраструктура выбіраецца ў Composition Root, разныя аплікацыі можаць перадаўаць выкарыстоўваць тыя ж домэнныя пакеты з разнымі адаптарамі і разным выборам модуляў. Аплікацыя на React Native можа выкарыстоўваць схованне, падтрымванае AsyncStorage, тады калі аплікацыя для адміністрацыі веб-сайту выкарыстоўвае IndexedDB і завантажае домэн аудыту заместо домэна платэжаў.
// 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 ў версіях для мобайла, адміністрацыі і настольных прыстроў адпаведае абоўсюды заўсёды. Ён ніколі не вывучае, чы робіцца з дадзенням через AsyncStorage чы IndexedDB.
Дазвол на викорыстоўванне аднаго домэна другім
Падазроўваючы, што payments-core патрэбуе токен адзынкі, які керуе auth-core, імпорт @my-app/auth-core забаранены правіламі меж. У замен сцэнарый платэжаў выкарыстоўвае токен кантракта домэна адзынкі, які знаходзіцца ў пакете contracts і які дазволены.
// 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 не прыяжоўвае AuthFacade.$; ён толькі яго выкарыстоўвае. Прыяжоўванне адбываецца тады, калі Composition Root завантажае оба модулі ў адны і той самы контейнер. Практычны наследак: якщо прыстановка завантажае функцыі платежаў без аутэнтыкацыі, рашэнне задачы не выйдзе пад час выконання, таму варта адрабатваць тэсты для кожнай меты прыстановкі, якія адразу рашаюць усі публічныя токены.
Спаяванне контэйнера з React, Vue і Angular
Паколькі контэйнер нічога не ведае пра будзь-якія бібліятэкі UI, кожная фрэймворк-сістэма атрымлея маленькі адаптар дыявалю 10–15 ліній коду.
React
Контейнер ствараецца адзін раз пад запуском і ніколі не заменяецца, таму значэнне контэкста ніколі не зменшыцца. эта мера пазбегае каскаду парадзейвання, які зазвычай асоціюецца з Context: корыстувальнікі чытаюць стабільны кансэльт. useInjection мемаізуе пошук па кожным контэйнеру і токэну і выклікае чыстую памятку, якщо прадастальнік не існуець.
// 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
Система плагінам Vue і типаваны InjectionKey робяць адаптар яшчэ корачэй. Плагін забезпечвае контейнер на рэвэле аплікацыі, а компонабель выкарыстоўвае яго для разгледвання токэнаў.
// 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
Свой хіерархічны інжектар Angular можа делегавацыю выпрацоўваць у контэйнер домэны через прадаўцоў фабрык. Перадача контэйнера за дапамогою InjectionToken і стварэнне новага контэйнера па кожным запуску запобегае таму, каб паралельныя запиты, адрасаваныя серверу, дзеліліся станам.
// 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())],
});
Кожны ўтаманны компанент Angular, які выкарыстоўвае фасад, патрабуе свой InjectionToken і прадаўцу фабрыкі, таму спіс расте разам з публічным API; калі ён стане вялікім, можна вырашыць генеравацыю з маленькага карточнага апісу.
Затраты і калі можна іх прыняць
Установка дадае прыблізна 21 КБ пасля стыснення і выкарыстоўвае emitDecoratorMetadata пад час складання. Для корпаратыўнага монорепо з дзесяткамі або большам чысла домэнаў такія затраты быстра компенсуюцца. Для прыкладнага програма з двума-трыма экранамі такая ізоляцыя є зайвым навантажэнням; звычная інжекцыя через канстрактары або нават сінглтоны на рэвэлі модуля будуць кращым рашэннем.
Правілы інжекцыі залежнасцяў у фронтэндзе
- Закольцаваюце механізм DI фрэймворку (Context, provide/inject, Angular providers) на перыметры UI; ядро домэнаў должна завісіць толькі ад токэнаў кантрактаў.
- Спачатку выберыце контэйнер для модуля: домэн, які экспортуе адзін запакаваны
ContainerModule, — гэта тое, што дапамагае заліць точку входу маленькай. - Іспользуйце токены
Symbol.forз типамServiceIdentifier<T>, ўпэўніваючыся, што дуплікаваныя пакеты і аўтоматычнае выважванне типаў продалжаюць працаваць. - Забезпечыце межы за дапамогою экспортаў і правілаў лінтингу, а не толькі стандартаў.
- Дазвольце толькі аднаму файлу, тое ўзроўнюванню складання, кантактавацца з конкрэтнымі рэалізаціямі; тады складанне для разных платформ становіцца простаю заменайяючыміся кэшуваннямі.
- Памятайце, што інжэкцыя — толькі інструмент. Мета — інверсія: якщо вы можете заменіць React на Vue, SQLite на IndexedDB або Axios на Fetch без правкі шара домэна, стратэгія выканала сваю роботу.
Супаўязаныя матэрыялы
- Выкарыстоўванне композіцыі і слотаў для адліквідавання перанасыць пропамі ў React: Дазвольце вам дазнацца, чаму пропы ў React з вялікай канфігурацыяй ствараюць проблемы з адтрымкай, а таксама як інверсія кантролю, композіцыя і слоты дапамагаюць ствараць сапраўдна перадаўальныя компоненты.
- Інтэграцыя інструментаў MCP у інтэрфейс чату ў React з вбудованым апраўленнем чалавекам: Дазвольце вам дазнацца, як протакол Model Context Protocol падходзіць для викорыстоўвання ў додзеўках на базе React: чаму бэкенд павінен хаваць MCP, як працюе сервер інструментаў, і як стрімаваць та апраўляць вызывы інструментаў у інтэрфейсе.