Component 間共用的資料有兩類,一類是來自 API 的資料,將來還會寫回 server,另一類是 component 間自己的狀態資料,不必寫回 server;事實上這種不必寫回 server 的共用資料,也可以使用 Observable Data Service 實作。
Version
Node.js 8.9.3
Angular CLI 1.6.2
Angular 5.1.2
RxJS 5.5.2
User Story
線上執行 : StackBlitz
- 共用 counter 初始值為
0 - 無論按哪個
+,或哪個-,最後都是同一個 counter 作用
Task
- 原來該有的功能必須保留,但拆成 2 個 component
- 但 2 個 component 會共用一份資料,且互相影響
Architecture
Observable Data Service vs. 普通 Service
相同點 :
- 兩者本質上都是 service
- 都使用
@Injectabledecorator - 都是回傳
Observable
相異點 :
- Observable data service 內部會使用
BehaviorSubject儲存一份資料;而普通 service 不會
- 原本只有
AppComponent,現在分成ChangeCounterComponent與ShowCounterComponent2 個 component - Component 依然注入
CounterService,不過此時不是普通 service,而是 Observable Data Service,為 2 個 component 共用的資料 - 根據
依賴反轉原則,component 不直接相依於CounterService,而是兩者相依於 interface;根據介面隔離原則,component 只相依於它所需要的 interface,因此訂出ChangeCounterInterface與ShowCounterInterface2 個以 component 為需求的 interface,而CounterService必須實踐此 2 個 interface。如此 component 與 service 將徹底解耦合 -
ShowCounterComponent只負責 subscribeCounterService;當資料改變時,CounterService會自動更新ShowCounterComponent
Implementation
AppComponent
app.component.html
<app-change-value></app-change-value> <p></p> <app-change-value></app-change-value> <p></p> <app-show-value></app-show-value>
ChangeCounterComponent
change-counter.component.html
<button (click)="onIncrementClick()">+</button> <button (click)="onDecrementClick()">-</button>
只負責 增加 與 減少 counter 的 component,因此只有兩個 <button> 。
change-counter.component.ts
import { Component } from '@angular/core';
import { ChangeCounterInterface } from '../../interface/change-counter.interface';
@Component({
selector: 'app-change-value',
templateUrl: './change-counter.component.html',
styleUrls: ['./change-counter.component.css']
})
export class ChangeCounterComponent {
constructor(private counterService: ChangeCounterInterface) { }
onIncrementClick() {
this.counterService.addOne();
}
onDecrementClick() {
this.counterService.minusOne();
}
}
13 行
onIncrementClick() {
this.counterService.addOne();
}
使用 CounterService 提供的 addOne() 將 counter + 1。
17 行
onDecrementClick() {
this.counterService.minusOne();
}
使用 CounterService 提供的 minusOne() 將 counter - 1;
11 行
constructor(private counterService: ChangeCounterInterface) { }
因為 onIncrementClick() 與 onDecrementClick() 都使用了 CounterService ,因此需要 DI 注入 CounterService 。
根據 依賴反轉原則 ,我們不應該直接相依於 CounterService ,而應該相依於根據 ChangeCounterComponent 需求所定義出的 ChangeCounterInterface 。
change-counter.interface.ts
export abstract class ChangeCounterInterface {
abstract addOne(): void;
abstract minusOne(): void;
}
根據 介面隔離原則 ,因為 ChangeCounterComponent 只使用了 addOne() 與 minusOne() ,因此 ChangeCounterInterface 也應該只有 addOne() 與 minusOne() 。
ShowCounterComponent
show-counter.component.html
{{ counter$|async }}
只負責顯示 counter$ 的 component。
show-counter.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ShowCounterInterface } from '../../interface/show-counter.interface';
@Component({
selector: 'app-show-value',
templateUrl: './show-counter.component.html',
styleUrls: ['./show-counter.component.css']
})
export class ShowValueComponent {
counter$: Observable<number> = this.counterService.counter$;
constructor(private counterService: ShowCounterInterface) { }
}
11 行
counter$: Observable<number> = this.counterService.counter$;
counter$ 為 Observable ,只接使用 CounterService.counter$ property。
13 行
constructor(private counterService: ShowCounterInterface) { }
因為 ShowCounterComponent 只用了 CounterService ,因此需要 DI 注入 CounterService 。
根據 依賴反轉原則 ,我們不應該直接相依於 CounterService ,而應該相依於根據 ShowCounterComponent 需求所定義出的 ShowCounterInterface 。
show-counter.interface.ts
import { Observable } from 'rxjs/Observable';
export abstract class ShowCounterInterface {
abstract counter$: Observable<number>;
}
根據 介面隔離原則 ,因為 ShowCounterComponent 只使用了 counter$ property,因此 ShowCounterInterface 也應該只有 counter$ property。
CounterService
counter.service.ts
import { Injectable } from '@angular/core';
import { ChangeCounterInterface } from '../../interface/change-counter.interface';
import { ShowCounterInterface } from '../../interface/show-counter.interface';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class CounterService implements ChangeCounterInterface, ShowCounterInterface {
private store$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
counter$: Observable<number> = this.store$;
addOne(): void {
this.store$.next(this.store$.getValue() + 1);
}
minusOne(): void {
this.store$.next(this.store$.getValue() - 1);
}
}
第 8 行
export class CounterService implements ChangeCounterInterface, ShowCounterInterface
根據 介面隔離原則 ,我們依照 component 需求訂出 ChangeCounterInterface 與 ShowCounterInterface ,因此 CounterService 必須概括城市實現這些 interface。
第 9 行
private store$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
本文關鍵在此,使用 BehaviorSubject 當作 CounterService 內部的資料庫,儲存共用的 counter 。
Observable Data Service 關鍵就在於使用 BehaviorSubject 。
Q : 什麼是 BehaviorSubject ?
根據 RxJS 的 source code
BehaviorSubject.ts
export class BehaviorSubject<T> extends Subject<T>
BehaviorSubject 繼承於 Subject 。
Subject.ts
export class Subject<T> extends Observable<T> implements ISubscription
而 Subject 繼承於 Observable 。
因此 Observable 所有的特性, Subject 與 BehaviorSubject 都有,如被 subscribe() 。
Subject 與 BehaviorSubject 算是一種特殊的 Observable ,提供一些原本 Observable 沒有的功能
Q : Observable 與 Subject 有何差別 ?
-
Observable只能從HttpClient或 array、event 提供資料,無法自行新增 -
Subject可以透過next()手動新增資料至 Observable
const subject: Subject = new Subject();
subject.subscribe((value) => {
console.log('Subscription got ', value);
});
subject.next('Hello World');
// Hello World
若你要手動將資料推進 Observable ,就使用 Subject
Q : Subject 與 BehaviorSubject 有何差別 ?
-
Subject不能提供初始值,但BehaviorSubject可提供初始值 -
Subject在被subscribe()後,必續再next()才能收到變動資料;BehaviorSubject只要被subscribe()後,就可收到之前最後一筆資料,不用再等next() -
BehaviorSubject提供了getValue(),能獲得目前BehaviorSubject最新一筆資料;而Subject無法
const subject: BehaviorSubject = new BehaviorSubject('Hello World');
subject.subscribe((value) => {
console.log('Subscription got ', value);
});
// Hello World
subject.next('Hello Taiwan')
// Hello Taiwan
Q : 為什麼 Observable Data Service 要使用 BehaviorSubject ?
因為 HTML 的 async pipe,每個 HTML template 執行 subscribe() 的時間不一致,有快有慢,若 subscribe() 早於 next() ,則可收到變動資料;若晚於 next() ,則可能收不到資料,必須等下一次 next() 。
若使用 BehaviorSubject ,無論 subscribe() 早於 next() 或晚於 next() , async pipe 都會獲得最新一筆 next() ,因此 Observable Data Service 必須使用 BehaviorSubject 。
12 行
addOne(): void {
this.store$.next(this.store$.getValue() + 1);
}
使用 this.store$.getValue() 獲得目前 store$ 最新一筆資料,也就是目前的 counter 值,然後 +1 。
最後使用 this.store$.next() 將最新的 counter 寫入 BehaviorSubject 。
16 行
minusOne(): void {
this.store$.next(this.store$.getValue() - 1);
}
使用 this.store$.getValue() 獲得目前 store$ 最新一筆資料,也就是目前的 counter 值,然後 -1 。
最後使用 this.store$.next() 將最新的 counter 寫入 BehaviorSubject 。
10 行
counter$: Observable<number> = this.store$;
已經都自己維護一份 BehaviorSubject 的 store$ ,我們希望當 BehaviorSubject 使用 next() 改變資料時,能通知所有 subscribe() 的 component 都能自動更新,這也是我們使用 Observable Data Service 的初衷。
既然 BehaviorSubject 繼承於 Observable ,理論上我們也可直接將 store$ 設定為 public ,直接被 component subscribe() ,但因為 BehaviorSubject 提供 next() ,因此也可能被 component 誤用 next() 而新增資料,因此我們改用真的 Observable 給 component 訂閱,確保 store$ 不會被 component 新增資料。
由於 store$ 也是一種 Observable ,根據 里式替換原則 ,父類別可被子類別取代,因此不需要特別傳型。
AppModule
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ChangeCounterComponent } from './component/change-counter/change-counter.component';
import { ShowValueComponent } from './component/show-counter/show-counter.component';
import { ChangeCounterInterface } from './interface/change-counter.interface';
import { CounterService } from './service/counter/counter.service';
import { ShowCounterInterface } from './interface/show-counter.interface';
@NgModule({
declarations: [
AppComponent,
ChangeCounterComponent,
ShowValueComponent
],
imports: [
BrowserModule
],
providers: [
CounterService,
{provide: ChangeCounterInterface, useExisting: CounterService},
{provide: ShowCounterInterface, useExisting: CounterService}
],
bootstrap: [AppComponent]
})
export class AppModule { }
由於我們 component 與 service 都是基於 依賴反轉原則 與 介面隔離原則 所設計,因此必須藉由 DI container 幫我們將相對應的 service 注入。
19 行
providers: [
CounterService,
{provide: ChangeCounterInterface, useExisting: CounterService},
{provide: ShowCounterInterface, useExisting: CounterService}
],
一般我們使用普通 service 時,都是每個 component 注入新的 service,但使用 Observable Data Service 時不可如此,因為我們就是希望各 component 共用一份資料,只要在 provider 使用 useExisting ,Angular DI container 就會以 Singleton 方式建立 service,各 component 才能共用同一份 store$ 的 counter。
Conclusion
- 只要是 component 間有共用資料,且彼此會根據資料互動,就可以使用 Observable Data Service
- Observable Data Service 同時使用了 DI 與 RxJS,讓 component 與 service 解耦合,只要 service 的資料更新,所有 component 的資料解會隨之更新
Sample Code
完整的範例可以在我的 StackBlitz 或 GitHub 上找到
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。