[聚合文章] 使用 Observable Data Service 儲存 Component 間共用變數

Angular 2018-01-07 17 阅读

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
  • 都使用 @Injectable decorator
  • 都是回傳 Observable

相異點 :

  • Observable data service 內部會使用 BehaviorSubject 儲存一份資料;而普通 service 不會

  • 原本只有 AppComponent ,現在分成 ChangeCounterComponentShowCounterComponent 2 個 component
  • Component 依然注入 CounterService ,不過此時不是普通 service,而是 Observable Data Service,為 2 個 component 共用的資料
  • 根據 依賴反轉原則 ,component 不直接相依於 CounterService ,而是兩者相依於 interface;根據 介面隔離原則 ,component 只相依於它所需要的 interface,因此訂出 ChangeCounterInterfaceShowCounterInterface 2 個以 component 為需求的 interface,而 CounterService 必須實踐此 2 個 interface。如此 component 與 service 將徹底解耦合
  • ShowCounterComponent 只負責 subscribe CounterService ;當資料改變時, 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 需求訂出 ChangeCounterInterfaceShowCounterInterface ,因此 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 所有的特性, SubjectBehaviorSubject 都有,如被 subscribe()

SubjectBehaviorSubject 算是一種特殊的 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$;

已經都自己維護一份 BehaviorSubjectstore$ ,我們希望當 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

完整的範例可以在我的 StackBlitzGitHub 上找到

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。