DevGang
Авторизоваться

Как избежать дублирования экземпляров сервисов в Angular

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

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

Введение

Если вы хотите создать только один экземпляр какого-либо сервиса для всего приложения, вы должны создать Singleton.

Почему вы можете этого хотеть?

Наиболее распространенной причиной является разделение некоторого ценного состояния между всеми частями вашего приложения.

Взгляните на простой сервис настройки приложений:

@Injectable()
export class SettingsService {
  private settings = new Map();

  public get(key: string): any {
    return this.settings.get(key);
  }

  public set(key: string, value: any): any {
    return this.settings.set(key, value);
  }
}

и этот модуль:

@NgModule({
  imports: [BrowserModule],
  declarations: [ApplicationComponent],
  bootstrap: [ApplicationComponent],
  providers: [SettingsService]
})
export class AppModule {}

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

@Component({
  selector: 'app',
  template: ''
})
class ApplicationComponent {
  constructor(private settings: SettingsService) {
    settings.set('FEATURE', true);
  }
}

А затем использовать его в каком-то компоненте:

this.isFeatureAvailable = settings.get('FEATURE');
...

Но иногда Angular может создавать более одного экземпляра SettingsService, поэтому ваши настройки будут различаться в зависимости от экземпляра, и это приведет к серьезным проблемам конфигурации в вашем приложении.

Что ж, давайте посмотрим, почему это происходит и как с этим справиться.

Проблема

Angular создаст новые экземпляры для любого из InjectionToken или Injectable в случаях использования:

  1. Ленивой загрузки модулей
  2. Инжекты в модулях потомках

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

Вот демо с демонстрацией проблемы.

Решения

Здесь нужно понять самое важное - добавление любого Injectable (или InjectionToken) в список @NgModule.providers для любой пары модулей Eager и Lazy будет дублировать Injectable!

Итак, первый шаг - не добавлять сервисы, которые должны быть одиночными, в список @NgModule.providers любого модуля.

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

Вы должны выбрать, какую стратегию использовать, потому что есть два с ее плюсами и минусами:

  1. Метод @NgModule static forRoot()
  2. @Injectable({ providedIn: ‘root’ })

forRoot()

Метод forRoot является своего рода соглашением между разработчиками Angular для вызова этого метода только в корневом модуле (например AppModule), поэтому любая служба будет предоставляться только один раз.

Для этой техники вы должны создать модуль и реализовать метод static forRoot(): ModuleWithProviders.

Пример:

@NgModule({
  imports: [CommonModule]
})
export class SettingsModule {
  public static forRoot(): ModuleWithProviders {
    return {
      ngModule: SettingsModule,
      providers: [SettingsService]
    };
  }
}

Примечание: метод forRoot не обрабатывается никаким кодом в Angular Compiler, поэтому вы можете называть его как хотите (forMySuperHotRootAppModule()), но это не рекомендуется.

Хороший пример использования forRoot это RouterModule.

Вот демо с решением forRoot.

providedIn: ‘root’

Когда вы пометите как Injectable предоставленный в корне, Angular resolver будет знать, что таковой Injectable, используемый в ленивом модуле, был добавлен в корневой модуль, и будет искать его в корневом инжекторе, а не во вновь созданном ленивом загруженном модуле (поведение по умолчанию).

@Injectable({
  providedIn: 'root'
})
export class SettingsService {
  private settings = new Map();

  public get(key: string): any {
    return this.settings.get(key);
  }

  public set(key: string, value: any): any {
    return this.settings.set(key, value);
  }
}

Огромный плюс этого решения - возможность Angular использовать обход деревьев с помощью providedIn.

Вот демо с решением providedIn.

Пример с синглтоном

Вы можете легко узнать, создал ли кто-то второй экземпляр вашего сервиса.

@Injectable({
  providedIn: 'root'
})
export class GuardedSingletonService {
  constructor(@Optional() @SkipSelf() parent?: GuardedSingletonService) {
    if (parent) {
      throw Error(
        `[GuardedSingletonService]: trying to create multiple instances,
        but this service should be a singleton.`
      );
    }
  }
}

Также это можно сделать как базовый класс:

export class RootInjectorGuard {
  constructor(type: Type) {
    const parent = inject(type, InjectFlags.Optional | InjectFlags.SkipSelf);

    if (parent) {
      throw Error(`[${type}]: trying to create multiple instances,
      but this service should be a singleton.`);
    }
  }
}

Использование:

@Injectable({
  providedIn: 'root'
})
export class MySingletonService extends RootInjectorGuard {
  constructor() {
    super(MySingletonService);
  }
}

Бонус

Вот несколько вопросов, на которые я искал ответы, когда столкнулся с этой проблемой.

Как справиться с этим с InjectionToken?

У InjectionToken есть второй аргумент.

class MyDep {}

class MyService {
  constructor(readonly myDep: MyDep) {}
}

const MyServiceToken = new InjectionToken('MyToken', {
  providedIn: 'root',
  factory: () => new MyService(inject(MyDep))
});

Что делать, если я использую forRoot в сочетании с providedIn: 'root'?

Нет различий между использованием: forRoot или providedIn или forRoot + providedIn. Сервис будет создан только один раз.

Что делать, если я использую forRoot и список поставщиков?

Сервис будет продублирован.

Что делать, если я использую providedIn и список поставщиков?

Сервис будет продублирован.

#Angular
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу