Галоўная / Артыкулы

Введэнне залежнасцей у монорепозітарыі на TypeScript: InversifyJS для React і Angular

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

Выдавец: REACTAPP.TOP14 хв чытання3191 слоў

Крыніцы: 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. Пяць крэтэріяў дапамагаюць застаўляць цялеспрабавнасць ацэнкі:

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

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

    У гэтым абзацы знаходзяцца тры рашэнні:

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

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