toSignal
函數的來源是一個 Observable
,當滿足錯誤條件時,Observable
會拋出錯誤。 Observable
拋出錯誤後,每當讀取 signal
時都會拋出錯誤。對於開啟錯誤模式對話方塊 (error modal dialog) 的 global error handler 來說,這種行為可能是不受歡迎的。當讀取 signal
時,會引發錯誤,global error handler 會在非終止循環中開啟錯誤模式對話方塊。 幸運的是,toSignal
提供了 rejectErrors
選項來拒絕所有錯誤,以打破這個意外的循環。
我將在這篇文章中展示 Observable 和 toSignal 中的錯誤處理行為。他們是:
Observable
在拋出錯誤後停止產生值,而 AsyncPipe
顯示最後一個成功的值。toSignal
錯誤處理的預設行為。 讀取 signal
時會引發錯誤。 如果應用程式有全域錯誤處理程序 (global error handler),則該處理程序會接收錯誤並進行處理。toSignal
函數傳遞 rejectErrors
選項,以將錯誤傳回給 RxJS。 RxJS 捕獲錯誤,就好像它們是 uncaught exception 一樣。 然後,signal
顯示最後一次成功的值,與 AsyncPipe
相同。Observable
使用 catchError
運算子捕獲錯誤並傳回一個新的 Observable。例如,錯誤處理程序可以執行邏輯來顯示錯誤對話方塊。// error-dialog.component.ts
import { ChangeDetectionStrategy, Component, computed, ElementRef, output, viewChild } from '@angular/core';
@Component({
selector: 'app-error-dialog',
standalone: true,
template: `
<dialog #dialog class="overlay">
<ng-content>Error occurs</ng-content>
<div>
<button autofocus (click)="closeClicked.emit()">Close</button>
</div>
</dialog>`,
})
export default class ErrorDialogComponent {
closeClicked = output();
dialog = viewChild.required('dialog', { read: ElementRef<HTMLElement>});
nativeDialog = computed(() => this.dialog().nativeElement);
open() {
this.nativeDialog().showModal();
}
close() {
this.nativeDialog().close();
}
}
ErrorDialogComponent
由一個對話框 (dialog) 組成,該對話框以程式設計方式開啟和關閉模式。此元件有一個輸出,當按一下關閉按鈕時,該輸出會向父元件發送事件。
// error-token.constant.ts
import { InjectionToken, WritableSignal } from '@angular/core';
export const ERROR_DIALOG_TOKEN = new InjectionToken<{ show: WritableSignal<boolean> }>('ERROR_DIALOG_TOKEN');
export const appConfig = {
providers: [
{
provide: ERROR_DIALOG_TOKEN,
useValue: { show: signal(false) }
}
]
}
ERROR_DIALOG_TOKEN
是一個注入令牌 (injection token),它是一個 { show: WritableSignal<boolean> }
物件。 當訊號值為 true 時,將開啟錯誤對話框。當訊號值為 false 時,錯誤對話方塊將關閉。在 appConfig
中,我將 ERROR_DIALOG_TOKEN
注入到 { show: signal(false) }
中,以便錯誤對話框保持隱藏狀態。
import { ErrorHandler, inject, Injectable } from "@angular/core";
import { ERROR_DIALOG_TOKEN } from "./error-token.constant";
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
modal = inject(ERROR_DIALOG_TOKEN);
handleError(error: any) {
console.error('in GlobalErrorHandler', error);
this.modal.show.set(true);
}
}
export const appConfig = {
providers: [
{
provide: ErrorHandler,
useClass: GlobalErrorHandler,
},
]
}
GlobalErrorHandler
是一個集中式錯誤處理程序,用於處理應用程式的錯誤處理邏輯。在 GlobalErrorHandler
中,我注入 ERROR_DIALOG_TOKEN
以取得對 { show: WritableSignal<boolean> }
的引用。 show
切換值以開啟和關閉錯誤對話方塊。在 appConfig
中,我將 ErrorHandler
注入 GlobalErrorHandler
類別 (class)。
讓我們來解釋一下 Observable 和 toSignal 的錯誤處理。
// async-pipe-error.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { BehaviorSubject, scan, map } from 'rxjs';
import { AsyncPipe } from '@angular/common';
@Component({
selector: 'app-async-pipe-error',
standalone: true,
imports: [AsyncPipe],
template: `
<div>
<p>total: {{ total$ | async }}</p>
</div>
<button (click)="something.next(1)">Add</button>
<button (click)="something.next(-1)">Subtract</button>
`,
})
export default class AsyncPipeErrorComponent {
something = new BehaviorSubject(0);
total$ = this.something.pipe(
scan((acc, v) => acc + v, 0),
map((v) => {
if (v === 5) {
throw new Error('throw an async pipe error');
}
return v;
}),
);
}
AsyncPipeErrorComponent
元件由一個 BehaviorSubject
, something, 組成,初始值為0。total$
是一個 Observable,它會累積值,當值等於 5 時拋出錯誤。 當累計值小於5時,值輸出成功。 在 HTML 範本中, Add 按鈕向 BehaviorSubject 發出1,而 Subtract 按鈕則向 BehaviorSubject 發出-1。此外,我將 AsyncPipe
匯入到 imports
以解析 $total
Observable 並顯示該值。當 Observable
拋出錯誤時,AsyncPipe
將錯誤傳回給 RxJS。因此,AsyncPipe
顯示自 Observable
停止產生值以來最後一個成功的值。
// default-errors.componen.ts
import { ChangeDetectionStrategy, Component, effect, inject, viewChild } from '@angular/core';
import { BehaviorSubject, map, scan } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import ErrorDialogComponent from '../errors/error-dialog.component';
import { ERROR_DIALOG_TOKEN } from '../errors/error-token.constant';
@Component({
selector: 'app-default-errors-example',
standalone: true,
imports: [ErrorDialogComponent],
template: `
<div>
<p>total: {{ total() }}</p>
</div>
<button (click)="something.next(1)">Add</button>
<button (click)="something.next(-1)">Subtract</button>
<app-error-dialog (closeClicked)="modal.show.set(false)">
<p>Error is thrown and the global error handler handles it.</p>
</app-error-dialog>`,
})
export default class DefaultErrorsComponent {
modal = inject(ERROR_DIALOG_TOKEN);
errorDialog = viewChild.required(ErrorDialogComponent);
something = new BehaviorSubject(0);
#total$ = this.something.pipe(
scan((acc, v) => acc + v, 0),
map((v) => {
if (v === 5) {
throw new Error('Error is thrown and the global error handler handles it');
}
return v;
}),
);
total = toSignal(this.#total$);
constructor() {
effect(() => {
if (this.modal.show()) {
console.log('in effect, open dialog');
this.errorDialog().open();
} else if (!this.modal.show()) {
console.log('in effect, close dialog');
this.errorDialog().close();
}
})
}
}
DefaultErrorsComponent
元件由一個BehaviorSubject, something, 組成,初始值為0。#total$
是一個 Observable,它會累積值並在累積值等於 5 時拋出錯誤。 當累計值小於5時,值輸出成功。 toSignal
函數從 #total$
建立 signal 並將結果指派給 total
。最後,HTML 範本顯示 total
的值。
GlobalErrorHandler
類別攔截錯誤並呼叫 handlerErro
r 方法來更新 show
訊號的值。
DefaultErrorsComponent
元件的 constructor 有一個 effect
比較 show
訊號的值。當值為 true 時,將開啟錯誤對話框,否則,將關閉錯誤對話框。但是, modal.show().set(false)
會觸發 change detection,並讀取 total
。 當 Observable 拋出錯誤時,total
訊號會讀取錯誤,導致 GloblErrorHandler
將 show
訊號設為 true 並重新開啟錯誤對話方塊。 最終,元件陷入重新開啟錯誤對話方塊、關閉它、然後重新開啟它的循環。
這可能不是預期的行為,我可以透過傳遞 rejectErrors
屬性來修復它。
// reject-errors.component.ts
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import { BehaviorSubject, catchError, map, scan, throwError } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
import ErrorDialogComponent from '../errors/error-dialog.component';
@Component({
selector: 'app-reject-errors-example',
standalone: true,
imports: [ErrorDialogComponent],
template: `
<div>
<p>total: {{ total() }}</p>
</div>
<button (click)="something.next(1)">Add</button>
<button (click)="something.next(-1)">Subtract</button>
<app-error-dialog (closeClicked)="this.errorDialog().close()">
<p>Error Dialog only opens once.</p>
</app-error-dialog>`,
})
export default class RejectErrorsComponent {
errorDialog = viewChild.required(ErrorDialogComponent);
something = new BehaviorSubject(0);
#total$ = this.something.pipe(
scan((acc, v) => acc + v, 0),
map((v) => {
if (v === 5) {
throw new Error('throw a rejectErrors error');
}
return v;
}),
catchError((e) => {
this.errorDialog().open();
return throwError(() => e);
})
)
total = toSignal(this.#total$, { initialValue: 0, rejectErrors: true });
}
RejectErrorsComponent
元件與 DefaultRejectComponent
元件類似,只是向 toSignal
函式傳遞了 rejectErrors
屬性。 當 #total$
Observable 拋出錯誤時,catchError
運算子捕獲錯誤,打開錯誤對話框,並重新拋出錯誤。 當 Observable 拋出錯誤時, total
訊號將其重新拋出給RxJS。 RxJS 將錯誤捕獲為 uncaught exception,並且 Observable 不會產生新值。 錯誤對話方塊打開,使用者將其關閉。 當按一下按鈕向 BehaviorSubject 發出值時,Observable 不執行任何操作, total
訊號會讀取最後一個成功值 4 並顯示它。因此,錯誤對話方塊不會再次開啟。
// catch-error.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { BehaviorSubject, catchError, of, scan, concatMap, throwError } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-catch-error',
standalone: true,
template: `
<div>
<p>total: {{ total() }}</p>
</div>
<button (click)="something.next(1)">Add</button>
<button (click)="something.next(-1)">Subtract</button>
`,
})
export default class CatchErrorComponent {
something = new BehaviorSubject(0);
total$ = this.something.pipe(
scan((acc, v) => acc + v, 0),
concatMap((v) => {
if (v === 5) {
return throwError(() => new Error('throw an error')).pipe(
catchError((e) => {
console.error(e);
return of(-5);
})
)
}
return of(v);
}),
)
total = toSignal(this.total$, { initialValue: 0 });
}
CatchErrorComponent
元件與其他元件的區別在於它使用 concatMap
運算子傳回內部 Observable。 當累加值不為 5 時,Observable 會傳回累加值。當累加值為5時,concatMap
呼叫 throwError
運算子拋出錯誤。然後,catchError
運算子會擷取錯誤、記錄它並傳回適當的值 -5。由於內部 Observable 處理了錯誤,因此 total$
Observable 不會意外完成。當點擊按鈕將下一個值泵送到 BehaviourSubject 時,它可以發出新值。
鐵人賽的第 35 天到此結束