歡迎光臨
我們一直在努力

Angular應用架構設計-3:Ngrx Store

文章摘要: 模組化、樹狀的state 在Ngrx中資料儲存在store中資料從store中select得來

這是有關Angular應用架構設計系列文章中的一篇,在這個系列當中,我會結合這近兩年中對Angular、Ionic、甚至Vuejs等框架的使用經驗,總結在應用設計和開發過程中遇到的問題、或總結的經驗,來說一下Angular應用的架構設計相關的一些問題,包括像元件設計、元件直接的資料互動與通訊、Ngrx Store的使用、Rxjs的使用與響應式程式設計思想。這些設計思想和方法,不僅適用於Angular,也適用於Vuejs、React等前端框架。當然,應用架構設計沒有一個放之四海皆準的標準,他只能是根據具體情況具體分析,得出一個儘可能有點設計方案。如果大家有更好的想法,歡迎交流。

上一部分介紹使用Data Service模式,來實現單向數據流、事件流。這實際上就是Redux模式,在React中,有Redux和Flux,在Angular中,就有Ngrx。我們先來結合之前的單向資料、事件流,看一下Ngrx的組成部分及其功能:

使用Ngrx後,所有的資料都放在Ngrx的store裡,並通過 select 的方式使用, select 出來的資料是一個可訂閱的 Observable 資料物件;所有對資料的修改,都通過分發一個 action ,由 reducer 來響應這個事件,事件處理的結果要更新store裏面的資料的話,就通過 commit 更新資料,更新的資料會通知訂閱者去更新。

在Angular中使用Store,元件和store的關係,以及資料和事件如何互動,就是如上圖所示。我們就來看一下我們怎樣才能用好Ngrx。

模組化、樹狀的state

在Ngrx中資料儲存在store中,儲存的資料叫 state ,這個state可以是一個樹狀結構,我們可以將樹狀結構的第一級作為模組,然後將每個模組裏面的資料物件也儘量的按照資料本身的關係,以樹狀方式組織。

我們來看一個簡單的例項,一個使用者中心頁,頁面的設計大致如下:

頁面上包含一些使用者資訊,使用者所擁有的錢包的餘額、優惠券的餘額等資訊,還有優惠券的列表等。

相應的,我們的store裏面的數據結構,大致設計如下:

在這個結構下,我們將整個app的state分成幾個模組,使用者資訊、訂單、購物車、商品等,然後在user模組裡,包含的資料有使用者資訊、使用者訊息、使用者地址、使用者的優惠券、錢包等等資訊。

在這個例子當中,我們把使用者的優惠券資訊、錢包等資訊放在使用者資訊裏面,這些元件使用這些資料的方式和關係如下:

使用者的state是這樣設計:

export interface UserState {
  authenticated: boolean
  account: Account
  vouchers: Array
  wallet: any
}

const initialState: UserState = {
  authenticated: false,
  account: null,
  vouchers: [],
  wallet: undefined
}

我們的select是這樣:

export const account = (state: State) => state.user.account
export const userVouchers = (state: State) => state.user.account.vouchers
export const userWallet = (state: State) => state.user.account.wallet

從這個select中我們可以看出,所有的select都是從整個store的根開始的,也就是AppState。然後根據樹狀結構一級一級的往下select,比如使用者資訊就是 state.user.account 。當store裏面的資料發生修改時,我們是這樣修改的:

export function reducer(state = initialState, action: user.Actions): UserState {
  switch (action.type) {
    case user_account.GET_WALLET_SUCCESS: {
      const wallet = action.wallet // 從action中得到更新的資料
      return Object.assign({}, state, {
        wallet: wallet
      })
    }
    ...
  }
}

從這個reducer的這個方法我們可以看出,Ngrx更新store裡的資料的時候,在原有的state(user模組的state)的基礎上,更新要更新的那個物件的引用,把這個state物件裏面的所有引用複製到一個新的物件裡。通過這種更新方式,我們就可以:

  1. 更新使用者state的引用值。
  2. 將原先所有資料(除了被更改的)的引用複製到新的state中,這樣就能保證沒有被更改的資料的引用值沒有修改。
  3. 被修改的資料,它的引用也會被修改。

通過這樣的修改方式,再加上我們從store裡select的資料是 Observable 型別的,所以,只有被修改的資料的訂閱會被觸發,那麼我們就可以通過合理的設計我們的state的數據結構和與相應的元件直接的資料關係,來更合理的處理我們的資料的互動和處理。

在我們上面的使用者資訊的元件中,使用者state的每個資料被修改,整個使用者的state的引用值就會被更新,但是,它裏面沒有被修改的那部分資料的引用值也沒有被修改。

在這個例項中,我們將使用者的優惠券、錢包資料放在了使用者基本資訊的物件裡。實際上只是爲了演示這種樹狀的數據結構,並不是說在這個例子中有什麼特別的用處。

一個數據的多個響應

有時候,我們需要在一個數據被修改的時候,更新頁面上兩個地方。比如說很多應用中都會有」我的訊息」頁面,用列表的方式顯示訊息,在頁面的右上角也有一個使用者的未讀訊息數。使用者可以點一個訊息,然後這個訊息直接在頁面上展開閱讀,再點一下就收縮這條訊息。當一個訊息被閱讀的時候,右上角的訊息數會減少1。

這個例子中,使用者的state中有一個messages:

export interface UserState {
  account: Account
  messages: Array
  ...
}

const initialState: UserState = {
  account: null,
  messages: [],
  ...
}

在我們的reducer中,閱讀訊息的時候,可以更改這一條訊息的是否已讀狀態,把所有的訊息放到新的列表裡(因為到更新訊息的引用值),或者直接從伺服器重新獲得訊息列表。但是無論如何,訊息列表的引用值會被修改。我們爲了在頁面中2個地方更新訊息資料:

  1. 可以使用2個select,分別用於獲取訊息列表,和統計訊息列表中的未讀數。
  2. 使用1個獲取訊息列表,然後在元件中訂閱的地方統計未讀訊息數。

我推薦是第一種方式,因為這樣我們的元件就可以儘量的簡單,把有關資料和對資料的查詢操作放在select裡。所以這兩個select可以這樣:

export const messages = (state: State) => state.user.messages
export const messageCount = (state: State) => {
  // 過濾未讀的訊息並統計數量
  return _.filter(state.user.messages, msg => !msg.read).count()
}

通過這個例項,我們可以將Ngrx的select看作是從資料模型到頁面元件裡資料模型的對映。所以這個select不是簡單的將store裏面的資料簡單的暴露給元件,而是應該承擔資料對映的功能。

資料模型和檢視模型

在上面的例子中,我們從資料模型 messages 中,通過select得到了一個新資料,也就是新訊息數量,繫結到某個頁面的顯示元件中。這個state的messages資料是我們的資料模型,而這個顯示在右上角的新訊息數,就是一個檢視模型,也就是在顯示元件(也可能是功能元件)中顯示的資料。下面我們就討論一下這個資料模型和檢視模型。

資料模型和檢視模型之間的關係,其實就很像我們的資料庫,其中資料模型就是資料庫中的一個個表,而檢視模型就是針對這個資料模型做的查詢操作。查詢可能是把幾個表關聯到一起展示,也可能是針對一個表根據一些條件做查詢,也可能再針對這個結果做一個統計等。

例如在一個表中,儲存的是訊息,裏面存的發信人、收信人都是存的使用者的id,但是我們需要的資料是使用者的暱稱。那我們就可以關聯訊息表和使用者表,根據使用者的id關聯,在返回的結果中包含訊息和收信人、發信人的暱稱。

而在Ngrx中的select就可以當做是資料庫的SQL查詢語句,它根據store裏面的資料,根據一些條件查詢,或做某一些統計,結果就是一個包含結果的 Observable 物件。每當state裏面的資料更新的時候,最新的資料也會通過這些select查詢被更新,並繫結到顯示元件上。

所以,我們的資料從服務上獲取,到最終顯示到頁面上經歷幾個狀態:

  1. 從伺服器獲取的資料。
  2. 儲存到store裏面的資料。
  3. select以後要顯示到頁面上的資料。

然後,會有兩個對資料的操作:

  1. 從伺服器獲取的資料,可能會經過一些簡單的修改、合併、轉換,儲存到store中,儲存的時候,要從業務和資料的角度出發,更好的設計數據結構,能夠將這個資料更好的與最終的顯示元件結合。
  2. 我們使用select,通過對資料做一些查詢、合併、統計,得到一個最終用於展示到顯示元件的資料。

通過這種方式,我們就能讓我們的模型,和我們的展示的檢視之間更好的解耦,把對資料的查詢和轉換留在store的select裏面,讓顯示元件無需爲了顯示而處理資料。

進一步解耦元件跟資料模型

剛纔我們把資料的展示過程中對資料的處理,和元件直接做了解耦,也就是不在元件中轉換資料,而是在select中轉換好。但是,即便這樣,我們的store和我們的元件直接的關聯還是太緊密了,我們看一個例子:

export class UserComponent{
    users$ = this.store.select(state => state.users);
    foo$ = this.store.select(state => state.foo);
    bar$ = this.store.select(state => state.bar);
    constructor(private store: Store){}
    addUser(user: User): void {
        this.store.dispatch({type: ADD_USER, payload: {user}}
    }
    removeUser(userId: string): void {
        this.store.dispatch({type: REMOVE_USER, payload: {userId}}
    }
}

根據我們上面的說法,這樣用似乎沒什麼問題,資料從store中select得來,繫結到模板中,資料的更新發送到store中處理。但是,這個元件和store的關聯還是太緊密,我們的元件需要指定store中儲存的資料的結構,store裏面能夠處理的action,引數是什麼樣的。

而我們在設計應用架構的時候,一直都在說解耦解耦,顯然這樣的關聯是違背了我們的解耦原則。一般我們說解耦的時候,大多數情況是要把展示邏輯和業務邏輯解耦,也就是頁面上觸發一個事件的時候不需要知道業務處理模組裏面的具體情況。也就是在Ngrx中儘量把dispatch action的部分封裝到一個Service當中,不要讓顯示元件直接去使用store內部的action。而對於資料獲取,我們還是需要知道store裏面的數據結構,才能在頁面顯示。

所以,對於上面的程式碼,我們可以建立一個如下的Service類:

export class UserService{
    // 只將state裏面的使用者模組暴露出來,元件就從該服務中通過這個user$來訪問內部資料
    users$ = this.store.select(state => state.users);
    
    constructor(private store: Store, private http: Http){
    }
    addUser(user: User): void {
        this.store.dispatch({type: ADD_USER, payload: {user}}
    }
    removeUser(userId: string): void {
        this.store.dispatch({type: REMOVE_USER, payload: {userId}}
    }
    
    fetchUsers(): void{
        this.store.dispatch({type: GET_USER, payload: null}
    }
}

這樣我們的這個 UserService 作為store和元件直接的橋樑,將store的action隱藏起來,只給元件暴露出了很友好的事件方法。

未經允許不得轉載:頭條楓林網 » Angular應用架構設計-3:Ngrx Store