ngRx 官方示例分析 - 2. Action 管理


我們從 Action 名稱開始。

解決 Action 名稱沖突問題

ngRx 中,不同的 Action 需要一個 Action Type 進行區分,一般來說,這個 Action Type 是一個字符串,如何定義和使用這個字符串是需要首先考慮的問題。需要保證不同的 Action 名稱不能沖突,使用的時候還需要方便,編碼的時候,最好有提示等等。

首先處理命名沖突問題,示例使用 util 中定義的一個字典來檢查是否已經定義了一個 Action

app/util.ts

/**
 * This function coerces a string into a string literal type.
 * Using tagged union types in TypeScript 2.0, this enables
 * powerful typechecking of our reducers.
 *
 * Since every action label passes through this function it
 * is a good place to ensure all of our action labels
 * are unique.
 */

const typeCache: { [label: string]: boolean } = {};
export function type<T>(label: T | ''): T {
  if (typeCache[<string>label]) {
    throw new Error(`Action type "${label}" is not unique"`);
  }

  typeCache[<string>label] = true;

  return <T>label;
}

使用 TypeScript 的 Playground 翻譯一下,可以得到如下的結果:

var typeCache = {};
function type(label) {
    if (typeCache[label]) {
         throw new Error("Action type \"" + label + "\" is not unique\"");
    }
    typeCache[label] = true;
    return label;
}

可以更加直觀地看到,這個 type 函數可以接收一個字符串,在類型緩存對象 typeCache 中檢查是否已經設置過這個 Key, 如果已經設置過一次,拋出異常,這樣可以避免命名沖突問題。如果沒有,則將這個 Action Type 保存為緩存對象的鍵,值設置為 true. 最后返回這個 Action Type 的字符串。特別需要注意的是它已經被作為一個類型返回了。

使用 String Literal Type 實現 Action 名稱的強類型化

具體的 Action Type 有哪些呢?它們分別定義在 /app/actions/books.ts/app/actions/layouts.ts 和 /app/actions/collection.ts 中。 

 實現 Action 接口

然后,我們再看看 Action 接口的定義,它來自 @ngrx/store

export interface Action {
  readonly type: string;
}

這里是對一個 Action 基本的要求,需要一個字符串類型的 Action 名稱。

為了便於使用每一種 Action ,使用了 Action Creator。以后可以直接使用這些 class 來創建 Action,而不用直接創建對象。這里定義了每種 Action 所對應的 Action 實現。例如 SearchAction 的實現如下所示:

/**
 * Every action is comprised of at least a type and an optional
 * payload. Expressing actions as classes enables powerful
 * type checking in reducer functions.
 *
 * See Discriminated Unions: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
 */
export class SearchAction implements Action {
  type = ActionTypes.SEARCH;

  constructor(public payload: string) { }
}

可以看到它的 Action 類型使用 ActionTypes 來定義,值固定設置為了 "[Book] Search",可以通過構造函數傳遞這個 Action 所使用的附加數據。

ActionTypes 的定義如下所示,實際上是一個對象。我們通過屬性來獲取實際的值,這個值來自前面的 type 函數返回值。

/**
 * For each action type in an action group, make a simple
 * enum object for all of this group's action types.
 *
 * The 'type' utility function coerces strings into string
 * literal types and runs a simple check to guarantee all
 * action types in the application are unique.
 */
export const ActionTypes = {
  SEARCH:           type('[Book] Search'),
  SEARCH_COMPLETE:  type('[Book] Search Complete'),
  LOAD:             type('[Book] Load'),
  SELECT:           type('[Book] Select'),
};

其它三種的 Action 以此類推。

 我們先看看最后的 5 行。這里導出類型的別名。

這里使用了 TypeScript 中的 String Literal Types, 使得我們可以利用強類型的方式來使用字符串。

/**
 * Export a type alias of all actions in this action group
 * so that reducers can easily compose action types
 */
export type Actions
  = SearchAction | SearchCompleteAction | LoadAction | SelectAction;

代碼實現

代碼的全部內容如下所示。

import { Action } from '@ngrx/store';
import { Book } from '../models/book';
import { type } from '../util';

/**
 * For each action type in an action group, make a simple
 * enum object for all of this group's action types.
 *
 * The 'type' utility function coerces strings into string
 * literal types and runs a simple check to guarantee all
 * action types in the application are unique.
 */
export const ActionTypes = {
  SEARCH:           type('[Book] Search'),
  SEARCH_COMPLETE:  type('[Book] Search Complete'),
  LOAD:             type('[Book] Load'),
  SELECT:           type('[Book] Select'),
};


/**
 * Every action is comprised of at least a type and an optional
 * payload. Expressing actions as classes enables powerful
 * type checking in reducer functions.
 *
 * See Discriminated Unions: https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
 */
export class SearchAction implements Action {
  type = ActionTypes.SEARCH;

  constructor(public payload: string) { }
}

export class SearchCompleteAction implements Action {
  type = ActionTypes.SEARCH_COMPLETE;

  constructor(public payload: Book[]) { }
}

export class LoadAction implements Action {
  type = ActionTypes.LOAD;

  constructor(public payload: Book) { }
}

export class SelectAction implements Action {
  type = ActionTypes.SELECT;

  constructor(public payload: string) { }
}

/**
 * Export a type alias of all actions in this action group
 * so that reducers can easily compose action types
 */
export type Actions
  = SearchAction
  | SearchCompleteAction
  | LoadAction
  | SelectAction;

然后是 /app/actions/layouts.ts.

import { Action } from '@ngrx/store';
import { type } from '../util';

export const ActionTypes = {
  OPEN_SIDENAV:   type('[Layout] Open Sidenav'),
  CLOSE_SIDENAV:  type('[Layout] Close Sidenav')
};


export class OpenSidenavAction implements Action {
  type = ActionTypes.OPEN_SIDENAV;
}

export class CloseSidenavAction implements Action {
  type = ActionTypes.CLOSE_SIDENAV;
}


export type Actions
  = OpenSidenavAction
  | CloseSidenavAction;

以及 /app/actions/collection.ts 的定義。

import { Action } from '@ngrx/store';
import { Book } from '../models/book';
import { type } from '../util';

export const ActionTypes = {
  ADD_BOOK:             type('[Collection] Add Book'),
  ADD_BOOK_SUCCESS:     type('[Collection] Add Book Success'),
  ADD_BOOK_FAIL:        type('[Collection] Add Book Fail'),
  REMOVE_BOOK:          type('[Collection] Remove Book'),
  REMOVE_BOOK_SUCCESS:  type('[Collection] Remove Book Success'),
  REMOVE_BOOK_FAIL:     type('[Collection] Remove Book Fail'),
  LOAD:                 type('[Collection] Load'),
  LOAD_SUCCESS:         type('[Collection] Load Success'),
  LOAD_FAIL:            type('[Collection] Load Fail'),
};

/**
 * Add Book to Collection Actions
 */
export class AddBookAction implements Action {
  type = ActionTypes.ADD_BOOK;

  constructor(public payload: Book) { }
}

export class AddBookSuccessAction implements Action {
  type = ActionTypes.ADD_BOOK_SUCCESS;

  constructor(public payload: Book) { }
}

export class AddBookFailAction implements Action {
  type = ActionTypes.ADD_BOOK_FAIL;

  constructor(public payload: Book) { }
}

/**
 * Remove Book from Collection Actions
 */
export class RemoveBookAction implements Action {
  type = ActionTypes.REMOVE_BOOK;

  constructor(public payload: Book) { }
}

export class RemoveBookSuccessAction implements Action {
  type = ActionTypes.REMOVE_BOOK_SUCCESS;

  constructor(public payload: Book) { }
}

export class RemoveBookFailAction implements Action {
  type = ActionTypes.REMOVE_BOOK_FAIL;

  constructor(public payload: Book) { }
}

/**
 * Load Collection Actions
 */
export class LoadAction implements Action {
  type = ActionTypes.LOAD;

  constructor() { }
}

export class LoadSuccessAction implements Action {
  type = ActionTypes.LOAD_SUCCESS;

  constructor(public payload: Book[]) { }
}

export class LoadFailAction implements Action {
  type = ActionTypes.LOAD_FAIL;

  constructor(public payload: any) { }
}

export type Actions
  = AddBookAction
  | AddBookSuccessAction
  | AddBookFailAction
  | RemoveBookAction
  | RemoveBookSuccessAction
  | RemoveBookFailAction
  | LoadAction
  | LoadSuccessAction
  | LoadFailAction;

 總結

  • Action 的名稱使用了 String Literal Type 來實現強類型支持
  • 針對每種 Action 實現了 Action 接口,其中固定了所對應的 Action 類型,使用中不必再提供這個 Action 串,實際上是 Action Creator。
  • 對於應用中常見的大量 Action 名稱沖突問題,通過 type 函數解決,這個函數將會緩存已經定義的 Action 類型。 

 

參考資源:

上一篇:ngRx 官方示例分析 - 1. 介紹

下一篇:ngRx 官方示例分析 - 3. reducers


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM