ローダーと通信エラーの実装

f:id:ryotah:20171214221323p:plainf:id:ryotah:20171214221325p:plain

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))
      );
  }
}