Angular даёт два способа сделать сервисы доступными для инъекции:
Автоматическая регистрация — через providedIn в декораторе @Injectable, декораторе @Service или фабрику в конфигурации InjectionToken
Ручная регистрация — через массив providers в компонентах, директивах, маршрутах или конфигурации приложения
В предыдущем руководстве разобрано, как создавать сервисы с providedIn: 'root'. Этого хватает в большинстве обычных случаев. Здесь — дополнительные схемы и автоматической, и ручной настройки провайдеров.
Автоматическая регистрация зависимостей, которые не являются классами¶
Декоратор @Injectable с providedIn: 'root' хорошо подходит для сервисов (классов). Иногда нужно глобально отдать значение другого типа: объект конфигурации, функцию или примитив. Для этого в Angular есть InjectionToken.
InjectionToken — объект, по которому система инъекции зависимостей Angular однозначно находит значение для инъекции. Это особый ключ: через него в DI сохраняют и получают значение любого типа.
1 2 3 4 5 6 7 8 91011121314
import{InjectionToken}from'@angular/core';// Create a token for a string valueexportconstAPI_URL=newInjectionToken<string>('api.url');// Create a token for a functionexportconstLOGGER=newInjectionToken<(msg:string)=>void>('logger.function');// Create a token for a complex typeexportinterfaceConfig{apiUrl:string;timeout:number;}exportconstCONFIG_TOKEN=newInjectionToken<Config>('app.config');
Строковый параметр (например, 'api.url') — описание только для отладки. Angular опознаёт токены по ссылке на объект, а не по этой строке.
import{InjectionToken}from'@angular/core';exportinterfaceAppConfig{apiUrl:string;version:string;features:Record<string,boolean>;}// Globally available configuration using providedInexportconstAPP_CONFIG=newInjectionToken<AppConfig>('app.config',{providedIn:'root',factory:()=>({apiUrl:'https://api.example.com',version:'1.0.0',features:{darkMode:true,analytics:false,},}),});// No need to add to providers array - available everywhere!@Component({selector:'app-header',template:`<h1>Version: {{ config.version }}</h1>`,})exportclassHeader{config=inject(APP_CONFIG);// Automatically available}
Когда использовать InjectionToken с фабричными функциями¶
InjectionToken с фабричной функцией удобен, когда класс не подходит, а зависимость всё равно нужно предоставить глобально.
/app/logger.token.ts
1 2 3 4 5 6 7 8 910111213141516171819
import{InjectionToken,inject}from'@angular/core';import{APP_CONFIG}from'./config.token';// Logger function typeexporttypeLoggerFn=(level:string,message:string)=>void;// Global logger function with dependenciesexportconstLOGGER_FN=newInjectionToken<LoggerFn>('logger.function',{providedIn:'root',factory:()=>{constconfig=inject(APP_CONFIG);return(level:string,message:string)=>{if(config.features.logging!==false){console[level](`[${newDate().toISOString()}] ${message}`);}};},});
/app/storage.token.ts
1 2 3 4 5 6 7 8 910
// Providing browser APIs as tokensexportconstLOCAL_STORAGE=newInjectionToken<Storage>('localStorage',{// providedIn: 'root' is configured as the defaultfactory:()=>window.localStorage,});exportconstSESSION_STORAGE=newInjectionToken<Storage>('sessionStorage',{providedIn:'root',factory:()=>window.sessionStorage,});
/app/feature-flags.token.ts
1 2 3 4 5 6 7 8 91011121314151617
// Complex configuration with runtime logicexportconstFEATURE_FLAGS=newInjectionToken<Map<string,boolean>>('feature.flags',{providedIn:'root',factory:()=>{constflags=newMap<string,boolean>();// Parse from environment or URL paramsconsturlParams=newURLSearchParams(window.location.search);constenableBeta=urlParams.get('beta')==='true';flags.set('betaFeatures',enableBeta);flags.set('darkMode',true);flags.set('newDashboard',false);returnflags;},});
У такого подхода несколько преимуществ:
Ручная настройка провайдера не нужна — работает так же, как providedIn: 'root' у сервисов
Участвует в tree-shaking — в сборку попадает, только если реально используется
Типобезопасность — полная поддержка TypeScript для значений, которые не являются классами
Можно внедрять другие зависимости — фабричные функции вызывают inject(), чтобы получить другие сервисы
import{Injectable,Component,inject}from'@angular/core';// Service without providedIn@Injectable()exportclassLocalDataStore{privatedata:string[]=[];addData(item:string){this.data.push(item);}}// Component must provide it@Component({selector:'app-example',// A provider is required here because the `LocalDataStore` service has no providedIn.providers:[LocalDataStore],template:`...`,})exportclassExample{dataStore=inject(LocalDataStore);}
Сервис с providedIn: 'root' можно переопределить на уровне компонента. Экземпляр тогда живёт столько же, сколько компонент. Когда компонент уничтожается, уничтожается и предоставленный сервис.
1 2 3 4 5 6 7 8 91011121314151617
import{Injectable,Component,inject}from'@angular/core';@Injectable({providedIn:'root'})exportclassDataStore{privatedata:ListItem[]=[];}// This component gets its own instance@Component({selector:'app-isolated',// Creates new instance of `DataStore` rather than using the root-provided instance.providers:[DataStore],template:`...`,})exportclassIsolated{dataStore=inject(DataStore);// Component-specific instance}
Система инъекции зависимостей Angular иерархическая. Когда компонент запрашивает зависимость, Angular начинает с инжектора этого компонента и поднимается по дереву, пока не найдёт провайдер. У каждого компонента в дереве приложения может быть свой инжектор, и эти инжекторы повторяют дерево компонентов.
Такая иерархия даёт следующее:
Экземпляры с областью видимости. Разные части приложения держат разные экземпляры одного сервиса
Переопределение. Дочерние компоненты переопределяют провайдеры родителей
Экономия памяти. Сервисы создаются только там, где они нужны
В Angular любой элемент с компонентом или директивой может предоставлять значения всем своим потомкам.
123456789
graph TD
subgraph platform
subgraph root
direction TB
A[SocialApp] --> B[UserProfile]
A --> C[FriendList]
C --> D[FriendEntry]
end
end
В примере выше:
SocialApp может предоставлять значения для UserProfile и FriendList
FriendList может предоставлять значения для инъекции в FriendEntry, но не в UserProfile: тот не входит в это дерево
Систему инъекции зависимостей Angular удобно представить как хеш-таблицу или словарь. Каждый объект конфигурации провайдера задаёт пару «ключ — значение»:
Ключ (идентификатор провайдера). Уникальный идентификатор, по которому запрашивают зависимость
Значение. То, что Angular возвращает, когда запрашивают этот токен
При ручной регистрации обычно встречается краткая запись:
12345678
import{Component}from'@angular/core';import{LocalService}from'./local-service';@Component({selector:'app-example',providers:[LocalService],// Service without providedIn})exportclassExample{}
Это сокращение более подробной конфигурации провайдера:
123456789
{// This is the shorthand versionproviders:[LocalService],// This is the full versionproviders:[{provide:LocalService,useClass:LocalService}]}
В Angular есть встроенный класс InjectionToken. Он создаёт уникальную ссылку на объект для инжектируемых значений и для случая, когда у одного интерфейса несколько реализаций.
Можно ли использовать интерфейсы TypeScript как идентификаторы для инъекции?¶
Интерфейсы TypeScript нельзя использовать для инъекции: во время выполнения их нет.
1 2 3 4 5 6 7 8 9101112131415161718192021222324
// ❌ This won't work!interfaceDataService{getData():string[];}// Interfaces disappear after TypeScript compilation@Component({providers:[{provide:DataService,useClass:LocalDataService},// Error!],})exportclassExample{privatedataService=inject(DataService);// Error!}// ✅ Use InjectionToken insteadexportconstDATA_SERVICE_TOKEN=newInjectionToken<DataService>('DataService');@Component({providers:[{provide:DATA_SERVICE_TOKEN,useClass:LocalDataService}],})exportclassExample{privatedataService=inject(DATA_SERVICE_TOKEN);// Works!}
InjectionToken даёт значение времени выполнения, с которым работает DI Angular, и сохраняет типобезопасность через параметр обобщённого типа TypeScript.
useClass предоставляет класс JavaScript как зависимость. Это вариант по умолчанию в краткой записи:
1 2 3 4 5 6 7 8 910111213141516
// Shorthandproviders:[DataService];// Full syntaxproviders:[{provide:DataService,useClass:DataService}];// Different implementationproviders:[{provide:DataService,useClass:MockDataService}];// Conditional implementationproviders:[{provide:StorageService,useClass:environment.production?CloudStorageService:LocalStorageService,},];
import{Injectable,Component,inject}from'@angular/core';// Base logger@Injectable()exportclassLogger{log(message:string){console.log(message);}}// Enhanced logger with timestamp@Injectable()exportclassBetterLoggerextendsLogger{overridelog(message:string){super.log(`[${newDate().toISOString()}] ${message}`);}}// Logger that includes user context@Injectable()exportclassEvenBetterLoggerextendsLogger{privateuserService=inject(UserService);overridelog(message:string){constname=this.userService.user.name;super.log(`Message to ${name}: ${message}`);}}// In your component@Component({selector:'app-example',providers:[UserService,// EvenBetterLogger needs this{provide:Logger,useClass:EvenBetterLogger},],})exportclassExample{privatelogger=inject(Logger);// Gets EvenBetterLogger instance}
// Define configuration interfaceexportinterfaceAppConfig{apiUrl:string;appTitle:string;features:{darkMode:boolean;analytics:boolean;};}// Create injection tokenexportconstAPP_CONFIG=newInjectionToken<AppConfig>('app.config');// Define configurationconstappConfig:AppConfig={apiUrl:'https://api.example.com',appTitle:'My Application',features:{darkMode:true,analytics:false,},};// Provide in bootstrapbootstrapApplication(AppComponent,{providers:[{provide:APP_CONFIG,useValue:appConfig}],});// Use in component@Component({selector:'app-header',template:`<h1>{{ title }}</h1>`,})exportclassHeader{privateconfig=inject(APP_CONFIG);title=this.config.appTitle;}
useFactory предоставляет функцию, которая создаёт новое значение для инъекции:
1 2 3 4 5 6 7 8 91011
exportconstloggerFactory=(config:AppConfig)=>{returnnewLoggerService(config.logLevel,config.endpoint);};providers:[{provide:LoggerService,useFactory:loggerFactory,deps:[APP_CONFIG],// Dependencies for the factory function},];
Зависимости фабрики можно пометить как необязательные:
// Service that needs runtime configurationclassApiClient{constructor(privatehttp:HttpClient,privatebaseUrl:string,privaterateLimitMs:number,){}asyncfetchData(endpoint:string){// Apply rate limiting based on user tierawaitthis.applyRateLimit();returnthis.http.get(`${this.baseUrl}/${endpoint}`);}privateasyncapplyRateLimit(){// Simplified example - real implementation would track request timingreturnnewPromise((resolve)=>setTimeout(resolve,this.rateLimitMs));}}// Factory function that configures based on user tierimport{inject}from'@angular/core';import{HttpClient}from'@angular/common/http';constapiClientFactory=()=>{consthttp=inject(HttpClient);constuserService=inject(UserService);// Assuming userService provides these valuesconstbaseUrl=userService.getApiBaseUrl();constrateLimitMs=userService.getRateLimit();returnnewApiClient(http,baseUrl,rateLimitMs);};// Provider configurationexportconstapiClientProvider={provide:ApiClient,useFactory:apiClientFactory,};// Usage in component@Component({selector:'app-dashboard',providers:[apiClientProvider],})exportclassDashboard{privateapiClient=inject(ApiClient);}
bootstrapApplication(App,{providers:[{provide:API_BASE_URL,useValue:'https://api.example.com'},{provide:INTERCEPTOR_TOKEN,useClass:AuthInterceptor,multi:true},LoggingService,// Used throughout the app{provide:ErrorHandler,useClass:GlobalErrorHandler},],});
Преимущества:
Один экземпляр снижает расход памяти
Доступен везде без дополнительной настройки
Глобальным состоянием проще управлять
Недостатки:
Всегда попадает в бандл JavaScript, даже если значение нигде не инжектируют
Сложно настроить по-разному для разных частей приложения
Труднее тестировать отдельные компоненты изолированно
Зачем предоставлять при запуске, если есть providedIn: 'root'?¶
Провайдер при запуске нужен, когда:
У провайдера есть побочные эффекты (например, установка клиентского маршрутизатора)
Провайдеру нужна конфигурация (например, маршруты)
Используется схема Angular provideSomething (например, provideRouter, provideHttpClient)
Провайдеры компонента или директивы уместны, когда:
У сервиса состояние конкретного компонента — валидаторы форм, кэш компонента, менеджеры состояния интерфейса
Нужны изолированные экземпляры — каждому компоненту своя копия сервиса
Сервис нужен только одному дереву компонентов — специализированные сервисы без глобального доступа
Делается переиспользуемый компонент — он должен работать сам по себе, со своими сервисами
1 2 3 4 5 6 7 8 9101112131415161718
// Specialized form component with its own validation service@Component({selector:'app-advanced-form',providers:[FormValidationService,// Each form gets its own validator{provide:FORM_CONFIG,useValue:{strictMode:true}},],})exportclassAdvancedForm{}// Modal component with isolated state management@Component({selector:'app-modal',providers:[ModalStateService,// Each modal manages its own state],})exportclassModal{}
Преимущества:
Лучше инкапсуляция и изоляция
Проще тестировать компоненты по отдельности
Несколько экземпляров живут рядом с разной конфигурацией
Недостатки:
Новый экземпляр на каждый компонент (выше расход памяти)
Общего состояния между компонентами нет
Провайдер нужно указывать везде, где сервис нужен
Всегда попадает в тот же бандл JavaScript, что и компонент или директива, даже если значение нигде не инжектируют
Если несколько директив на одном элементе предоставляют один и тот же токен, победит одна из них, но какая именно — не определено.
Провайдеры уровня маршрута уместны для следующего:
Сервисы конкретной возможности — нужны только определённым маршрутам или модулям возможностей
Зависимости лениво загружаемого модуля — подгружаются только вместе с конкретной возможностью
Конфигурация конкретного маршрута — настройки, которые меняются от области приложения
routes.ts
1 2 3 4 5 6 7 8 9101112131415161718
exportconstroutes:Routes=[{path:'admin',providers:[AdminService,// Only loaded with admin routes{provide:FEATURE_FLAGS,useValue:{adminMode:true}},],loadChildren:()=>import('./admin/admin.routes'),},{path:'shop',providers:[ShoppingCartService,// Isolated shopping statePaymentService,],loadChildren:()=>import('./shop/shop.routes'),},];
Сервисы, предоставленные на уровне маршрута, доступны всем компонентам и директивам этого маршрута, а также его охранникам и резолверам.
Эти сервисы создаются независимо от компонентов маршрута, поэтому прямого доступа к данным конкретного маршрута у них нет.
Вместо того чтобы заставлять пользователя собирать сложные провайдеры вручную, автор библиотеки экспортирует функции, которые возвращают конфигурацию провайдеров.
/libs/analytics/src/providers.ts
1 2 3 4 5 6 7 8 910111213141516171819202122232425
import{InjectionToken,Provider,inject}from'@angular/core';// Configuration interfaceexportinterfaceAnalyticsConfig{trackingId:string;enableDebugMode?:boolean;anonymizeIp?:boolean;}// Internal token for configurationconstANALYTICS_CONFIG=newInjectionToken<AnalyticsConfig>('analytics.config');// Main service that uses the configurationexportclassAnalyticsService{privateconfig=inject(ANALYTICS_CONFIG);track(event:string,properties?:any){// Implementation using config}}// Provider function for consumersexportfunctionprovideAnalytics(config:AnalyticsConfig):Provider[]{return[{provide:ANALYTICS_CONFIG,useValue:config},AnalyticsService];}
main.ts
123456789
// Usage in consumer appbootstrapApplication(App,{providers:[provideAnalytics({trackingId:'GA-12345',enableDebugMode:!environment.production,}),],});