iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 30
0
Modern Web

ngrx/store 4 學習筆記系列 第 30

[ngrx/store-30] ngrx/store 完成篇

ngrx/store 完成篇

今天開始 今天完成
 
今天目標:完成會員報告
前次已經將會員報告 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)
           );
   }
}
  1. 協議建立 CanActivate 跟 CanActivateChild 介面
  2. constructor 導入 store
  3. 建立一個幫忙函數 checkStore(),先看看 store 中的報告存在嗎?狀態樹中的報告會初始為空陣列,如果陣列是空的,分派一個動作 store.dispatch(new fromStore.getReportAction()),透過 repot.effect 經過 report.service 從後端拿資料
  4. 等後端(非同步)拿到資料後 take(1) 函數返回
  5. 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] }));
    }
}
  1. 導入 Store,刪除報告跟 Router 服務
  2. 加入 ChangeDetectionStrategy,將 changeDection.Default 改為 .OnPush官方文件,這篇文章可以參考 ANGULAR CHANGE DETECTION EXPLAINED 這個意思是當 Angular 載入一個元件時,有一個複雜的流程判斷它以下的 DOM 或者子元件有沒有變化,有變化的話要隨時更新虛擬 DOM, 如果我們告訴 Angular,不用檢查了,我的資料來自於 Observable 的 push,那會節省很多時間來做載入的動作,而現在我們的資料都來自於 store ,所以我們告訴 Angular 不用幫我檢查
  3. this.report$ 直接指向 store.select(fromStore.getReports)
  4. 當使用者點擊一份摘要時,用 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);
    }
}
  1. 一樣導入 Store 刪除服務
  2. 加入 ChangeDetectionStrategy 變為 .OnPush 
  3. 將報告指向我們之前在 selector 做的 Join 狀態 store.select(fromStore.getSelectedReport)

結語

如之前規劃,我們將 store 導入之後,所有元件的資料來自於 store,而服務則專門對應後端
https://ithelp.ithome.com.tw/upload/images/20180115/20103574SZZjPpQcVR.jpg

當然就像之前講的,這樣的架構比較適合中大型的專案,畢竟需要很多額外的程式碼,但是帶來的好處,就像之前提的

  1. 可追蹤:因為有了Chrome extention 的 Redux DevTool,您可以清楚了解狀態改變的流程,以進行對可能存在的程式錯誤做除錯。
  2. 增加效能:對一些 Component 的 ChangeDetectionStrategy 為 OnPush
  3. 易於測試:因為 Reducer 是純函數,便於測試

很可惜篇幅關係,我們沒有談到單元測試的部分,也沒有談到 store forFeature 的做法,讓我先休息一下,可能的話會在我的部落格 將這個部分做個整理


上一篇
[ngrx/store-29] ngrx/store 之報告篇
系列文
ngrx/store 4 學習筆記30

尚未有邦友留言

立即登入留言