使用 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
- 都使用
@Injectable
decorator - 都是回傳
Observable
相異點 :
- Observable data service 內部會使用
BehaviorSubject
儲存一份資料;而普通 service 不會
- 原本只有
AppComponent
,現在分成ChangeCounterComponent
與ShowCounterComponent
2 個 component - Component 依然注入
CounterService
,不過此時不是普通 service,而是 Observable Data Service,為 2 個 component 共用的資料 - 根據
依賴反轉原則
,component 不直接相依於CounterService
,而是兩者相依於 interface;根據介面隔離原則
,component 只相依於它所需要的 interface,因此訂出ChangeCounterInterface
與ShowCounterInterface
2 個以 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上找到