今天開始 今天完成
今天目標:完成會員報告
前次已經將會員報告 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 的做法,讓我先休息一下,可能的話會在我的部落格 將這個部分做個整理