title: 你或許看過四不像,但是你聽過四很像嗎?:NgRx
date: 2018-06-25
categories: Training
keywords:


img

前言

別太在意包裝上的照片與實物之間的差異:RxJS 我們透過原始碼可以了解 RxJS 對於監控對象提供了3種狀態(nexterrorcomplete)的通知功能。
img
我們可以透過時做 Observer 介面來決定3種狀態發生時要做什麼處理,而 Subscriber 除了可以決定要做什麼事情之外,還可以決定要如何通知別人,所以如果我們不需要提供通知功能只要實作 Observer 介面即可。
那 NgRx 是什麼東西?在做什麼事?上網搜尋得到的答案大概就是 Angular 版的 Redux Pattern。
NgRx = Redux + RxJS + Angular
同時應該也會看到許多類似下圖的圖解。
img

Store

NgRx 官方頁面 可以看到需多功能,其中 Store 大概是最常用也是最實用的功能,因此本篇就以略懂 RxJS 的角度來看看 NgRx 的 Store 在做什麼。

安裝

我們開啟 @ngrx/store 說明頁面,來看看它的教學,首先就是安裝套件,我們可以在專案目錄下透過下列指令來安裝:
npm install @ngrx/store

建立 Reducer

接著建立一個 Reducer 函式-counterReducer,由範例可以大概了解它會傳入2個參數 stateaction,並action.type 回傳處理後的 state 值
{% codeblock counter.ts lang:ts %}
import { Action } from ‘@ngrx/store’;

export const INCREMENT = ‘INCREMENT’;
export const DECREMENT = ‘DECREMENT’;
export const RESET = ‘RESET’;

const initialState = 0;

export function counterReducer(state: number = initialState, action: Action) {
switch (action.type) {
case INCREMENT:
return state + 1;

case DECREMENT:
  return state - 1;

case RESET:
  return 0;

default:
  return state;

}
}
{% endcodeblock %}

註冊 StoreModule

接著我們可以看到它在 AppModule 內透過 StoreModule.forRoot 將 StoreModule 註冊成 SharedModule,並同時帶入 counterReducer。

StoreModule 提供了 forRootforFeature 2種方法。
參考資料:使用 forRoot() 幫助 SharedModule 提供單一實例服務 - Poy Chang

{% codeblock app.module.ts lang:ts %}
import { NgModule } from ‘@angular/core’;
import { StoreModule } from ‘@ngrx/store’;
import { counterReducer } from ‘./counter’;

@NgModule({
imports: [StoreModule.forRoot({ count: counterReducer })],
})
export class AppModule {}
{% endcodeblock %}

使用

最後我們從使用範例上可以看到它做了幾件事情:

{% codeblock my-counter.component.ts lang:ts %}
import { Component } from ‘@angular/core’;
import { Store, select } from ‘@ngrx/store’;
import { Observable } from ‘rxjs’;
import { INCREMENT, DECREMENT, RESET } from ‘./counter’;

interface AppState {
count: number;
}

@Component({
selector: ‘app-my-counter’,
template: <button (click)="increment()">Increment</button> <div>Current Count: { { count$ | async } }</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button> ,
})
export class MyCounterComponent {
count$: Observable;

constructor(private store: Store) {
this.count$ = store.pipe(select(‘count’));
}

increment() {
this.store.dispatch({ type: INCREMENT });
}

decrement() {
this.store.dispatch({ type: DECREMENT });
}

reset() {
this.store.dispatch({ type: RESET });
}
}

{% endcodeblock %}

流程

我們重新在整理一下,我們實作了一個 counterReducer 並將它註冊到 NgRx 的 Store 內,接著我們會在 Component 上來呼叫它。
img
我們在點擊 Increment 按鈕時透過 Store 的 dispatch 方法傳送一個 type: INCREMENT 的 Action 物件。
img

從 NgRx 原始碼可以得知 Action 是一個只有 type 屬性的介面。
{% codeblock my-counter.component.ts lang:ts %}
export interface Action {
type: string;
}
{% endcodeblock %}

Store 內部會自行找到我們所建立的 counterReducer 並將 Action 值傳遞給它。
img
接著執行 counterReducer 函式,依照所傳入的 action.type 做處理並回傳新的 State。
img
Store 會接收到 counterReducer 函式所回傳的 State。
img
在 Component 上 透過 Store.select 方法來取得 State 目前所需要的資料。
img

原始碼

我們可以從 GitHub 網站下載 NgRx 原始碼,查看 \modules\store\src\store.ts 可以看到 NgRx 的 Store 類別實作了 RxJS 的 Observer 介面並繼承 Observable 類別,而 ActionsSubject 底層就是透過 BehaviorSubject 來包裝 Action,最特別的就是範例所使用的 dispatch 方法其實就是呼叫 next 方法
Store 對應 Observer 介面的3個方法(nexterrorcomplete)也只觸發 BehaviorSubject 所對應的方法,本質上比較像 Subscriber,因為它並沒有決定要做什麼事。
{% codeblock store.ts lang:ts %}
@Injectable()
export class Store extends Observable implements Observer {
constructor(
state$: StateObservable,
private actionsObserver: ActionsSubject,
private reducerManager: ReducerManager
) {
super();
this.source = state$;
}

dispatch(action: V) {
this.actionsObserver.next(action);
}

next(action: Action) {
this.actionsObserver.next(action);
}

error(err: any) {
this.actionsObserver.error(err);
}

complete() {
this.actionsObserver.complete();
}
}

export class ActionsSubject extends BehaviorSubject implements OnDestroy {

}
{% endcodeblock %}

select 就是 RxJS Operator

接著觀察 select 方法相關程式碼,如果有研究過 RxJS Operator 的原始碼應該可以發現 select 就是 NgRx 自己實作的 RxJS Operator,我們可以看到最關鍵的 selectOperator 函式:

{% codeblock store.ts lang:ts %}
@Injectable()
export class Store extends Observable implements Observer {
lift(operator: Operator<T, R>): Store {
const store = new Store(this, this.actionsObserver, this.reducerManager);
store.operator = operator;

return store;

}

select(
pathOrMapFn: ((state: T) => any) | string,
…paths: string[]
): Observable {
return select.call(null, pathOrMapFn, …paths)(this);
}
}

export function select<T, K>(
pathOrMapFn: ((state: T) => any) | string,
…paths: string[]
) {
return function selectOperator(source$: Observable): Observable {
let mapped$: Observable;

if (typeof pathOrMapFn === 'string') {
  mapped$ = source$.pipe(pluck(pathOrMapFn, ...paths));
} else if (typeof pathOrMapFn === 'function') {
  mapped$ = source$.pipe(map(pathOrMapFn));
} else {
  throw new TypeError(
    `Unexpected type '${typeof pathOrMapFn}' in select operator,` +
      ` expected 'string' or 'function'`
  );
}

return mapped$.pipe(distinctUntilChanged());

};
}
{% endcodeblock %}

腦補時間

雖然還沒仔細研究底層運作,但是就目前認知,大概可以推想出可能的運作模式,NgRx 會透過 RxJS 的 Subject 來監看 actionstate,我們可以透過 dispatch 方法來觸發 Subject(負責監看 Action) 的 next 方法,Store 會透過訂閱,再發生 next 時幫我們去呼叫我們所撰寫的 Reducer 方法,接著將回傳值透過 next 塞入 Subject(負責監看 State)內,最後 NgRx 再對外提供一個已經訂閱 Subject(負責監看 State) 的 Select Operator,而我們則透過訂閱這個 Subject 來取得對應 Action 的資料。
img

四很像

看過 NgRx Store 的程式碼後聯想起漫畫 HUNTER×HUNTER 內雲古對小傑說的一句話。
img
將所有學到的技能一次同時展現就會變成很厲害的絕招,沒錯,Store 可以說是將 RxJS 的分散的功能都整合在一起,我們可以將 Component 內的操作都建立對應的 Action,並將相關邏輯處理都包再 Reducer 內,這樣整個 class 就會更加乾淨。
還有別忘了 Store 同時也是個 SharedModule,只要畫面上所有有訂閱的 Component 都可以即時收到通知,例如將登入、登出封裝到 Store,我們在登入成功後將使用者資訊儲存到 State 內,這樣有訂閱的 Component 就可以檢查權限是否足夠,相對的,收到登出通知時 Component 可以決定是否關閉畫面。