Галоўная / Артыкулы / Фронтэнд-DI, незалежныя ад фрамворка, з InversifyJS і коранем складання

Фронтэнд-DI, незалежныя ад фрамворка, з InversifyJS і коранем складання

Як выбраць контейнер DI для TypeScript, запакаваць кожную домэну як ContainerModule, з’ўязаць усё ў аднам Composition Root і стварыць мостаў да React, Vue і Angular.

3230 слоў

Большыя кодавесцы фронтэнду рэдка калі зламваюцца через адзін паказвальны компонент; яны зламваюцца тады, калі бізнес-правіла таямніча заплутваюцца з 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. Пяць крэтарыяў дапамагаюць застаўляць цэлі ацэнкі:

  1. Агностыцызм фрэймворка. Контейнер павінен работаць у чыстам TypeScript без дадзейных залежнасцяў ад React, Vue чыра Angular, і павінен функцыонаваць у браузеры, у Node.js для SSR і ў React Native.
  2. Модулярнасць. Домэн павінен магчымае выдатка прыкладзены модуль з усімі неабходнымі звязкамі ўнутры, каб аплікацыя загружала яго толькі. Гэта запобегае наявнасці 500-радковага main.ts, у яком кожны сервіс рэгіструецца вручную.
  3. Кантроль жыцёвага циклу. Сінглтаны, транзыентныя экземпляры і рэшэння ў межах апранткі павінны быць першым класам.
  • Магчымасць тэставання. Замена залежнасці на мак-клас чы рэплікацыю павинна здзейсніцца за адну лінію коду, без падзьвічання контейнера.
  • Вплыв на пакет. Навантажэння ў працоўным сераве павінна быць малым, як у варыянце з некаштрованым кодам, так і пасля його складчэння чы архівавання.
  • Кандыдаты

    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; гэта дазволяе адсаціваць власны надтэраг кантейнера ад коду прыкладнай програмы. Было выведзена чатыро заключэння:

    1. Фреймворк DI належыць да UI, а не да домэны. Выведзенне useContext() або @Injectable() з Angular у бібліятэкі домэны спаўнае правіла бізнесу з адним фреймворкам і не падтрымлівае першы крэатарыяў.
    2. Ручная інжэкцыя працюе толькі для малых проектаў. У вялікам корпоратывным монорепо Composition Root ператвараецца на тыячы ліній ручна напісанага кэрування.
  • InversifyJS выграў за рахунак модулярнасці. Бн — ўзеўны кандыдат, у якога модуль (ContainerModule) ўжо є першакласным элементам, які бібліятэка можа стварыць, інкапсуляваць і экспортаваць як адну ейчаку.
  • Канфігурацыя кампаўлявання мае значэнне. InversifyJS і TSyringe патрабуюць у файле 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>,
    };
    

    У гэтым фрагменте адбываюцца трыя рашэння:

    1. Адна назва для типу і значэння. 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';
    

    Інкапсуляцыя застаўленае два разы:

    1. Адміністратыўны API. Файл index.ts бібліятэкі перэекспортуе толькі authContainerModule. Класы на кшталт LoginUseCase чыста не ўможлівы да выкарыстання з іншых пакетаў.
    2. Правіла лінтавання. Правіло 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>, каб дубліруючыяся пакеты і аналіз типа продовжалі працаваць.
  • Забезпечыце межы за дапамою экспортаў і правілаў лінтингу, а не толькі стандартаў.
  • Дазволіце толькі аднаму файлу, Composition Root, кантактываць з конкрэтнымі реалізацыямі; тады будова для калькольных платформ стане простаю заменай кэшаў.
  • Памятайце, што інжэкцыя — толькі інструмент. Мета — інверсія: якщо вы можете заменіць React на Vue, SQLite на IndexedDB або Axios на Fetch без правкі шара домэна, стратэгія выпалоўвала сваю задачу.
  • Спаднія матэрыялы

  • Выбір структуры папак у React: семь лейаутаў і ўскладнення, якія з’являюцца — Пораўняйце плоскі, типовыя, функцыйна-оріѐнтаваныя, Atomic Design, DDD, Feature-Sliced Design і монорепо-лейауты для React, а таксама дазнаецеся, на якім розмаху кожны з іх перестае працаваць.
  • Перанесення Angular HttpClient да сервера Fetch за дапамою withFetch() — Дазнаецеся, чым вызвана замена XMLHttpRequest на fetch у Angular HttpClient, як withFetch() дазволяе реалізаваць edge SSR і стрімінг, і які наэфекты гэта мае для вашых інтерцептароў.
  • Як Angular Dependency Injection рашуеяць службы за дапамою inject() — З’ясавайце, як Angular DI задаёць службы через функцию inject(), элементы providers і іерархічныя injectоры, а таксама як контролаваць экзанпляры і ухілівацца ад памылак з прасабою недастатку провайдера.
  • SOLID як пытанне пра змяны: рефактораванне службы Spring Boot — Навучыцеся прыкладваць кожны з прынцыпаў SOLID у бэкенде Spring Boot, задаючыся пытанням, што можа змяніцца, і як ухілівацца ад надмернага проектавання, якое часта спрычынаюць прынцыпы SOLID.