我們從 Action 名稱開始。
解決 Action 名稱沖突問題
在 ngRx 中,不同的 Action 需要一個 Action Type 進行區分,一般來說,這個 Action Type 是一個字符串,如何定義和使用這個字符串是需要首先考慮的問題。需要保證不同的 Action 名稱不能沖突,使用的時候還需要方便,編碼的時候,最好有提示等等。
首先處理命名沖突問題,示例使用 util 中定義的一個字典來檢查是否已經定義了一個 Action
/** * 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;
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 類型。