Как избежать дублирования экземпляров сервисов в 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
в случаях использования:
- Ленивой загрузки модулей
- Инжекты в модулях потомках
Это происходит потому, что Angular создает новый модуль Injector
для любого лениво загруженного модуля, это поведение отлично описано в документации и в этой статье.
Вот демо с демонстрацией проблемы.
Решения
Здесь нужно понять самое важное - добавление любого Injectable (или InjectionToken) в список @NgModule.providers
для любой пары модулей Eager и Lazy будет дублировать Injectable
!
Итак, первый шаг - не добавлять сервисы, которые должны быть одиночными, в список @NgModule.providers любого модуля.
По сути, вы можете добавить сервис к поставщикам прикладного модуля, и он будет работать. Но другие разработчики могут не знать, что вы хотите использовать этот сервис в качестве одиночного, и кто-то добавит этот сервис в список поставщиков загруженного модуля, и Angular создаст его второй экземпляр.
Вы должны выбрать, какую стратегию использовать, потому что есть два с ее плюсами и минусами:
- Метод @NgModule
static forRoot()
@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
Присоединяйся в тусовку
В этом месте могла бы быть ваша реклама
Разместить рекламу