Фронтэнд-DI, незалежныя ад фрамворка, з InversifyJS і коранем складання
Як выбраць контейнер DI для TypeScript, запакаваць кожную домэну як ContainerModule, з’ўязаць усё ў аднам Composition Root і стварыць мостаў да React, Vue і Angular.
Большыя кодавесцы фронтэнду рэдка калі зламваюцца через адзін паказвальны компонент; яны зламваюцца тады, калі бізнес-правіла таямніча заплутваюцца з React hooks, HTTP-кліентамі і API для зберагчыка, пакуль нельга тэставаць чы ўжываць які-небудзь элемент самастоятельна. Введэнне залежнасцяў — это механізм низкага рэвэля, який падтрымлівае аддзеленнасць гэтых правілаў, але толькі якщо структура з’яўлення залежнасцяў спроектавана намерова. У гэтым кяліку показана, як выбраць контейнер для монорепо на TypeScript з калькама домэнаў, як задаць токены з абеспекай типоў, як захаваць кожны домэн у адзіном модуле, як склаць граф у Composition Root і як выказаць яго для React, Vue і Angular через мосты па дзесяткі лінакоў кожны.
Чаму ізоляцыю трэба адначасова з моделюванням домэнаў
Класычныя рэкамендаціі па дизайне, адасвечаным домэнам, кажу, што деталі рэалізацыі трэба адкладзіць: спачатку стварыце модэлі энтытаў і кейсы выкарыстання, а інструменты выберыце пазней. У практыцы команда, якая запускае большую архітэктурную змяну, выгадвае, якшто з самага пачатку раз’ясніць адну тэхнічную прыблуду — а самае гэта, як насправдзе будзе забезпечвацца ізоляцыя модуляў. Без даговоранага механізма першыя калькі домэнаў ствараюцца спадзяючыся на інтуіцыю, і стандарты так і не вяртаюцца.
Асновным прынцыпам являецца прынцып інверсіі залежнасцяў: вышэрагульнія правілы должны залежаць ад абстракцый, а конкрэтныя деталі — ад тых сабе абстракцый. ін’екцыя залежнасцяў — это практычны метод, які яго рэалізуе. Класы запрашваюць абстрактныя токены заместо конкрэтных реалізацый, і ўсё, што знаходзится за межамі класаў, вяршыць выбор таго, на шта будуць пераказаны гэтыя токены. У падходах DDD і Clean Architecture гэта не проста дапаможны элемент; домен не должен ведаць, з якім фрэймворкам UI, кліентам HTTP чы слайям зберагання дадзеных ён працюе.
Большасць команд фронтэнду спачатку выбірають React Context (або provide/inject у Vue). Гэта здаецца дастатнім: задаецца значэнне поблізу корня, і яго можна чытаць будзь-дзе нижэй. Але абмежэння стаюць зрозумелыя быстра. Не існуе керавання жыцёвым циклам, няма поняття фабрык чы рэнджоў, і кожны корыстувальнік значэння тепер прыўязаны да рантайму React, таму логіка не можа працаваць чы пераблакавацца без яго. Шлях ад гэтай выходнай точкі да правильнага Composition Root адбываецца так, як паспісвае рэшта гэтага кяліку. Складанне спецыяльнага контейнера рэдкасцю ўзрабатнае, таму першы крок — чыста ацэніць існуючыя варыянты IoC.
Крэтарыі для контейнера DI фронтэнду
Фрэймворкі бэкенду, такія як 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 мае адну з найэфектывniejszych іерархічных систем DI у розрабоце фронтэнду, з можлівасцю ін’екцыі на адазе токена, прадастчыкаў з актуальным дыяпазонам і вбудованай можлівасцю паўзовага завантажэння. Якбы яго можна было выкарыстоўваць без рантайму Angular, ён стаў бы серйозным кандыдатам для ролі слоя домэну. Але гэта немагчыма, і гэта ёсць проблема.
React Context
Context на самай працоўцы не ёсць контэйнерам DI; гэта спосаб ухіленьняся ад неабходнасці передачы значэнняў праз множлівасць пропаў. Прадастчык размешчаецца блізка да корню, а компоненты чытаюць яго за дапамою useContext(). Будзь-яя змена значэння, якое прадаецца, прыводзіць да перыскалявання кожнага корыстувальніка, і тут няма жыцёвага циклу, няма прыўязкі фабрыкі і няма ізоляцыі дыяпазона. Яго ўжоўтканая перавага — гэта тое, што ён не дадае нічога дапаможнага у пакете.
Vue provide і inject
Механізм Vue перадае значэнні па дрэву компанентаў і выкарыстоўвае архітэктурныя ліміты Context: ён не являецца практычным аднойчынам для графа домэнных служб. У ён аднойчынае эрганічная перавага — залежнасці можна зарэўнаваць глобальна через плагін, замест таго каб абгортваць дрэва ў вялікія прадаставальнікі. Сучасная функцыя 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 больш не патрабуе аддзельнага polyfill-а 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 завантажвае оба модулі ў адны і той самы контейнер. Практычны наследак: якщо прыстанок завантажвае функціяналізацыю платэжаў без аутантыкаціі, рашэнне задачы не выйдзе пад час запуску, таму варта адрабатваць тэсты для кожнай меты прыстанка, якія адразу рашаюць кожны публічны токен.
Спаяванне контэйнера з фреймворкамі UI
Паколькі контэйнер нічога не ведае пра будзь-якія бібліятэкі 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 — Дазвольце вам дазнацца, чырваёныя пропы ў React з великай канфігурацыяй ствараюць проблемы з адтрымкай, і як інверсія кантролю, композіцыя і слоты дапамагаюць ствараць справжна перадпрацоўваныя компоненты.
- Інтэграцыя інструментаў MCP у інтерфейс чату ў React з вбудованым апраўленнем чалавекам — Дазвольце вам дазнацца, як протакол Model Context Protocol падходзіць для викорыстоўвання ў додатках на React: чаму бэкенд павінен хаваць MCP, як працюе сервер інструментаў, і як стрімаваць та апраўляць вызывы інструментаў у інтерфейсе.