ngrx

ngrx/store

Store

Strore是Angular基於Rxjs的狀態管理,保存了Redux的核心概念,並使用RxJs擴展的Redux實現。使用Observable來簡化監聽事件和訂閱等操做。
在看這篇文章以前,已經假設你已瞭解rxjsredux
官方文檔 有條件的話,請查看官方文檔進行學習理解。javascript

安裝

npm install @ngrx/store

Tutorial

下面這個Tutorial將會像你展現如何管理一個計數器的狀態和如何查詢以及將它顯示在Angular的Component上。你能夠經過StackBlitz來在線測試。css

1.建立actionshtml

src/app/counter.actions.ts

import {Action} from '@ngrx/store';

export enum ActionTypes {
    Increment = '[Counter Component] Increment',
    Decrement = '[Counter Component] Decrement',
    Reset = '[Counter Component] Reset',
}

export class Increment implements Action {
    readonly type = ActionTyoes.Increment;
}

export class Decrement implements Action {
    readonly type = ActionTypes.Decrement;
}

export class Reset implements Action {
    readonly tyoe = Actiontypes.Reset;
}

2.定義一個reducer經過所提供的action來處理計數器state的變化。java

src/app/counter.reducer.ts

import {Action} from '@ngrx/store';
import {ActionTypes} from './conter.actions';

export const initailState = 0;

export function conterReducer(state = initialState, action: Action) {
    switch(action.type) {
        case ActionTypes.Increment:
            return state + 1;
        case ActionTypes.Decrement:
            return state - 1;
        case ActionTypes.Reset:
            return 0;
        default:
            return state;
    }
}

3.在src/app/app.module.ts中導入 StoreModule from @ngrx/storecounter.reducergit

import {StroeModule} from '@ngrx/store';
import {counterReducer} from './counter.reducer';

4.在你的AppModuleimports array添加StoreModule.forRoot,並在StoreModule.forRoot中添加count 和 countReducer對象。StoreModule.forRoot()函數會註冊一個用於訪問store的全局變量。github

scr/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
 
import { AppComponent } from './app.component';
 
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';

@NgModule({
    declaration: [AppComponent],
    imports: [
        BrowserModule,
        StoreModule.forRoot({count: countReducer})
    ],
    provoders: [],
    bootstrap: [AppComponent]
})
export class AppModule {}

5.在app文件夾下新建立一個叫my-counter的Component,注入Store service到你的component的constructor函數中,並使用select操做符在state查詢數據。更新MyCounterComponent template,添加添加、減小和重設操做,分別調用increment,decrement,reset方法。並使用async管道來訂閱count$ Observable。web

src/app/my-counter/my-counter.component.html

<button (click)="increment()">Increment</button>
 
<div>Current Count: {{ count$ | async }}</div>

<button (click)="decrement()">Decrement</button>

<button (click)="reset()">Reset Counter</button>

更新MyCounterComponent類,建立函數並分發(dispatch)Increment,Decrement和Reset actions.typescript

import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Increment, Decrement, Reset } from '../counter.actions';

@Component({
    selector: 'app-my-counter',
    templateUrl: './my-counter.component.html',
    styleUrls: ['./my-counter.component.css'],
})
export class MyCounterComponent (
    count$: Observable<number>;
    
    constructor(private store: Stare<{count: number}>) {
        this.count$ = store.pipe(select('count'));
    }
    
    increment() {
        this.store.dispatch(new Increment());
    }
    
    decrement() {
        this.store.dispatch(new Decrement());
    }
    
    reset() {
        this.store.dispatch(new Reset());
    }
)

6.添加MyCounter component到AppComponent template中npm

<app-my-counter></app-my-counter>

Actions

Actions是NgRx的核心模塊之一。Action表示在整個應用中發生的獨特的事件。從用戶與頁面的交互,與外部的網絡請求的交互和直接與設備的api交互,這些和更多的事件經過actions來描述。redux

介紹

在NgRx的許多地方都使用了actions。Actions是NgRx許多系統的輸入和輸出。Action幫助你理解如何在你的應用中處理事件。

Action接口(Action interface)

NgRx經過簡單的interface來組成Action

interface Action {
    type: string;
}

這個interface只有一個屬性:type,string類型。這個type屬性將描述你的應用調度的action。這個類型的值以[Source]的形式出現和使用,用於提供它是什麼類型的操做的上下文和action在哪裏被調度(dispatched)。您能夠向actions添加屬性,以便爲操做提供其餘上下文或元數據。最多見的屬性就是payload,它會添加action所需的全部數據。
下面列出的是做爲普通javascript對象編寫的操做的示例:

{
    type: '[Auth API] Login Success'
}

這個action描述了調用後端API成功認證的時間觸發。

{
    type: '[Login Page]',
    payload: {
        username: string;
        password: string;
    }
}

這個action描述了用戶在登陸頁面點擊登陸按鈕嘗試認證用戶的時間觸發。payload包含了登陸頁面提供的用戶名和密碼。

編寫 actions

有一些編寫actions的好習慣:

  • 前期——在開始開發功能以前編寫編寫action,以便理解功能和知識點
  • 分類——基於事件資源對actions進行分類
  • 編寫更多——action的編寫容易,因此你能夠編寫更多的actions,來更好的表達應用流程
  • 事件-驅動——捕獲事件而不是命令,由於你要分離事件的描述和事件的處理
  • 描述——提供針對惟一事件的上下文,其中包含可用於幫助開發人員進行調試的更詳細信息

遵循這些指南可幫助您瞭解這些actions在整個應用程序中的流程。
下面是一個啓動登錄請求的action示例:

import {} from '@ngrx/store';

export class Login Implements Action {
    readonly type = '[Login Page] Login'
    
    constructor(public: payload: {username: string, password: string}){}
}

action編寫成類,以便在dispatched操做時提供類型安全的方法來構造action。Login action 實現(implements) Action interface。在示例中,payload是一個包含username和password的object,這是處理action所需的其餘元數據.
在dispatch時,新實例化一個實例。

login-page.component.ts
click(username: string, password: string) {
    store.dispatch(new Login({username:username, password: password}))
}

Login action 有關於action來自於哪裏和事件發生了什麼的獨特上線文。

  • action的類型包含在[]內
  • 類別用於對特徵區域的action進行分組,不管他是組件頁面,後端api或瀏覽器api
  • 類別後面的Login文本是關於action發生了什麼的描述。在這個例子中,用戶點擊登陸頁面上的登陸按鈕來經過用戶名密碼來嘗試認證。

建立action unions

actions的消費者,不管是reducers(純函數)或是effects(帶反作用的函數)都使用actions的type來肯定是否要執行這個action。在feature區域,多個actions組合在一塊兒,可是每一個action都須要提供本身的type信息。看上一個Login action 例子,你將爲action定義一些額外的信息。

import {Action} from '@ngrx/store';

export enum ActionTypes {
    Login = '[Login Page] Login';
}

export class Login Implememts Action {
    readonly type = ActionTypes.Login;
    
    constructor(public paylad: {username: string, password: string})
}

export type Union = Login;

將action type string放在enum中而不是直接放在class內。此外,還會使用Union類去導出Loginclass.

Reducers

NgRx中的Reducers負責處理應用程序中從一個狀態到下一個狀態的轉換。Reducer函數從action的類型來肯定如何處理狀態。

介紹

Reducer函數是一個純函數,函數爲相同的輸入返回相同的輸出。它們沒有反作用,能夠同步處理每一個狀態轉化。每一個reducer都會調用最新的action,當前狀態(state)和肯定是返回最新修改的state仍是原始state。這個指南將會向你展現如何去編寫一個reducer函數,並在你的store中註冊它,並組成獨特的state。

關於reducer函數

每個由state管理的reducer都有一些共同點:

  • 接口和類型定義了state的特徵
  • 參數包含了初始state或是當前state、當前action
  • switch語句

下面這個例子是state的一組action,和相對應的reducer函數。
首先,定義一些與state交互的actions。

scoreboard-page.actions.ts

import {Action} from '@ngrx/store';

export enum Actiontypes {
    IncrementHome = '[Scoreboard Page] Home Score',
    IncrementAway = '[Scoreboard Page] Away Score',
    Reset = '[Scoreboard Page] Score Reset',
}

export class IncrementHome implements Action {
    readonly type = ActionTypes.IncrementHome;
}

export class IncrementAway implements Action {
    readonly type = ActionTypes.IncrementAway;
}

export class Reset implements Action {
    readonly type = ActionTypes.Reset;
    
    constructor(public payload: {home: number, away: number}) {}
}

export type ActionsUnion = IncrementHome | IncrementAway | Reset;

接下來,建立reducer文件,導入actions,並定義這個state的特徵。

定義state的特徵

每一個reducer函數都會監聽actions,上面定義的scorebnoard actions描述了reducer處理的可能轉化。導入多組actions以處理reducer其餘的state轉化。

scoreboard.reducer.ts

import * as Scoreboard from '../actions/scoreboard-page.actions';

export interface State {
    home: number;
    away: number;
}

根據你捕獲的內容來定義state的特徵,它是單一的類型,如number,仍是一個含有多個屬性的object。

設置初始state

初始state給state提供了初始值,或是在當前state是undefined時提供值。您可使用所需state屬性的默認值設置初始state。建立並導出變量以使用一個或多個默認值捕獲初始state。

scoreboard.reducer.ts

export const initialState: Satate = {
    home: 0,
    away: 0,
};

建立reducer函數

reducer函數的職責是以不可變的方式處理state的更變。定義reducer函數來處理actions來管理state。

scoreboard.reducer.ts

export function reducer {
    satate = initialState,
    action: Scoreboard.ActionsUnion
}: State {
    switch(action.type) {
        case Scoreboard.ActionTypes.IncrementHome: {
            return {
                ...state,
                home: state.home + 1,
            }
        }
        case Scoreboard.ActionTypes.IncrementAway: {
            return {
                ...state,
                away: state.away + 1,
            }
        }
        case Scoreboard.ActionTypes.Reset: {
            return action.payload;
        }
        default: {
            return state;
        }
    }
}

Reducers將switch語句與TypeScript在您的actions中定義的區分聯合組合使用,以便在reducer中提供類型安全的操做處理。Switch語句使用type union來肯定每種狀況下正在使用的actions的正確特徵。action的types定在你的action在你的reducer函數的case語句。type union 也約束你的reducer的可用操做。
在這個例子中,reducer函數處理3個actions:IncrementHome,IncrementAway,Reset。每一個action都有一個基於ActionUnion提供的強類型。每一個action均可以不可逆的處理state。這意味着state更變不會修改源state,而是使用spread操做返回一個更變後的新的state。spread語法從當前state拷貝屬性,並建立一個新的返回。這確保每次更變都會有新的state,保證了函數的純度。這也促進了引用完整性,保證在發生狀態更改時丟棄舊引用

注意:spread操做只執行淺複製,不處理深層嵌套對象。您須要複製對象中的每一個級別以確保不變性。有些庫能夠處理深度複製,包括 lodashimmer

當action被調度時,全部註冊過的reducers都會接收到這個action。經過switch語句肯定是否處理這個action。由於這個緣由,每一個switch語句中老是包含default case,當這個reducer不處理action時,返回提供的state。

註冊root state

state在你的應用中定義爲一個large object。註冊reducer函數。註冊reducer函數來管理state的各個部分中具備關聯值的鍵。使用StoreModule.forRoot()函數和鍵值對來定義你的state,來在你的應用中註冊一個全局的StoreStoreModule.forRoot()在你的應用中註冊一個全局的providers,將包含這個調度state的action和select的Store服務注入到你的component和service中。

app.module.ts

import {NgModule} from '@angular/core';
import {StoreModule} form '@ngrx/store';
import {scoreboardReducer} from './reducers/scoreboard.resucer';

@NgModule({
    imports: [StoreModule.forRoot({game: scoreboardReducer})],
})

export class AppModule {}

使用StoreModule.forRoot()註冊states能夠在應用啓動時定義狀態。一般,您註冊的state始終須要當即用於應用的全部區域。

註冊Feature state

Feature states的行爲和root state相同,可是你在你的應用中須要定義具體的特徵區域。你的state是一個large object,Feature state會在這個object中以鍵值對的形式註冊。
下面這個state object的例子,你將看到Feature state如何以遞增的方式構建你的state。讓咱們從一個空的state開始。

app.module.ts

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

這裏在你的應用中建立了一個空的state

{
}

如今使用scoreboardreducer和名稱爲ScoreboarModule的特徵NgModule註冊一個額外的state。

scoreboard.module.ts

import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { scoreboardReducer } from './reducers/scoreboard.reducer';

@NgModule({
  imports: [StoreModule.forFeature('game', scoreboardReducer)],
})
export class ScoreboardModule {}

添加ScoreboardModuleAPPModule

app.module.ts

import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { ScoreboardModule } from './scoreboard/scoreboard.module';

@NgModule({
  imports: [StoreModule.forRoot({}), ScoreboardModule],
})
export class AppModule {}

每一次ScoreboardModule被加載,這個game將會變爲這個object的一個屬性,並被管理在state中。

{
    game: { home: 0, away: 0}
}

Feature state的加載是eagerly仍是lazlly的,取決於你的應用。可使用Feature state 隨時間和不一樣特徵區域構建狀態對象。

select

Selector是一個得到store state的切片的純函數。@ngrx/store提供了一些輔助函數來簡化selection。selector提供了不少對state的切片功能。

  • 輕便的
  • 記憶化
  • 組成的
  • 可測試的
  • 類型安全的

當使用createSelectorcreateFeatureSelector函數時,@ngrx/store會跟蹤調用選擇器函數的最新參數。由於選擇器是純函數,因此當參數匹配時能夠返回最後的結果而不從新調用選擇器函數。這能夠提供性能優點,特別是對於執行昂貴計算的選擇器。這種作法稱爲memoization

使用selector切片state

index.ts

import {createSelector} from '@ngrx/store';

export interface FeatureState {
    counter: number;
}

export interface AppSatte {
    feature: FeatureState;
}

export const selectFeature = (state: AppState) => state.feature;

export const selectFeatureCount = createSelector(
    selectFeature,
    (state: FeatrureState) => state.counter
)

使用selectors處理多切片

createSelector可以從基於一樣一個state的幾個切片state中獲取一些數據。
createSelector最多可以接受8個selector函數,以得到更加完整的state selections。
在下面這個例子中,想象一下你有selectUser object 在你的state中,你還有book object的allBooks數組。
你想要顯示你當前用戶的全部書。
你可以使用createSelector來實現這些。若是你在allBooks中更新他們,你的可見的書將永遠是最新的。若是選擇了一本書,它們將始終顯示屬於您用戶的書籍,而且在沒有選擇用戶時顯示全部書籍。
結果將會是你從你的state中過濾一部分,而且他永遠是最新的。

import {createSelecotr} from '@ngrx/store';

export interface User {
    id: number;
    name: string;
}

export interface Book {
    id: number;
    userId: number;
    name: string;
}

export interface AppState {
    selectoredUser: User;
    allBooks: Book[];
}

export const selectUser = (state: AppSate) => state.selectedUser;
export const SelectAllBooks = (state: AppState) => state.allBooks;

export const SelectVisibleBooks = createSelector(
    selectUser,
    selectAllBooks,
    (selectedUser: User, allBooks: Books[]) => {
        if(selectedUser && allBooks) {
            return allBooks.filter((book: Book) => book.UserId === selectedUser.id);
        }else {
            return allBooks;
        }
    }
)

使用selecotr props

當store中沒有一個適合的select來獲取切片state,你能夠經過selector函數的props
在下面的例子中,咱們有計數器,並但願他乘以一個值,咱們能夠添加乘數並命名爲prop

index.ts

export const getCount = createSelector(
    getCounterValue,
    (counter, props) => counter * props.multiply
);

在這個組件內部,咱們定義了一個props

ngOnInit() {
    this.counter = this.store.pipe(select(formRoot.getCount, {multiply: 2}));
}

記住,selector只將以前的輸入參數保存在了緩存中,若是你用另外一個乘數來從新使用這個selector,selector總會去從新計算它的值,這是由於他在接收兩個乘數。爲了正確地記憶selector,將selector包裝在工廠函數中以建立選擇器的不一樣實例

index.ts

export const getCount = () => {
    createSelector(
        (state, props) => state.counter[props.id],
        (counter, props) => counter * props* multiply
    );
}

組件的selector如今調用工廠函數來建立不一樣的選擇器實例:

ngOnInit() {
        this.counter2 = this.store.pipe(select(fromRoot.getCount(), { id: 'counter2', multiply: 2 }));
        this.counter4 = this.store.pipe(select(fromRoot.getCount(), { id: 'counter4', multiply: 4 }));
        this.counter6 = this.store.pipe(select(fromRoot.getCount(), { id: 'counter6', multiply: 6 }));
    }

選擇Feature States

createFeatureSelector是一個返回頂級feature state的便捷方法。它返回一個按特徵切片的state的typed selector方法。
示例

index.ts

import { createSelector, createFeatureSelector } from '@ngrx/store';
 
export interface FeatureState {
    counter: number;
}
 
export interface AppState {
      feature: FeatureState;
}
 
export const selectFeature = createFeatureSelector<AppState, FeatureState>('feature');
 
export const selectFeatureCount = createSelector(
      selectFeature,
      (state: FeatureState) => state.counter
);

下面這個selector沒法被編譯,由於foo不是AppState的一個特徵切片。

index.ts

export const selectFeature = createFeatureSelector<AppSatte, FeatureState>('foo');

重置Memoized Selector

經過調用createSelector或createFeatureSelector返回的選擇器函數初始化具備memoized值null。在第一次將其memoized值存儲在內存中時調用selector。若是以後使用相同的參數調用selector,則它將返回memoized值。若是以後使用不一樣的參數調用選擇器,它將從新計算並更新其memoized值。

import { createSelector } from '@ngrx/store';

export interface State {
    counter1: number;
    counter2: number;
}

export const selectorCounter1 = (state: State) => state.counter1;
export const selectorCounter2 = (state: State) => state.counter2;

export const selectTotal = createSelector(
    selectCounter1,
    selectCounter2,
    (counter1, counter2) => counter1 + counter2
); // selecotTotal是一個值爲null的memoized值,由於他尚未被調用。

let state = { counter1: 3, counter2: 4};

selectTotal(state); //計算值3 + 4,返回7,selectTotal的memoized值的值爲7
selectTotal(state); //不作計算,直接返回selectTotal的memoized值的值7

state = {...state, counter2: 5};

selectTotal(state); //計算值3 + 5,返回8.selectTotal的memoized值的值爲8

selector memoized值會無限期的保留在內存中。若是memoized的值是一個不須要的大數據集,它能夠從新設置memoized值爲null,這樣就能夠將大數據集從內存中刪除。這能夠經過在selector上調用release方法來完成。

selectTotal(state); // 返回memoized值8
selectTotal.release(); //memoized的值被設爲null

釋放selector也會遞歸的釋放全部的先祖選擇器。

export interface State {
    evenNums: number[];
    oddNums: number[];
}

export const selectSumEvenNums = createSelector(
    (state: State) => state.evenNums,
    evenNums => evenNums.reduce((prev, curr) => prev + curr)
);

export const selectSumOddNums = createSelector(
    (state: State) => state.oddNums,
    oddNums => oddNums.reduce((prev, curr) => prev + curr)
);

export const selecTotal = createSelector(
    selectorSumEvenNums,
    selectorSumOddNums,
    (evenSum,oddSum) => evenSum + oddSum
);

selectTotal({
    evenNums: [2, 4],
    oddNums: [1, 3],
})
// selectSumEvenNums: 6
// selectSumOddNums: 4
// selectTotal: 10

selectTotal.release();

// selectSumEvenNums: null
// selectSumOddNums: null
// selectTotal: null

高級技巧

Selector可以爲你的應用state組成一個讀取模型(CQRS)。就CQRS架構模式而言,NgRx將讀取模型(Selector)與寫入模型(reducer)分開。高級技巧就是講selector與RxJs的操做符結合起來。
這個部分將涵蓋:如何將selector與pipe操做結合;演示利用createSelectorscan來顯示state的狀態變化歷史。

使用pipe查詢一個非空state

僞裝咱們有一個選擇器叫selectValue
咱們只使用RxJspipe操做來實現這一個功能:

app.component.ts

import {map, filter } from 'rxjs/operators';

store
    .pipe(
        map(state => selectValues(state)),
        filter(val => val !== undefined)
    )
    .subscribe(/* ... */)

咱們可使用select()方法重寫上述內容:

app.component.ts

import { select } from '@ngrx/store';
import { map, filter } from '@rxjs/operators';

store.
    pipe(
        select(selectValues),
        filter(val => val !== undefined)
    )
    .subscribe(/* ... */)

提取pipe操做

咱們使用pipe()方法來提取操做:

app.component.ts

import { select } from 'ngrx/store';
import { pipe } from 'rxjs';
import { filter } from 'rxjs/operators';

export const selectFilteredValues = pipe(
    select(selectValues),
    filter(val => val !== undefined)
);

store.pipe(selectFilteredValues).subscribe(/* ... */);

獲取最後N個state轉化

index.ts

export const selectProjectedValues = createSelector(
    selectFoo,
    selectBar,
    (foo, bar) => {
        if(foo && bar) {
            return {foo, bar};
        }
        
        return undefined;
    }
);
select-last-state-transition.ts

export const selectLastStateTransitions = (count: number) => {
    return pipe(
        select(selectProjectedValues),
        scan((acc, curr) => {
            return [curr, acc[0], acc[1] ].filter(val => val !== undefined);
        } [] as {foo: number; bar: string}[])
    );
}
app.component.ts

store.pipe(selectLastStateTransitons(3)).subscribe(/* ... */);

高級

Meta-reducers

@ngrx/store將多個reducers組合成一個reducer。
開發者能夠認爲meta-reducers像是一個在action -> reducer管道中的鉤子。Meta-reducers容許開發者在調用普通Reducer以前預處理操做。
使用metaReducers配置meta-reducers。metaReducers會從你提供的數組中,從右到左的執行。

注意:Meta-reducers在NgRx中相似於Redux的中間件。

使用meta-reducer來記錄全部的actions

app.module.ts

import { StoreModule, ActionReducer, MetaReducer } from '@ngrx/store';
import { reducers } from './reducers';

export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
    return function (state, action) {
        console.log('state', state);
        console.log('action', action);
        
        return reduccer(state, action);
    }
}

export const metaReducers: MetaReducer<any>[] = [debug];

@NgModule({
    imports: [StoreModule.forRoot(reducers, { metaReducers })],
})
export class AppMpdule{}

使用依賴注入(injection)

注入Reducers

使用InjectionToken和Provider經過依賴注入註冊reducer,注入root reducers到你的應用中。

app.module.ts

import { NgModule, InjectionToken } from '@angular/core';
import { StoreModule, ActionReducerMap } from '@ngrx/store';
 
import { SomeService } from './some.service';
import * as fromRoot from './reducers';

export const REDUCER_TOKEN = new InjectionToken<ActionReducerMap<fromRoot.State>>('Registered Reducers');

export function getReducers(someService: SomeService) {
    return someService.getReducers();
}

@NgModule({
    imports: [StoreModule.forRoot(REDUCER_TOKEN)],
    providers: [
        {
            provide: REDUCER_TOKEN,
            deps: [SomeService],
            useFactory: getReducers,
        }
    ],
})
export class AppModule {}

經過功能模塊組成state也可以注入Reducers。

feature.module.ts

import { NgModule, InjectionToken } from '@angular/core';
import { StoreModule, ActionReducerMap } from '@ngrx/store';
 
import * as fromFeature from './reducers';
 
export const FEATURE_REDUCER_TOKEN = new InjectionToken<
  ActionReducerMap<fromFeature.State>
>('Feature Reducers');
 
export function getReducers(): ActionReducerMap<fromFeature.State> {
  return {};
}
 
@NgModule({
  imports: [StoreModule.forFeature('feature', FEATURE_REDUCER_TOKEN)],
  providers: [
    {
      provide: FEATURE_REDUCER_TOKEN,
      useFactory: getReducers,
    },
  ],
})
export class FeatureModule {}

注入 Meta-Reducers

經過META_REDUCERSProvider來注入。

app.module.ts

import { MetaReducer, META_REDUCERS } from '@ngrx/store';
import { SomeService } from './some.service';
import * as fromRoot from './reducers';
 
export function getMetaReducers(
  some: SomeService
): MetaReducer<fromRoot.State>[] {}
 
@NgModule({
  providers: [
    {
      provide: META_REDUCERS,
      deps: [SomeService],
      useFactory: getMetaReducers,
    },
  ],
})
export class AppModule {}

Entity

Entity State adapter用於管理記錄集合。
Entity提供了API去操做和查詢entity集合。

  • 建立樣板reducers來管理models集合
  • 提供高性能的CRUD操做來管理entity集合
  • 用於選擇實體信息的可擴展類型安全適配器

安裝

npm install @ngrx/entity --save

Entity Interface

EntityState<T>

entity state 是具備如下接口的給定entity集合的預約義通用接口:

interface EntityState<V> {
    ids: string[] | number[];
    entities: {
        [id: string | id: number]: V
    };
}

ids: 集合中全部entity的id的數組集合
entities: 以id爲索引的entity合集字典
經過提供額外的屬性來擴展entity state

user.reducer.ts

export interface User {
    id: string;
    name: string;
}

export interface State extends EntityState<User> {
    //額外的entity state 屬性
    selectedUserId: number | null;
}

EntityAdapter<T>

爲提供的entity適配器提供一個通用的類型接口。entity適配器提供了許多的集合方法來管理entity state

export const adapter: EntityAdapter<User> = createEntityAdapter<User>();

Entity Adapter(entity適配器)

createEntityAdapter<T>

一種爲單個entity state集合返回通用entity適配器的方法。返回的適配器提供了許多適配器方法,用於對集合類型執行操做。這個方法的構建須要提供兩個參數。

  • selectId: 用於選擇字典id的方法。當entity具備id屬性時,可選。
  • sortcomparer: 用於集合排序的比較方法。只有在顯示以前須要對集合進行排序時才須要比較器功能。設置爲false將會不對集合排序,這將使CRUD操做更高效。
user.reducer.ts

import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';

export interface User {
    id: string;
    name: string;
}

export interface State extends EntityState<User> {
    selectedUserId: number;
}

export function selectUserId(a: user): string {
    return a.id;
}

export function sortByName(a: User, b: User): number {
    return a.name.localeCompare(b.name);
}

export const adapter: EntityAdapter<User> = createEntityAdapter<User>({
    selectId: selectUserId,
    sortComparer: sortByName
})

Adapter方法

這些方法由使用createEntityAdapter時返回的適配器對象提供。這些方法在reducer函數中用於根據您提供的操做管理entity集合。

getInitialState

根據提供的類型返回entity state的initialState。還經過提供的配置對象提供附加狀態。initialState提供你的reducer函數。

user.reducer.ts

import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
 
export interface User {
  id: string;
  name: string;
}
 
export interface State extends EntityState<User> {
  // additional entities state properties
  selectedUserId: number | null;
}

export const initialState: State = adapter.getInitialState({
    selectedUserId: null;
});

export function reducer(state = initialState, action): State {
    switch(action.type) {
        defualt: {
            return state;
        }
    }
}

Adapter集合方法

entity適配器也提供針對entity的操做方法。這些方法能夠一次改變一個到多個記錄。若是進行了更改,則每一個方法返回新修改的state,若是未進行任何更改,則返回相同的state。

  • addOne: 添加一個entity到集合中
  • addMany: 添加多個entity到集合中
  • addAll: 使用提供的集合更換當前的集合
  • removeOne: 從集合中刪除一個entity
  • removeMany: 經過id或者謂詞從集合中刪除多個entity
  • removeAll: 清空集合
  • updateOne: 更新集合中的一個entity
  • updateMany: 更新集合中的多個entity
  • upsertOne: 在集合中添加或更新一個entity
  • upsertMany: 在集合中添加或更新多個entity
  • map: 經過定義的map函數來更新集合中的多個entity,相似於Array.map
user.models.ts

export interface User {
    id: string;
    name: string;
}
user.actions.ts

import { Action } from '@ngrx/store';
import { Update } from '@ngrx/entity';

import { User } from '../models/user.model';

export enum UserActionTypes {
    LOAD_USERS = '[User]Load User',
    ADD_USER = '[User]Add User',
    UPSERT_USER = '[User]Upsert User',
    ADD_USERS = '[User] Add Users',
    UPSERT_USERS = '[User] Upsert Users',
    UPDATE_USER = '[User] Update User',
    UPDATE_USERS = '[User] Update Users',
    MAP_USERS = '[User] Map Users',
    DELETE_USER = '[User] Delete User',
    DELETE_USERS = '[User] Delete Users',
    DELETE_USERS_BY_PREDICATE = '[User] Delete Users By Predicate',
    CLEAR_USERS = '[User] Clear Users',
}

export class LoadUsers implements Action {
    readonly type = UserActiontypes.LOAD_USERS;
    
    constructor(public payload: { users: User[] }) {}
}

export class AddUser implements Action {
    readonly type = UserActiontypes.ADD_USER;
    
    constructor(public payload: { user: User }) {}
}

export class UpsertUser implements Action {
    readonly type = UserActiontypes.UPSERT_USER;
    
    constructor(public payload: { users: User }) {}
}

export class AddUsers implements Action {
    readonly type = UserActiontypes.ADD_USERS;
    
    constructor(public payload: { users: User[] }) {}
}

export class UpsertUsers implements Action {
    readonly type = UserActiontypes.UPSERT_USERS;
    
    constructor(public payload: { users: User[] }) {}
}


export class UpdateUser implements Action {
    readonly type = UserActiontypes.UPDATE_USER;
    
    constructor(public payload: { users: User }) {}
}

export class UpdateUsers implements Action {
    readonly type = UserActiontypes.UPDATE_USERS;
    
    constructor(public payload: { users: User[] }) {}
}

export class MapUsers implements Action {
    readonly type = UserActiontypes.MAP_USERS;
    
    constructor(public payload: { users: User[] }) {}
}

export class DeleteUser implements Action {
    readonly type = UserActiontypes.DALETE_USER;
    
    constructor(public payload: { id: string }) {}
}

export class DeleteUsers implements Action {
    readonly type = UserActiontypes.DELETE_USERS;
    
    constructor(public payload: { ids: string[] }) {}
}

export class DeleteUsersByPredicate implements Action {
    readonly type = UserActiontypes.DELETE_USERS_BY_PREDICATE;
    
    constructor(public payload: { predicate: Predicate<User> }) {}
}

export class ClearUsers implements Action {
    readonly type = UserActiontypes.CLEAR_USERS;
}

export type UserActionsUnion = 
| LoadUsers
| AddUser
| UpsertUser
| AddUsers
| UpsertUsers
| UpdateUser
| UpdateUsers
| MapUsers
| DeleteUser
| DeleteUsers
| DeleteUsersByPredicate
| ClearUsers;
user.reducer.ts

import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { User } from '../models/user.model';
import { UserActionsUnion, UserActiontypes } from '../actions/user.actions';

export interface State extends EntityState<User> {
    selectedUserId: number | null;
}

export const adapter: EntityAdapter<User> = createEntityAdapter<User>();

export conmst initialState: State = adapter.getInitialState({
    selectedUserId: null;
});

export function reducer(state = initialState, action: UserActionsUnion): State {
  switch (action.type) {
    case UserActionTypes.ADD_USER: {
      return adapter.addOne(action.payload.user, state);
    }
 
    case UserActionTypes.UPSERT_USER: {
      return adapter.upsertOne(action.payload.user, state);
    }
 
    case UserActionTypes.ADD_USERS: {
      return adapter.addMany(action.payload.users, state);
    }
 
    case UserActionTypes.UPSERT_USERS: {
      return adapter.upsertMany(action.payload.users, state);
    }
 
    case UserActionTypes.UPDATE_USER: {
      return adapter.updateOne(action.payload.user, state);
    }
 
    case UserActionTypes.UPDATE_USERS: {
      return adapter.updateMany(action.payload.users, state);
    }
 
    case UserActionTypes.MAP_USERS: {
      return adapter.map(action.payload.entityMap, state);
    }
 
    case UserActionTypes.DELETE_USER: {
      return adapter.removeOne(action.payload.id, state);
    }
 
    case UserActionTypes.DELETE_USERS: {
      return adapter.removeMany(action.payload.ids, state);
    }
 
    case UserActionTypes.DELETE_USERS_BY_PREDICATE: {
      return adapter.removeMany(action.payload.predicate, state);
    }
 
    case UserActionTypes.LOAD_USERS: {
      return adapter.addAll(action.payload.users, state);
    }
 
    case UserActionTypes.CLEAR_USERS: {
      return adapter.removeAll({ ...state, selectedUserId: null });
    }
 
    default: {
      return state;
    }
  }
}

export const getSelectedUserId = (state: State) => state.selectedUser.id;

const {
    selectIds,
    selectEntities,
    selectAll,
    selectTotal
} = adapter.getSelectors();

export const selectUserIds = selectids;
export const selectUserEntities = selectEntities;
export const selectAllUsers = selectAll;
export const selectUserTotal = selectTotal;

entity選擇器

建立的entity適配器返回的getSelectors方法提供了從entity中選擇信息的功能。
getSelectors方法將選擇器函數做爲其惟一參數,以選擇已定義entity state片斷。

index.ts

import { createSelector, createFeatureSelector, ActionReducerMap, } from '@mgrx/store';
import * as fromUser from './user.reducer';
 
export interface State {
  users: fromUser.State;
}
 
export const reducers: ActionReducerMap<State> = {
  users: fromUser.reducer,
};
 
export const selectUserState = createFeatureSelector<fromUser.State>('users');
 
export const selectUserIds = createSelector(
  selectUserState,
  fromUser.selectUserIds
);
export const selectUserEntities = createSelector(
  selectUserState,
  fromUser.selectUserEntities
);
export const selectAllUsers = createSelector(
  selectUserState,
  fromUser.selectAllUsers
);
export const selectUserTotal = createSelector(
  selectUserState,
  fromUser.selectUserTotal
);
export const selectCurrentUserId = createSelector(
  selectUserState,
  fromUser.getSelectedUserId
);
 
export const selectCurrentUser = createSelector(
  selectUserEntities,
  selectCurrentUserId,
  (userEntities, userId) => userEntities[userId]
);

Effects

Effects是Rxjs的反作用模型。Effects使用流來提供新的操做源來和外部交流(例如網絡請求,webScoket消息和基於時間的事件)
介紹
在基於服務的angualr應用中,component直接經過service來和外部資源進行溝通。相反的,effects提供了與服務的交互來將他們與組件隔離。effects是處理任務的地方,例如獲取數據,生成多個事件的長時間運行的任務,以及組件不須要明確瞭解這些交互的其餘外部交互。
關鍵概念

  • Effects隔離component的反作用,容許component有更多的純的select state和actions調用。
  • Effects是一個監聽store每個actions調用的Observerable的長期運行的服務。
  • Effects根據他們所須要監聽的action來過濾actions。
  • Effects執行同步或異步的任務並返回一個新的action。

和基於組件的反作用比較
在基於服務的應用中,component經過許多不一樣的service與數據交互,這些service經過屬性和方法公開數據。這些服務可能又依賴於管理另一些數據的服務。你的component使用這些service來完成任務。
想象一下,你的應用用來管理電影。下面這個component用於獲取並展現電影列表。

movies-page.component.ts

@component({
    template: `
        <li *ngFor="let move of movies">{{movie.name}}</li>
    `
})
export class MoviesPageComponent {
    movies: Movie[];
    
    consatructor(private movieService: MoviesService){}
    
    ngOnInit() {
        this.movieService.getAll().subscribe(movies => this.movies = movies);

    }
}

你還須要相應的service來提取電影。

@Injectable({
    providenIn: 'root'
})
export class MovieService {
    constructor (private http: HttpClient) {}
    
    getAll() {
    return this.http.get('/movies'); 
    }
}

這個component有多個職責:

  • 管理電影的狀態
  • 使用service來執行反作用,調用外部API來獲取電影
  • 更改component中電影的狀態

使用Effects能夠減小component的職責。在更大的應用程序中,這變得更加劇要,由於您有多個數據源,須要多個service來獲取這些數據,而service可能依賴於其餘service。
Effects處理外部數據和交互,使service只執行與外部交互相關的任務。接下來,重構component以將電影數據放入Store中。Effects處理獲取電影數據。

@Component({
    template: `
    <div *ngFor="let movie of movies$ | async">
      {{ movie.name }}
    </div>
  `
})
export class MoviesPageComponent {
    movies$: Observable = this.store.selet(state => state.movies);
    
    constructor(private store: Store<{movies: Movie[]}>) {}
    
    ngOnInit() {
        this.store.dispatch({type: '[movies Page]Load Movies'})
    }
}

電影仍然經過MoviesService獲取,但該組件再也不關注如何獲取和加載電影.它只負責聲明加載電影和使用選擇器訪問電影列表數據的意圖。Effect經過異步的方式獲取電影。
撰寫Effects
要隔離component的反作用,必須建立一個Effects類來偵聽事件和執行任務。
Effects是一個擁有多個不一樣部分的可注入服務:

  • 一個可注入的Action service,提供一個在全部Action調度後,最新的狀態改變的Observable劉
  • 使用Effects裝飾器,用元數據裝飾Observable流。元數據用於註冊訂閱Store流,而後,從Effects返回的任何操做都會從新回到Store流。
  • Effects訂閱了Store的Observable。
  • service注入Effects,完成外部API交互並處理流。

爲了說明如何處理上面示例中的加載電影,讓咱們看一下MovieEffects。

movie.effects.ts

import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';

@Injectable()
export class MovieEffects {
}
相關文章
相關標籤/搜索