ローダーと通信エラーの実装
Angularアプリのローダーと通信時エラーの実装について。
(Angular 5.0.1を利用)
ローディング
ローダーの種類
- 通信時につねに表示するもの (A)
- 通信時に画面をブロックしたいもの (B)
Aの実装方針
HTTP_INTERCEPTORS
を利用して自動で実行させる
http-interceptor-loader.ts
@Injectable() export class HttpInterceptorLoader implements HttpInterceptor { constructor(private store: Store<formRoot.AppState>) {} intercept( req: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { // 今回はngrxを利用 // load開始時と終了時にカウントの増減をする this.store.dispatch(new LoadCountAdded()); return next.handle(req).pipe( finalize(() => { setTimeout(() => this.store.dispatch(new LoadCountRemoved()), 0); }) ); } }
actionとstate
/** * action */ export class LoadCountAdded implements Action { readonly type = LOAD_COUNT_ADDED; // `withBlocker`があるのは B タイプのローダーでも利用するため constructor(public withBlocker = false) { } } export class LoadCountRemoved implements Action { readonly type = LOAD_COUNT_REMOVED; constructor(public withBlocker = false) { } }
/** * state */ const initialState: State = { loadCount: { withBlocker: 0, withoutBlocker: 0, }, };
stateをsubscribe
export class AppComponent { constructor(private store: Store<fromRoot.AppState>) { // http://ricostacruz.com/nprogress/を利用 NProgress.configure({ showSpinner: false }); this.store.select(fromRoot.getProgressNeeded) .subscribe(needed => { needed ? NProgress.start() : NProgress.done(); } ); } }
Bの実装方針
- 必要なタイミングでdispatchすればいい
利用例
@Effect() load$: Observable<any> = this.actions$ .ofType<actions.LoadData>(actions.LOAD_DATA) .map(action => action.payload) .switchMap(key => { // `withBlocker`フラグをtrueにする this.store.dispatch(new layout.LoadCountAdded(true)); return this.service.getData(key) .catch(() => { // TODO }) .finally( () => this.store.dispatch(new layout.LoadCountRemoved(true)) ); });
stateをsubscribe
// app-blockerというブロッカーコンポーネントの表示非表示をコントロールする @Component({ selector: 'app-root', template: `<router-outlet></router-outlet> <app-blocker *ngIf="blockerNeeded$ | async"></app-blocker>`, styles: [], }) export class AppComponent { blockerNeeded$: Observable<boolean>; constructor(private store: Store<fromRoot.AppState>) { this.blockerNeeded$ = this.store.select(fromRoot.getBlockerNeeded); } }
BlockerComponent
@Component({ selector: 'app-blocker', template: ` // bootstrap4のクラス // やっていること -> 中央にコンテンツを配置 <div class="d-flex justify-content-center align-items-center h-100"> <app-spinner size="lg"></app-spinner> </div> `, styles: [` :host { position: fixed; // 状況に応じて変更 // z-index: 1000; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255,255,255,.38); }`] }) export class BlockerComponent { }
通信エラー
実装方針
- エラー表示ロジックを実装した
HttService
をつくる HttService
ではエラー表示ロジックのみ実装し、エラーの解決はしない- エラーの解決は利用状況により異なることが想定されるので、各Feature Serviceにまかせる
@Injectable() export abstract class HttpService { protected apiUrl = environment.apiUrl; constructor( protected http: HttpClient, private injector: Injector, ) { } // resolve cyclic dependency get ngbModal(): NgbModal { return this.injector.get(NgbModal); } protected handleError(err: HttpErrorResponse | Error): ErrorObservable { const modalRef = this.ngbModal.open(NgbmAlertComponent, { backdrop: 'static' }); // エラー処理 if (err instanceof Error || err.error instanceof Error) { const msg = err instanceof Error ? err.message : err.error.message; // A client-side or network error occurred Object.assign(modalRef.componentInstance, { title: 'Error', message: `An error occurred: ${err.message}` }); } else { // The backend returned an unsuccessful response code. Object.assign(modalRef.componentInstance, { title: 'Error', message: `Backend returned code ${err.status}, message was: ${err.message}` }); } return Observable.throw(err); } }
利用例
@Injectable() export class ApiService extends HttpService { getHero(): Observable<Hero> { return this.http .get(`${this.apiUrl}/hero.json`) .pipe( map(json => /* ... */), catchError(err => this.handleError(err)) ); } }