使用 Observable Data Service 儲存 Component 間共用變數
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

- 共用 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
1 | <app-change-value></app-change-value> |
ChangeCounterComponent
change-counter.component.html
1 | <button (click)="onIncrementClick()">+</button> |
只負責 增加 與 減少 counter 的 component,因此只有兩個 <button>。
change-counter.component.ts
1 | import { Component } from '@angular/core'; |
13 行
1 | onIncrementClick() { |
使用 CounterService 提供的 addOne() 將 counter + 1。
17 行
1 | onDecrementClick() { |
使用 CounterService 提供的 minusOne() 將 counter - 1;
11 行
1 | constructor(private counterService: ChangeCounterInterface) { } |
因為 onIncrementClick() 與 onDecrementClick() 都使用了 CounterService,因此需要 DI 注入 CounterService。
根據 依賴反轉原則,我們不應該直接相依於 CounterService,而應該相依於根據 ChangeCounterComponent 需求所定義出的 ChangeCounterInterface。
change-counter.interface.ts
1 | export abstract class ChangeCounterInterface { |
根據 介面隔離原則,因為 ChangeCounterComponent 只使用了 addOne() 與 minusOne(),因此 ChangeCounterInterface 也應該只有 addOne() 與 minusOne()。
ShowCounterComponent
show-counter.component.html
1 | {{ counter$|async }} |
只負責顯示 counter$ 的 component。
show-counter.component.ts
1 | import { Component } from '@angular/core'; |
11 行
1 | counter$: Observable<number> = this.counterService.counter$; |
counter$ 為 Observable,只接使用 CounterService.counter$ property。
13 行
1 | constructor(private counterService: ShowCounterInterface) { } |
因為 ShowCounterComponent 只用了 CounterService,因此需要 DI 注入 CounterService。
根據 依賴反轉原則,我們不應該直接相依於 CounterService,而應該相依於根據 ShowCounterComponent 需求所定義出的 ShowCounterInterface。
show-counter.interface.ts
1 | import { Observable } from 'rxjs/Observable'; |
根據 介面隔離原則,因為 ShowCounterComponent 只使用了 counter$ property,因此 ShowCounterInterface 也應該只有 counter$ property。
CounterService
counter.service.ts
1 | import { Injectable } from '@angular/core'; |
第 8 行
1 | export class CounterService implements ChangeCounterInterface, ShowCounterInterface |
根據 介面隔離原則,我們依照 component 需求訂出 ChangeCounterInterface 與 ShowCounterInterface,因此 CounterService 必須概括承受實現這些 interface。
第 9 行
1 | private store$: BehaviorSubject<number> = new BehaviorSubject<number>(0); |
本文關鍵在此,使用 BehaviorSubject 當作 CounterService 內部的資料庫,儲存共用的 counter。
Observable Data Service 關鍵就在於使用 BehaviorSubject。
Q : 什麼是 BehaviorSubject ?
根據 RxJS 的 source code
BehaviorSubject.ts
1 | export class BehaviorSubject<T> extends Subject<T> |
BehaviorSubject 繼承於 Subject。
Subject.ts
1 | 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
1 | const subject: Subject = new Subject(); |
若你要手動將資料推進 Observable,就使用 Subject
Q : Subject 與 BehaviorSubject 有何差別 ?
Subject不能提供初始值,但BehaviorSubject可提供初始值Subject在被subscribe()後,必續再next()才能收到變動資料;BehaviorSubject只要被subscribe()後,就可收到之前最後一筆資料,不用再等next()BehaviorSubject提供了getValue(),能獲得目前BehaviorSubject最新一筆資料;而Subject無法
1 | const subject: BehaviorSubject = new BehaviorSubject('Hello World'); |
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 行
1 | addOne(): void { |
使用 this.store$.getValue() 獲得目前 store$ 最新一筆資料,也就是目前的 counter 值,然後 +1。
最後使用 this.store$.next() 將最新的 counter 寫入 BehaviorSubject。
16 行
1 | minusOne(): void { |
使用 this.store$.getValue() 獲得目前 store$ 最新一筆資料,也就是目前的 counter 值,然後 -1。
最後使用 this.store$.next() 將最新的 counter 寫入 BehaviorSubject。
10 行
1 | 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
1 | import { BrowserModule } from '@angular/platform-browser'; |
由於我們 component 與 service 都是基於 依賴反轉原則 與 介面隔離原則 所設計,因此必須藉由 DI container 幫我們將相對應的 service 注入。
19 行
1 | providers: [ |
一般我們使用普通 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
完整的範例可以在我的 GitHub上找到