Spec-Zone.ru › Angular

Определение поставщиков зависимостей

Angular предоставляет два способа сделать сервисы доступными для внедрения зависимостей:

  1. Автоматическое предоставление — с помощью providedIn в декораторе @Injectable, декоратора @Service или путем предоставления фабрики в конфигурации InjectionToken
  2. Ручное предоставление — с помощью массива providers в компонентах, директивах, маршрутах или конфигурации приложения

В предыдущем руководстве вы узнали, как создавать сервисы с помощью providedIn: 'root', который подходит для большинства распространенных случаев. В этом руководстве рассматриваются дополнительные шаблоны настройки автоматических и ручных поставщиков.

Автоматическое предоставление зависимостей, не являющихся классами

Декоратор @Injectable с providedIn: 'root' отлично подходит для сервисов (классов), однако иногда требуется глобально предоставлять значения других типов — например, объекты конфигурации, функции или примитивные значения. Для этого Angular предоставляет InjectionToken.

Что такое InjectionToken?

InjectionToken — это объект, который система внедрения зависимостей Angular использует для уникальной идентификации значений, предназначенных для внедрения. Его можно представить как специальный ключ, позволяющий сохранять и извлекать значения любого типа в системе DI Angular:

import {InjectionToken} from '@angular/core';

// Create a token for a string value
export const API_URL = new InjectionToken<string>('api.url');

// Create a token for a function
export const LOGGER = new InjectionToken<(msg: string) => void>('logger.function');

// Create a token for a complex type
export interface Config {
  apiUrl: string;
  timeout: number;
}
export const CONFIG_TOKEN = new InjectionToken<Config>('app.config');

ПРИМЕЧАНИЕ: Строковый параметр (например, 'api.url') служит исключительно описанием для отладки — Angular идентифицирует токены по ссылке на объект, а не по этой строке.

InjectionToken с providedIn: 'root'

InjectionToken с factory по умолчанию приводит к providedIn: 'root' (это можно переопределить с помощью свойства providedIn).

// 📁 /app/config.token.ts
import {InjectionToken} from '@angular/core';

export interface AppConfig {
  apiUrl: string;
  version: string;
  features: Record<string, boolean>;
}

// Globally available configuration using providedIn
export const APP_CONFIG = new InjectionToken<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>`,
})
export class Header {
  config = inject(APP_CONFIG); // Automatically available
}

Когда использовать InjectionToken с фабричными функциями

InjectionToken с фабричными функциями идеально подходит, когда нельзя использовать класс, но нужно глобально предоставлять зависимости:

// 📁 /app/logger.token.ts
import {InjectionToken, inject} from '@angular/core';
import {APP_CONFIG} from './config.token';

// Logger function type
export type LoggerFn = (level: string, message: string) => void;

// Global logger function with dependencies
export const LOGGER_FN = new InjectionToken<LoggerFn>('logger.function', {
  providedIn: 'root',
  factory: () => {
    const config = inject(APP_CONFIG);

    return (level: string, message: string) => {
      if (config.features.logging !== false) {
        console[level](`[${new Date().toISOString()}] ${message}`);
      }
    };
  },
});

// 📁 /app/storage.token.ts
// Providing browser APIs as tokens
export const LOCAL_STORAGE = new InjectionToken<Storage>('localStorage', {
  // providedIn: 'root' is configured as the default
  factory: () => window.localStorage,
});

export const SESSION_STORAGE = new InjectionToken<Storage>('sessionStorage', {
  providedIn: 'root',
  factory: () => window.sessionStorage,
});

// 📁 /app/feature-flags.token.ts
// Complex configuration with runtime logic
export const FEATURE_FLAGS = new InjectionToken<Map<string, boolean>>('feature.flags', {
  providedIn: 'root',
  factory: () => {
    const flags = new Map<string, boolean>();

    // Parse from environment or URL params
    const urlParams = new URLSearchParams(window.location.search);
    const enableBeta = urlParams.get('beta') === 'true';

    flags.set('betaFeatures', enableBeta);
    flags.set('darkMode', true);
    flags.set('newDashboard', false);

    return flags;
  },
});

Этот подход дает несколько преимуществ:

  • Не требуется ручная настройка поставщика — работает так же, как providedIn: 'root' для сервисов
  • Поддержка tree-shaking — включается только при фактическом использовании
  • Типобезопасность — полная поддержка TypeScript для значений, не являющихся классами
  • Можно внедрять другие зависимости — фабричные функции могут использовать inject() для доступа к другим сервисам

Общие сведения о ручной настройке поставщиков

Если вам нужен больший контроль, чем предоставляет providedIn: 'root', можно настроить поставщиков вручную. Ручная настройка с помощью массива providers полезна в следующих случаях:

  1. У сервиса нет providedIn — сервисы без автоматического предоставления необходимо предоставлять вручную
  2. Нужен новый экземпляр — чтобы создать отдельный экземпляр на уровне компонента или директивы вместо использования общего экземпляра
  3. Нужна конфигурация во время выполнения — когда поведение сервиса зависит от значений, определяемых во время выполнения
  4. Предоставляются значения, не являющиеся классами — объекты конфигурации, функции или примитивные значения

Пример: сервис без providedIn

import {Injectable, Component, inject} from '@angular/core';

// Service without providedIn
@Injectable()
export class LocalDataStore {
  private data: 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: `...`,
})
export class Example {
  dataStore = inject(LocalDataStore);
}

Пример: создание экземпляров для отдельных компонентов

Сервисы с providedIn: 'root' можно переопределять на уровне компонента. В этом случае экземпляр сервиса существует столько же, сколько и компонент. Поэтому при уничтожении компонента предоставленный сервис также уничтожается.

import {Injectable, Component, inject} from '@angular/core';

@Injectable({providedIn: 'root'})
export class DataStore {
  private data: 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: `...`,
})
export class Isolated {
  dataStore = inject(DataStore); // Component-specific instance
}

Иерархия инъекторов в Angular

Система внедрения зависимостей Angular имеет иерархическую структуру. Когда компонент запрашивает зависимость, Angular начинает поиск в инъекторе этого компонента и поднимается вверх по дереву, пока не найдет поставщика этой зависимости. У каждого компонента в дереве приложения может быть собственный инъектор, а эти инъекторы образуют иерархию, повторяющую структуру дерева компонентов.

Эта иерархия обеспечивает:

  • Экземпляры с ограниченной областью видимости: в разных частях приложения могут использоваться разные экземпляры одного и того же сервиса
  • Переопределение поведения: дочерние компоненты могут переопределять поставщиков родительских компонентов
  • Эффективное использование памяти: сервисы создаются только там, где они нужны

В Angular любой элемент с компонентом или директивой может предоставлять значения всем своим потомкам.

платформа

корень

SocialApp

UserProfile

FriendList

FriendEntry

В приведенном выше примере:

  1. SocialApp может предоставлять значения для UserProfile и FriendList
  2. FriendList может предоставлять значения для внедрения в FriendEntry, но не может предоставлять значения для внедрения в UserProfile, поскольку он не является частью дерева

Объявление поставщика

Представьте систему внедрения зависимостей Angular в виде хеш-таблицы или словаря. Каждый объект конфигурации поставщика задает пару «ключ — значение»:

  • Ключ (идентификатор поставщика): уникальный идентификатор, используемый для запроса зависимости
  • Значение: то, что Angular должен вернуть при запросе этого токена

При ручном предоставлении зависимостей обычно используется следующий сокращенный синтаксис:

import {Component} from '@angular/core';
import {LocalService} from './local-service';

@Component({
  selector: 'app-example',
  providers: [LocalService], // Service without providedIn
})
export class Example {}

На самом деле это сокращенная запись более подробной конфигурации поставщика:

{
  // This is the shorthand version
  providers: [LocalService],

  // This is the full version
  providers: [
    { provide: LocalService, useClass: LocalService }
  ]
}

Объект конфигурации поставщика

Каждый объект конфигурации поставщика состоит из двух основных частей:

  1. Идентификатор поставщика: уникальный ключ, который Angular использует для получения зависимости (задается с помощью свойства provide)
  2. Значение: фактическая зависимость, которую требуется получить Angular; оно настраивается с помощью разных ключей в зависимости от желаемого типа:
    • useClass — предоставляет класс JavaScript
    • useValue — предоставляет статическое значение
    • useFactory — предоставляет фабричную функцию, возвращающую значение
    • useExisting — предоставляет псевдоним для существующего поставщика

Идентификаторы поставщиков

Идентификаторы поставщиков позволяют системе внедрения зависимостей (DI) Angular получать зависимость по уникальному идентификатору. Идентификаторы поставщиков можно создать двумя способами:

  1. Имена классов
  2. Токены внедрения

Имена классов

В качестве идентификатора имени класса используется непосредственно импортированный класс:

import {Component} from '@angular/core';
import {LocalService} from './local-service';

@Component({
  selector: 'app-example',
  providers: [{provide: LocalService, useClass: LocalService}],
})
export class Example {
  /* ... */
}

Класс служит и идентификатором, и реализацией, поэтому Angular предоставляет сокращенную запись providers: [LocalService].

Токены внедрения

Angular предоставляет встроенный класс InjectionToken, который создает уникальную ссылку на объект для внедряемых значений или в случаях, когда нужно предоставить несколько реализаций одного интерфейса.

// 📁 /app/tokens.ts
import {InjectionToken} from '@angular/core';
import {DataService} from './data-service.interface';

export const DATA_SERVICE_TOKEN = new InjectionToken<DataService>('DataService');

ПРИМЕЧАНИЕ: Строка 'DataService' служит исключительно описанием для отладки. Angular идентифицирует токен по ссылке на объект, а не по этой строке.

Используйте токен в конфигурации поставщика:

import {Component, inject} from '@angular/core';
import {LocalDataService} from './local-data-service';
import {DATA_SERVICE_TOKEN} from './tokens';

@Component({
  selector: 'app-example',
  providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}],
})
export class Example {
  private dataService = inject(DATA_SERVICE_TOKEN);
}

Можно ли использовать интерфейсы TypeScript как идентификаторы для внедрения?

Интерфейсы TypeScript нельзя использовать для внедрения, поскольку во время выполнения они не существуют:

// ❌ This won't work!
interface DataService {
  getData(): string[];
}

// Interfaces disappear after TypeScript compilation
@Component({
  providers: [
    {provide: DataService, useClass: LocalDataService}, // Error!
  ],
})
export class Example {
  private dataService = inject(DataService); // Error!
}

// ✅ Use InjectionToken instead
export const DATA_SERVICE_TOKEN = new InjectionToken<DataService>('DataService');

@Component({
  providers: [{provide: DATA_SERVICE_TOKEN, useClass: LocalDataService}],
})
export class Example {
  private dataService = inject(DATA_SERVICE_TOKEN); // Works!
}

InjectionToken предоставляет значение времени выполнения, которое может использовать система DI Angular, сохраняя при этом типобезопасность благодаря параметру обобщенного типа TypeScript.

Типы значений поставщиков

useClass

useClass предоставляет класс JavaScript в качестве зависимости. Это поведение по умолчанию при использовании сокращенного синтаксиса:

// Shorthand
providers: [DataService];

// Full syntax
providers: [{provide: DataService, useClass: DataService}];

// Different implementation
providers: [{provide: DataService, useClass: MockDataService}];

// Conditional implementation
providers: [
  {
    provide: StorageService,
    useClass: environment.production ? CloudStorageService : LocalStorageService,
  },
];

Практический пример: замена логгера

Можно заменять реализации, чтобы расширить функциональность:

import {Injectable, Component, inject} from '@angular/core';

// Base logger
@Injectable()
export class Logger {
  log(message: string) {
    console.log(message);
  }
}

// Enhanced logger with timestamp
@Injectable()
export class BetterLogger extends Logger {
  override log(message: string) {
    super.log(`[${new Date().toISOString()}] ${message}`);
  }
}

// Logger that includes user context
@Injectable()
export class EvenBetterLogger extends Logger {
  private userService = inject(UserService);

  override log(message: string) {
    const name = 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},
  ],
})
export class Example {
  private logger = inject(Logger); // Gets EvenBetterLogger instance
}

useValue

useValue предоставляет любой тип данных JavaScript в качестве статического значения:

providers: [
  {provide: API_URL_TOKEN, useValue: 'https://api.example.com'},
  {provide: MAX_RETRIES_TOKEN, useValue: 3},
  {provide: FEATURE_FLAGS_TOKEN, useValue: {darkMode: true, beta: false}},
];

ВАЖНО: Типы и интерфейсы TypeScript нельзя использовать в качестве значений зависимостей. Они существуют только во время компиляции.

Практический пример: конфигурация приложения

Частый вариант использования useValue — предоставление конфигурации приложения:

// Define configuration interface
export interface AppConfig {
  apiUrl: string;
  appTitle: string;
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

// Create injection token
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// Define configuration
const appConfig: AppConfig = {
  apiUrl: 'https://api.example.com',
  appTitle: 'My Application',
  features: {
    darkMode: true,
    analytics: false,
  },
};

// Provide in bootstrap
bootstrapApplication(AppComponent, {
  providers: [{provide: APP_CONFIG, useValue: appConfig}],
});

// Use in component
@Component({
  selector: 'app-header',
  template: `<h1>{{ title }}</h1>`,
})
export class Header {
  private config = inject(APP_CONFIG);
  title = this.config.appTitle;
}

useFactory

useFactory предоставляет функцию, создающую новое значение для внедрения:

export const loggerFactory = (config: AppConfig) => {
  return new LoggerService(config.logLevel, config.endpoint);
};

providers: [
  {
    provide: LoggerService,
    useFactory: loggerFactory,
    deps: [APP_CONFIG], // Dependencies for the factory function
  },
];

Зависимости фабрики можно пометить как необязательные:

import {Optional} from '@angular/core';

providers: [
  {
    provide: MyService,
    useFactory: (required: RequiredService, optional?: OptionalService) => {
      return new MyService(required, optional || new DefaultService());
    },
    deps: [RequiredService, [new Optional(), OptionalService]],
  },
];

Практический пример: API-клиент на основе конфигурации

Ниже приведен полный пример использования фабрики для создания сервиса с конфигурацией времени выполнения:

// Service that needs runtime configuration
class ApiClient {
  constructor(
    private http: HttpClient,
    private baseUrl: string,
    private rateLimitMs: number,
  ) {}

  async fetchData(endpoint: string) {
    // Apply rate limiting based on user tier
    await this.applyRateLimit();
    return this.http.get(`${this.baseUrl}/${endpoint}`);
  }

  private async applyRateLimit() {
    // Simplified example - real implementation would track request timing
    return new Promise((resolve) => setTimeout(resolve, this.rateLimitMs));
  }
}

// Factory function that configures based on user tier
import {inject} from '@angular/core';
import {HttpClient} from '@angular/common/http';
const apiClientFactory = () => {
  const http = inject(HttpClient);
  const userService = inject(UserService);

  // Assuming userService provides these values
  const baseUrl = userService.getApiBaseUrl();
  const rateLimitMs = userService.getRateLimit();

  return new ApiClient(http, baseUrl, rateLimitMs);
};

// Provider configuration
export const apiClientProvider = {
  provide: ApiClient,
  useFactory: apiClientFactory,
};

// Usage in component
@Component({
  selector: 'app-dashboard',
  providers: [apiClientProvider],
})
export class Dashboard {
  private apiClient = inject(ApiClient);
}

useExisting

useExisting создает псевдоним для уже определенного поставщика. Оба токена возвращают один и тот же экземпляр:

providers: [
  NewLogger, // The actual service
  {provide: OldLogger, useExisting: NewLogger}, // The alias
];

ВАЖНО: Не путайте useExisting с useClass. useClass создает отдельные экземпляры, а useExisting гарантирует получение того же экземпляра-синглтона.

Несколько поставщиков

Используйте флаг multi: true, когда несколько поставщиков добавляют значения к одному токену:

export const INTERCEPTOR_TOKEN = new InjectionToken<Interceptor[]>('interceptors');

providers: [
  {provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
  {provide: INTERCEPTOR_TOKEN, useClass: LoggingInterceptor, multi: true},
  {provide: INTERCEPTOR_TOKEN, useClass: RetryInterceptor, multi: true},
];

При внедрении INTERCEPTOR_TOKEN вы получите массив, содержащий экземпляры всех трех перехватчиков.

Где можно указать поставщиков?

Angular предлагает несколько уровней регистрации поставщиков, каждый из которых по-разному влияет на область видимости, жизненный цикл и производительность:

  • Инициализация приложения — глобальные синглтоны, доступные повсюду
  • На элементе (компоненте или директиве) — изолированные экземпляры для отдельных деревьев компонентов
  • Маршрут — сервисы для определенных функций в лениво загружаемых модулях

Инициализация приложения

Используйте поставщиков уровня приложения в bootstrapApplication в следующих случаях:

  • Сервис используется в нескольких функциональных областях — например, HTTP-клиенты, ведение журналов или аутентификация, необходимые многим частям приложения
  • Нужен настоящий синглтон — один экземпляр, общий для всего приложения
  • Для сервиса не требуется конфигурация на уровне компонента — универсальные служебные средства, работающие одинаково повсюду
  • Предоставляется глобальная конфигурация — конечные точки API, флаги функций или настройки среды
// main.ts
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'?

Поставщик может понадобиться при инициализации приложения в следующих случаях:

  • Поставщик имеет побочные эффекты (например, устанавливает маршрутизатор на стороне клиента)
  • Поставщику требуется конфигурация (например, маршруты)
  • Используется шаблон provideSomething Angular (например, provideRouter, provideHttpClient)

Поставщики компонентов или директив

Используйте поставщиков компонентов или директив в следующих случаях:

  • У сервиса есть состояние, специфичное для компонента — валидаторы форм, кэши отдельных компонентов или средства управления состоянием пользовательского интерфейса
  • Нужны изолированные экземпляры — каждому компоненту нужна собственная копия сервиса
  • Сервис используется только одним деревом компонентов — специализированные сервисы, которым не нужен глобальный доступ
  • Создаются повторно используемые компоненты — компоненты, которые должны работать независимо и использовать собственные сервисы
// 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}},
  ],
})
export class AdvancedForm {}

// Modal component with isolated state management
@Component({
  selector: 'app-modal',
  providers: [
    ModalStateService, // Each modal manages its own state
  ],
})
export class Modal {}

Преимущества:

  • Лучшая инкапсуляция и изоляция
  • Упрощается индивидуальное тестирование компонентов
  • Могут сосуществовать несколько экземпляров с разными конфигурациями

Недостатки:

  • Для каждого компонента создается новый экземпляр (повышенный расход памяти)
  • У компонентов нет общего состояния
  • Поставщика необходимо указывать везде, где он требуется
  • Всегда включается в тот же пакет JavaScript, что и компонент или директива, даже если значение ни разу не внедряется

ПРИМЕЧАНИЕ: Если несколько директив на одном элементе предоставляют один и тот же токен, одна из них будет использована, но какая именно — не определено.

Поставщики маршрутов

Используйте поставщиков уровня маршрута для:

  • Сервисов для определенных функций — сервисов, необходимых только для отдельных маршрутов или функциональных модулей
  • Зависимостей лениво загружаемых модулей — сервисов, которые должны загружаться только вместе с определенными функциями
  • Конфигурации отдельных маршрутов — настроек, различающихся в разных областях приложения
// routes.ts
export const routes: 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 state
      PaymentService,
    ],
    loadChildren: () => import('./shop/shop.routes'),
  },
];

Сервисы, предоставленные на уровне маршрута, доступны всем компонентам и директивам этого маршрута, а также его защитникам и резолверам.

Поскольку экземпляры этих сервисов создаются независимо от компонентов маршрута, они не имеют прямого доступа к информации, относящейся к конкретному маршруту.

Шаблоны для авторов библиотек

При создании библиотек Angular часто требуется предоставлять пользователям гибкие возможности настройки, сохраняя при этом лаконичный API. Собственные библиотеки Angular демонстрируют эффективные шаблоны для решения этой задачи.

Шаблон provide

Вместо того чтобы требовать от пользователей вручную настраивать сложных поставщиков, авторы библиотек могут экспортировать функции, возвращающие конфигурации поставщиков:

// 📁 /libs/analytics/src/providers.ts
import {InjectionToken, Provider, inject} from '@angular/core';

// Configuration interface
export interface AnalyticsConfig {
  trackingId: string;
  enableDebugMode?: boolean;
  anonymizeIp?: boolean;
}

// Internal token for configuration
const ANALYTICS_CONFIG = new InjectionToken<AnalyticsConfig>('analytics.config');

// Main service that uses the configuration
export class AnalyticsService {
  private config = inject(ANALYTICS_CONFIG);

  track(event: string, properties?: any) {
    // Implementation using config
  }
}

// Provider function for consumers
export function provideAnalytics(config: AnalyticsConfig): Provider[] {
  return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService];
}

// Usage in consumer app
// main.ts
bootstrapApplication(App, {
  providers: [
    provideAnalytics({
      trackingId: 'GA-12345',
      enableDebugMode: !environment.production,
    }),
  ],
});

Расширенные шаблоны поставщиков с параметрами

В более сложных случаях можно сочетать несколько подходов к настройке:

// 📁 /libs/http-client/src/provider.ts
import {Provider, InjectionToken, inject} from '@angular/core';

// Feature flags for optional functionality
export enum HttpFeatures {
  Interceptors = 'interceptors',
  Caching = 'caching',
  Retry = 'retry',
}

// Configuration interfaces
export interface HttpConfig {
  baseUrl?: string;
  timeout?: number;
  headers?: Record<string, string>;
}

export interface RetryConfig {
  maxAttempts: number;
  delayMs: number;
}

// Internal tokens
const HTTP_CONFIG = new InjectionToken<HttpConfig>('http.config');
const RETRY_CONFIG = new InjectionToken<RetryConfig>('retry.config');
const HTTP_FEATURES = new InjectionToken<Set<HttpFeatures>>('http.features');

// Core service
class HttpClientService {
  private config = inject(HTTP_CONFIG, {optional: true});
  private features = inject(HTTP_FEATURES);

  get(url: string) {
    // Use config and check features
  }
}

// Feature services
class RetryInterceptor {
  private config = inject(RETRY_CONFIG);
  // Retry logic
}

class CacheInterceptor {
  // Caching logic
}

// Main provider function
export function provideHttpClient(config?: HttpConfig, ...features: HttpFeature[]): Provider[] {
  const providers: Provider[] = [
    {provide: HTTP_CONFIG, useValue: config || {}},
    {provide: HTTP_FEATURES, useValue: new Set(features.map((f) => f.kind))},
    HttpClientService,
  ];

  // Add feature-specific providers
  features.forEach((feature) => {
    providers.push(...feature.providers);
  });

  return providers;
}

// Feature configuration functions
export interface HttpFeature {
  kind: HttpFeatures;
  providers: Provider[];
}

export function withInterceptors(...interceptors: any[]): HttpFeature {
  return {
    kind: HttpFeatures.Interceptors,
    providers: interceptors.map((interceptor) => ({
      provide: INTERCEPTOR_TOKEN,
      useClass: interceptor,
      multi: true,
    })),
  };
}

export function withCaching(): HttpFeature {
  return {
    kind: HttpFeatures.Caching,
    providers: [CacheInterceptor],
  };
}

export function withRetry(config: RetryConfig): HttpFeature {
  return {
    kind: HttpFeatures.Retry,
    providers: [{provide: RETRY_CONFIG, useValue: config}, RetryInterceptor],
  };
}

// Consumer usage with multiple features
bootstrapApplication(App, {
  providers: [
    provideHttpClient(
      {baseUrl: 'https://api.example.com'},
      withInterceptors(AuthInterceptor, LoggingInterceptor),
      withCaching(),
      withRetry({maxAttempts: 3, delayMs: 1000}),
    ),
  ],
});

Зачем использовать функции-поставщики вместо прямой настройки?

Функции-поставщики дают авторам библиотек несколько преимуществ:

  1. Инкапсуляция — внутренние токены и детали реализации остаются закрытыми
  2. Типобезопасность — TypeScript проверяет правильность конфигурации во время компиляции
  3. Гибкость — функции легко объединять с помощью шаблона with*
  4. Устойчивость к изменениям — внутреннюю реализацию можно изменить, не нарушая работу пользователей библиотеки
  5. Единообразие — соответствует собственным шаблонам Angular (provideRouter, provideHttpClient и т. д.)

Этот шаблон широко используется в собственных библиотеках Angular и считается рекомендуемым подходом для авторов библиотек, которым нужно предоставлять настраиваемые сервисы.

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/di/defining-dependency-providers

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API