今天開始    今天完成
 
今天目標:完成會員報告
前次已經將會員報告 store 的部分完成,接下來我們來修改跟報告有關的元件 (components) 跟服務,在修改之前,我們先來做一件事,還記得當初我們設定會員模組時的路由設定
src/app/member/member-routing.module.ts
//...省略
const routes: Routes = [
   {
       path: '',
       children: [
           { path: 'report-list', component: ReportListComponent },
           { path: 'report/:rptId', component: ReportComponent },
           { path: '', redirectTo: 'report-list', pathMatch: 'full' }
       ]
   }
]
//... 省略
這些路徑下的元件實際上都會用到狀態樹裡的報告,我們希望確定在使用者進入這些元件前,報告已經從後端存到我們的 store 中, Angular 的路由防護 (Route Guard),很適合這種用途,因為沒有報告的話,進入這些元件也就沒有意義,寫法跟之前的 Auth.guard.ts 類似
第一步:建立目錄及檔案在 src/app/member 下,順便註冊到 member 模組
mkdir guards
cd guards
ng generate guard report --module member
第二步:修改 report.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import * as fromStore from '../../store';
import { of } from 'rxjs/observable/of';
import { tap, take, switchMap, catchError } from 'rxjs/operators';
@Injectable()
export class ReportGuard implements CanActivate, CanActivateChild {
   constructor(
       private store: Store<fromStore.State>
   ) { }
   canActivate(
       next: ActivatedRouteSnapshot,
       state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
       return this.checkStore().pipe(
           switchMap(() => of(true)),
           catchError(() => of(false))
       );
   }
   canActivateChild(
       next: ActivatedRouteSnapshot,
       state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
       return this.checkStore().pipe(
           switchMap(() => of(true)),
           catchError(() => of(false))
       );
   }
   // helper function
   checkStore(): Observable<boolean> {
       return this.store.select(fromStore.getReports)
           .pipe(
           tap(res => {
               if (res.length === 0) {
                   this.store.dispatch(new fromStore.getReportAction());
               }
           }),
           switchMap(res => of(res.length !== 0)),
           take(1)
           );
   }
}
checkStore(),先看看 store 中的報告存在嗎?狀態樹中的報告會初始為空陣列,如果陣列是空的,分派一個動作 store.dispatch(new fromStore.getReportAction()),透過 repot.effect 經過 report.service 從後端拿資料take(1) 函數返回canActivate 跟 canActivateChild 直接呼叫 checkStore()
第三步: 修改報告服務 src/app/member/services/reports.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { AppConfig } from '../../share';
import { Report, Response } from '../../models';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class ReportsService {
    constructor(
        private appConfig: AppConfig,
        private http: HttpClient
    ) {
    }
    // get report from server
    getReportsFromServer(): Observable<Response> {
        return this.http.get<Response>(this.appConfig.apiUrl + '/reports');
    }
}
跟之前使用者服務一樣,刪除所有跟狀態有關的變數跟函數,只留下跟後端連結的部分
第四步:因為我們現在的報告狀態直接在 StoreModule.forRoot 下,也就是程式一開始就會初始的部分,而這個部分會用到報告服務(report.service),也就是我們需要將報告服務從會員模組移到 app.module.ts 下。
另外一種選擇則是在會員模組下使用 StoreModule.forFeature() 將報告的部分載入,這時候報告模組就會完整的存在會員模組中,而一旦載入會員模組,這個部分的狀態也會接回原本的狀態樹中,但篇幅關係,我們先省略這個部分,直接將報告服務改到 app.module.ts 下,並從 member.module.ts 下刪除
src/app/app.module.ts
//...省略
import { ReportsService } from './member/services/reports.service';
//...
@NgModule({
    //... 省略
    providers: [
        AuthGuard,
        UtilsService,
        ReportsService,
        StartupService,
    //... 省略
最後來修改元件
src/app/member/report-list/report-list.component.ts
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Report } from '../../models';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import * as fromStore from '../../store';
@Component({
    selector: 'app-report-list',
    templateUrl: './report-list.component.html',
    styleUrls: ['./report-list.component.css'].
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReportListComponent implements OnInit {
    reports$: Observable<Report[]>;
    constructor(
        private store: Store<fromStore.State>
    ) { }
    ngOnInit() {
        this.reports$ = this.store.select(fromStore.getReports);
    }
    onClick(report: Report) {
        this.store.dispatch(new fromStore.Go({ path: ['/member/report', report.id] }));
    }
}
ChangeDetectionStrategy,將 changeDection 從 .Default 改為 .OnPush,官方文件,這篇文章可以參考 ANGULAR CHANGE DETECTION EXPLAINED 這個意思是當 Angular 載入一個元件時,有一個複雜的流程判斷它以下的 DOM 或者子元件有沒有變化,有變化的話要隨時更新虛擬 DOM, 如果我們告訴 Angular,不用檢查了,我的資料來自於 Observable 的 push,那會節省很多時間來做載入的動作,而現在我們的資料都來自於 store ,所以我們告訴 Angular 不用幫我檢查this.report$ 直接指向 store.select(fromStore.getReports)
store.dispatch(new fromStore.Go()) 移轉路徑到報告下修改 src/app/member/report/report.component.ts
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Report } from '../../models';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import * as fromStore from '../../store';
@Component({
    selector: 'app-report',
    templateUrl: './report.component.html',
    styleUrls: ['./report.component.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ReportComponent implements OnInit {
    report$: Observable<Report>;
    constructor(
        private store: Store<fromStore.State>
    ) { }
    ngOnInit() {
        this.report$ = this.store.select(fromStore.getSelectedReport);
    }
}
ChangeDetectionStrategy 變為 .OnPush selector 做的 Join 狀態 store.select(fromStore.getSelectedReport)
如之前規劃,我們將 store 導入之後,所有元件的資料來自於 store,而服務則專門對應後端
當然就像之前講的,這樣的架構比較適合中大型的專案,畢竟需要很多額外的程式碼,但是帶來的好處,就像之前提的
很可惜篇幅關係,我們沒有談到單元測試的部分,也沒有談到 store forFeature 的做法,讓我先休息一下,可能的話會在我的部落格 將這個部分做個整理