今天的目標:完成網站實例(使用服務)
為了提供元件一份報告的詳細內容,我們在報告服務增加一項 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
第一步:修改 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);
}
}
ReportsService
, ActivatedRoute
, ParamMap
ActivateRoute
可以取得目前路徑,我們在前一篇 定義 member-routing.module.ts 時,定義過 { path: 'report/:rptId', component: ReportComponent }
,這個地方我們可以用 this.route.snapshot.paramMap.get('rptId')
來取得目前的 report id第二步: 修改 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 稍加美化,這時由前面報告摘要元件,點入一個摘要,就會導入如以下截圖
到這裡簡單的報告篇大致完工,我們回頭來更新一些連結
第一步:更新瀏覽列 (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 {
//...
/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)
);
}
}
UserService
跟 Router
CanActivate
跟 CanLoad
兩個介面,CanActivate
是給一般的 path
使用,我們要保護整個會員區域,因為它使用延遲加載 (lazy loading),所以會用到 CanLoad
loginStatus$
它會是 UserService
的目前登入狀態checkLogin()
,這裡用 rxjs
5.5 建議使用的 pipe()
運算子,詳情請看官方文件,再用 tap
運算子,這個運算子其實是以前的 .do
用來 "監聽" 資料,它本身不會對資料做改變,但是當我們 "監聽" 到未登入資料時,我們用 this.router.navigate(['user/login'])
來導向登入畫面,用運算子 take(1)
來完成資料取得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 會出現以下截圖
好了,我們網站大致完成,當然在實際的網站,我們還要加入後台管理,像是報告的 CRUD (Create, Read, Update, Delete)等等,但是基本上架構已經架好,只要在後端,會員模組加入這些功能,當然還要加上資料庫,我們就先到此為止。
回顧一下,到目前很簡單功能網站的資料流,當然這種情況下,維護系統還不是太難
我們可以想像,當專案越來越大,它有一天可能會變成,甚至更複雜
這時候,牽一髮而動全身,專案裡面的人可能沒有人有辦法指出修改某一項服務,會造成的影響,我們接下來的時間裡,要將這個網站改造,變成下面
讓元件跟服務分開,資料的流動是單方向的,示意圖如下
我想這樣大家也會更了解為什麼要用 ngrx/store 了