iT邦幫忙

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

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

[ngrx/store-24] Angular 網站實例 - 會員報告篇

Angular 網站實例 - 會員報告篇

今天開始 今天完成

今天的目標:完成網站實例(使用服務)

修改報告服務

為了提供元件一份報告的詳細內容,我們在報告服務增加一項 getReport(id:number) 函數如下
src/app/member/services/reports.service.ts

//... 省略
  getReport(id: number): Observable<Report> {
        return this.reports$.pipe(
            map(reports => {
                return reports.filter(report => report.id === id)[0];
            })
        );
    }

這個函數很簡單的將目前的 reports,用陣列運算子 .filter()取得跟參數 id 一樣的報告,因為 .filter() 會回覆一個陣列,我們取一個回覆,這個回覆還是一個 Observable

報告元件 (report.component)

第一步:修改 src/app/member/report/report.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';

import { ReportsService } from '../services/reports.service';
import { Report } from '../../models';
import { Observable } from 'rxjs/Observable';

@Component({
    selector: 'app-report',
    templateUrl: './report.component.html',
    styleUrls: ['./report.component.css']
})
export class ReportComponent implements OnInit {

    report$: Observable<Report>;
    constructor(
        private reportService: ReportsService,
        private route: ActivatedRoute
    ) { }

    ngOnInit() {
        let id = this.route.snapshot.paramMap.get('rptId');
        this.report$ = this.reportService.getReport(+id);
    }
}
  1. 加入 ReportsService, ActivatedRoute, ParamMap
  2. ActivateRoute 可以取得目前路徑,我們在前一篇 定義 member-routing.module.ts 時,定義過 { path: 'report/:rptId', component: ReportComponent },這個地方我們可以用 this.route.snapshot.paramMap.get('rptId') 來取得目前的 report id
  3. 利用上面增加的服務來取得報告

第二步: 修改 report.component.html

<div class="container" *ngIf="(report$ | async) as report">
  <header class="center"><h3>{{report.master}}</h3></header>
  <section>
    <img alt="report image" src="{{report.image}}" class="image"/>
  </section>

  <section>
    <div class="title">{{ report.title }}</div>
    <p>{{report.report}}</p>
  </section>
</div>

async pipe 取訂閱 report$,其他用很簡單的方式來顯示報告內容

第三步:修改 report.component.css

.container {
  max-width: 700px;
  margin-right: auto;
  margin-left: auto;

}
header {
  margin: 10px auto;
  text-align: center;
}
section {
  margin-top: 50px;
  padding-left: 20px;
  padding-right: 20px;
}
.image {
  display:block;
  width: 100%;
  height: 100%;
  margin: auto;
  box-sizing: border-box;
}
.title {
  font-size: 20px;
  font-weight: 800;
}

用 css 稍加美化,這時由前面報告摘要元件,點入一個摘要,就會導入如以下截圖
https://ithelp.ithome.com.tw/upload/images/20180109/20103574KeCxtJHjVN.png

到這裡簡單的報告篇大致完工,我們回頭來更新一些連結

更新瀏覽列以及使用者登入

第一步:更新瀏覽列 (navbar) src/app/navbar/navbar.component.html

//...省略
  <a mat-button *ngIf="!(login$| async)" routerLink="/user/login">會員登入</a>
  <a mat-button *ngIf="login$ | async" routerLink="/member">會員中心</a>
  <button *ngIf="user$ | async " mat-button [matMenuTriggerFor]="userMenu">
    <span>{{user$ | async}}</span> <mat-icon>arrow_drop_down</mat-icon>
  </button>      
//... 省略

將會員中心的 routerLink 指向 /member
第二步:更新使用者登入 src/app/user/login/login.component.ts,我們在會員成功登入後,頁面導向 '/member'

//... 省略
import { Router } from '@angular/router';
//...
  constructor(
        private fb: FormBuilder,
        private userService: UserService,
        private router: Router,
        private snackbar: MatSnackBar
    ) { }
//...
   login() {
        this.userService.login(this.form.value)
            .subscribe(res => {
                if (res) {
                    this.snackbar.open('登入成功', 'OK', { duration: 3000 });
                    this.router.navigate(['/member']);
                } else {
    //... 
  1. 加入 Router
  2. 在登入成功後導向 /member,使用 this.router.navigate(['/member'])

增加路由防護

在登出的狀態下,如果使用者手工打入 http://localhost:4200/member ,會出現空白頁,因為我們將後端鎖住,不讓未登入的要求取得報告,這樣對使用者經驗 (UX) 不太好,我們希望在使用者進入會員的區域時,如果是未登入,將頁面導向登入畫面,讓會員執行登入
第一步:增加一個目錄 src/app/guards

mkdir guards

第二步:用 angular-cli 增加一個 guard,順便註冊到 app.module.ts

ng generate guard auth --module app -d

第三步:修改 auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanLoad, Router, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import { UserService } from '../user/service/user.service';
import { tap } from 'rxjs/operators';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {
    loginStatus$: Observable<boolean>
    constructor(
        private userService: UserService,
        private router: Router
    ) {
        this.loginStatus$ = userService.getLoginStatus();
    }
    canActivate(
        next: ActivatedRouteSnapshot,
        state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
        let url: string = state.url;
        return this.checkLogin(url);
    }
    canLoad(route: Route): Observable<boolean> {
        let url = `/${route.path}`;
        return this.checkLogin(url);
    }
    checkLogin(url: string): Observable<boolean> {
        return this.loginStatus$.pipe(
            tap(status => {
                if (!status) {
                    this.router.navigate(['user/login']);
                }
            }),
            take(1)
        );
    }
}
  1. 導入 UserServiceRouter
  2. 這個 class 要 implement CanActivateCanLoad 兩個介面,CanActivate 是給一般的 path 使用,我們要保護整個會員區域,因為它使用延遲加載 (lazy loading),所以會用到 CanLoad
  3. 建立一個變數 loginStatus$ 它會是 UserService 的目前登入狀態
  4. 建一個函數 checkLogin(),這裡用 rxjs 5.5 建議使用的 pipe() 運算子,詳情請看官方文件,再用 tap 運算子,這個運算子其實是以前的 .do 用來 "監聽" 資料,它本身不會對資料做改變,但是當我們 "監聽" 到未登入資料時,我們用 this.router.navigate(['user/login']) 來導向登入畫面,用運算子 take(1) 來完成資料取得
  5. CanActivate()CanLoad() 呼叫這個函數,回應 this.loginStatus$

第四步:修改 app-routing.module.ts

//... 省略
import { AuthGuard } from './guards/auth.guard';
const routes: Routes = [{
    path: '',
    children: [
        { path: 'home', component: HomeComponent },
        { path: 'user', redirectTo: '/user', pathMatch: 'full' },
        { path: 'member', loadChildren: './member/member.module#MemberModule', canLoad: [AuthGuard] },
        { path: '', redirectTo: '/home', pathMatch: 'full' },
    ]
}];
//... 省略

第五步:檢查一下 app.module.ts,angular-cli 有時會在註冊 service 發生錯誤

//... 省略
import { AuthGuard } from './guards/auth.guard';

@NgModule({
    //... 省略
       providers: [
        AuthGuard,
        UtilsService,
        StartupService,
        {
            provide: APP_INITIALIZER,
            useFactory: startupServiceFactory,
            deps: [StartupService, Injector],
            multi: true,
        },
//...省略

測試一下,在未登入狀態下,手工打入 http://localhost:4200/member 會出現以下截圖
https://ithelp.ithome.com.tw/upload/images/20180109/20103574A9Law1nmq9.png

專案回顧

好了,我們網站大致完成,當然在實際的網站,我們還要加入後台管理,像是報告的 CRUD (Create, Read, Update, Delete)等等,但是基本上架構已經架好,只要在後端,會員模組加入這些功能,當然還要加上資料庫,我們就先到此為止。

回顧一下,到目前很簡單功能網站的資料流,當然這種情況下,維護系統還不是太難
https://ithelp.ithome.com.tw/upload/images/20180109/20103574f06ewOBYxf.jpg

我們可以想像,當專案越來越大,它有一天可能會變成,甚至更複雜
https://ithelp.ithome.com.tw/upload/images/20180109/20103574Q5ayktDNak.jpg

這時候,牽一髮而動全身,專案裡面的人可能沒有人有辦法指出修改某一項服務,會造成的影響,我們接下來的時間裡,要將這個網站改造,變成下面
https://ithelp.ithome.com.tw/upload/images/20180109/20103574xelWsErYEP.jpg

讓元件跟服務分開,資料的流動是單方向的,示意圖如下
https://ithelp.ithome.com.tw/upload/images/20180109/20103574UlFStSPGnR.jpg

我想這樣大家也會更了解為什麼要用 ngrx/store 了


上一篇
[ngrx/store-23] Angular 網站實例 - 會員報告摘要篇
下一篇
[ngrx/store-25] ngrx/store 設定篇及會員篇之 Action, Reducer
系列文
ngrx/store 4 學習筆記30

尚未有邦友留言

立即登入留言