Моя идея состоит в том, что у меня будет служба глобального обработчика ошибок, которая перехватывает все неперехваченные ошибки и предоставляет ошибку как наблюдаемую для любых заинтересованных компонентов. Я хочу подписаться на наблюдаемое из моего компонента приложения, и если есть ошибка, показать страницу ошибки.
//error-service.ts
@Injectable({
providedIn: 'root',
})
export class ErrorService implements ErrorHandler {
error = new Subject<Error>();
handleError(error: any): void {
console.error('ERROR', error);
this.error.next(error);
}
}
Этот ErrorService зарегистрирован как глобальный обработчик ошибок для модуля приложения.
//app.module.ts
@NgModule({
providers: [{ provide: ErrorHandler, useClass: ErrorService }],
//...
})
export class AppModule {}
ErrorService внедряется в компонент приложения.
//app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
error = this.errorService.error;
constructor(private errorService: ErrorService) {}
}
Шаблон компонента приложения отображает страницу ошибки, если есть ошибка, в противном случае отображает выход маршрутизатора.
//app.component.html
<app-error-page *ngIf = "error | async"></app-error-page>
<router-outlet *ngIf = "!(error | async)"></router-outlet>
Однако это не работает. Глобальный обработчик ошибок действительно перехватывает неперехваченные ошибки, поэтому он правильно зарегистрирован, и метод handleError()
работает, однако компонент приложения не выполняет повторную визуализацию своего шаблона.
Если я отправлю новую ошибку в тему ErrorService.error
из другого компонента, а не из глобального обработчика ошибок (ErrorService.handleError()
), тогда это сработает — компонент приложения повторно отображает свой шаблон и показывает страницу ошибки.
У вас есть два разных экземпляра ErrorService
в вашем приложении.
Эта строка providers: [{ provide: ErrorHandler, useClass: ErrorService }],
настраивает поставщика в AppModule с именем зависимости ErrorHandler, который использует класс ErrorService
для создания этой зависимости.
А вот эта другая строчка в сервисе @Injectable({ providedIn: 'root'})
, чем-то похожа на добавление другого провайдера в rootModule с этим конфигом { provide: ErrorService , useClass: ErrorService }
Таким образом, вы получаете 2 провайдера в вашем rootModule, которые используют класс ErrorService
, каждый с другим именем InjectionToken или зависимостью.
providers: [
{ provide: ErrorHandler, useClass: ErrorService },
{ provide: ErrorService , useClass: ErrorService }
],
ErrorService
и использует его в качестве основного ErrorHandler приложения.AppComponent
, вы запрашиваете зависимость, настроенную с этим именем. Таким образом, система DI в этом случае выберет второго поставщика и внедрит другой экземпляр ErrorService
.IMO для вашего случая самое простое решение — вручную добавить второго провайдера в AppModule, который повторно предоставляет существующий ErrorHandler
под псевдонимом ErrorService.
providers: [
{ provide: ErrorHandler, useClass: ErrorService },
{ provide: ErrorService, useExisting: ErrorHandler }, // <---
],
Таким образом, когда какой-либо элемент запрашивает ErrorService, этот провайдер вернет уже созданный экземпляр службы для ErrorHandler.
В этом случае я также уберу { providedIn: 'root' }
в сервисе, чтобы лучше отразить поведение, ожидаемое от сервиса.
Ваше здоровье
Первая проблема с рассматриваемым кодом заключается в том, что
providers: [{ provide: ErrorHandler, useClass: ErrorService }],
регистрирует новый экземпляр ErrorService
как реализацию ErrorHandler
, поэтому, когда я внедрил ErrorService
в компонент приложения, я получил экземпляр, отличный от экземпляра, зарегистрированного для глобальной обработки ошибок.
Однако этой проблемы на самом деле не существовало в моем исходном коде — я представил эту проблему при упрощении кода, чтобы задать этот вопрос.
Строка выше должна быть изменена на
providers: [{ provide: ErrorHandler, useExisting: ErrorService }],
так что реализация ErrorHandler
использует тот же ErrorService
, который уже зарегистрирован как сервис.
Даже с этим исправлением оно не работает — подписка на Subject<Error>
в компоненте приложения получает новые ошибки, но представление не перерисовывается, поскольку Angular не обнаруживает, что что-то изменилось. Поэтому мне пришлось явно сообщить Angular об изменениях, используя NgZone.run().
//app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
error?: Error;
constructor(
private errorService: ErrorService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone
) {}
ngOnInit(): void {
this.errorService.error.subscribe((error) => {
this.error = error;
this.ngZone.run(() => {
this.changeDetectorRef.detectChanges();
});
});
}
}
Попробуйте использовать объект behaviorSubject или replaySubject вместо обычной темы.