はじめに
Angularで、1回の画面表示で複数のAPIを呼び出したい時がそれなりにある。
また、エラー処理に関する情報が少ない気がするので、エラー処理も含めて、どのようにするのがよいのか整理してみた。
Angularのバージョンは6.1.0(RxJS 6)。
前提
呼び出すAPIは次の形式とする。
export class XxxService {
api1(): Observable<Entity1>;
api2(key: string): Observable<Entity2>;
}
api1
を呼び出した後、 api2
を呼び出す想定。
また、API呼び出し中の画面は全画面ブロックではなく、部分的にローディング中メッセージ(もしくはアイコン)を表示する想定。
2つのAPIが独立な場合
もし api1
の戻り値を使用せず api2
を呼び出せるのであれば、単純に別々に呼び出せばよい。
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
result1: Observable<Entity1 | { error: any }>;
result2: Observable<Entity2 | { error: any }>;
constructor(private xxxService: XxxService) { }
ngOnInit() {
this.result1 = this.xxxService.api1().pipe(
catchError(e => of({ error: e }))
);
this.result2 = this.xxxService.api2('key2').pipe(
catchError(e => of({ error: e }))
);
}
}
上記のように catchError
でエラー内容を Observable
に変換しておくと、画面側で「正常」「エラー」「読み込み中」の表示切替が可能になる。
<ng-container *ngIf="result1 | async; let result1; else loading">
<ng-container *ngIf="!result1.error else result1Error">
{{result1}}
<ng-container>
<ng-template #result1Error>
result1の取得に失敗しました。詳細:{{result1.error}}
</ng-template>
</ng-container>
<ng-container *ngIf="result2 | async; let result2; else loading">
<ng-container *ngIf="!result2.error else result2Error">
{{result2}}
<ng-container>
<ng-template #result2Error>
result2の取得に失敗しました。詳細:{{result2.error}}
</ng-template>
</ng-container>
<ng-template #loading>
読み込み中
</ng-template>
2つのAPIが同期的動作を行う場合
api1
の呼び出し後、その結果を用いて api2
を呼び出す必要がある場合、 pipe
でつなげて処理する。
画面表示に2つ目のAPIの結果のみが必要な場合
flatMap
を用いて、 api2
の結果を取得する。
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
result2: Observable<Entity2 | { error: any }>;
constructor(private xxxService: XxxService) { }
ngOnInit() {
this.result2 = this.xxxService.api1().pipe(
flatMap(result1 => this.xxxService.api2(result1.key)),
// api1とapi2のエラー処理
catchError(e => of({ error: e }))
);
}
}
api1
で発生したエラーも api2
で発生したエラーも、1つの catchError
で処理することになるため、エラーの形式は揃えておく必要がある(通常は規約で揃っているはず)。
もしエラー処理を別々に行いたい場合は、いったん subscribe
する。
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
result2: Observable<Entity2 | { error: any }>;
constructor(private xxxService: XxxService) { }
ngOnInit() {
this.xxxService.api1().subscribe(
result1 => {
this.result2 = this.xxxService.api2(result1.key).pipe(
// api2のエラー処理
catchError(e => of({ error: e }))
);
},
// api1のエラー処理
e => console.error(e)
);
}
}
2つのAPIの結果を画面側で表示する場合
変数の型をObservableにしない場合
先の例で、 subscribe
で得られる result1
を変数に入れればよい。
api1
と api2
のエラーを個別に処理したい場合も、この方法での実装になる。
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
result1: Entity1;
result2: Observable<Entity2 | { error: any }>;
constructor(private xxxService: XxxService) { }
ngOnInit() {
this.xxxService.api1().subscribe(
result1 => {
this.result1 = result1;
this.result2 = this.xxxService.api2(result1.key).pipe(
// api2のエラー処理
catchError(e => of({ error: e }))
);
},
// api1のエラー処理
e => console.error(e)
);
}
}
2つの結果をObservableで受け取る場合
forkJoin
を用いて結合する。エラー処理が共通化できるのであれば、コードはすっきりする。
もちろん、 api1
でエラーが発生した場合は、 api2
は実行されない。
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
result: Observable<any>;
constructor(private xxxService: XxxService) { }
ngOnInit() {
this.result = this.xxxService.api1().pipe(
flatMap(result1 => forkJoin(of(result1), this.xxxService.api2(result1.key))),
// api1とapi2のエラー処理
catchError(e => of({ error: e }))
);
}
}
ただ、上記のように実装すると、結果は配列で取得されるため、テンプレート側ではインデックスで参照することになる。
<ng-container *ngIf="result | async; let result; else loading">
<ng-container *ngIf="!result.error else resultError">
{{result[0]}}<br>
{{result[1]}}
<ng-container>
<ng-template #resultError>
取得に失敗しました。詳細:{{result.error}}
</ng-template>
</ng-container>
<ng-template #loading>
読み込み中
</ng-template>
終わりに
全画面ローディングを入れずにエラー処理も入れるとなると、一工夫必要。
もっと良いやり方があるかもしれないため、ぜひ情報をいただければ。