這是有關Angular應用架構設計系列文章中的一篇,在這個系列當中,我會結合這近兩年中對Angular、Ionic、甚至Vuejs等框架的使用經驗,總結在應用設計和開發過程當中遇到的問題、和總結的經驗,來講一下Angular應用的架構設計相關的一些問題,包括像組件設計、組件之間的數據交互與通訊、Ngrx Store的使用、Rxjs的使用與響應式編程思想。這些設計思想和方法,不只適用於Angular,也適用於Vuejs、React等前端框架。
固然,應用架構設計沒有一個放之四海皆準的標準,他只能是根據具體狀況具體分析。若是你們有更好的想法,歡迎交流。javascript
上一部分介紹 使用Data Service模式,來實現單向數據流、事件流。這實際上就是Redux模式,在React中,有Redux和Flux,在Angular中,就有Ngrx。咱們先來結合以前的單向數據、事件流,看一下Ngrx的組成部分及其功能:
前端
使用Ngrx後,全部的數據都放在Ngrx的store裏,並經過select
的方式使用,select
出來的數據是一個可訂閱的Observable
數據對象;全部對數據的修改,都經過分發一個action
,由reducer
來響應這個事件,事件處理的結果要更新store裏面的數據的話,就經過commit
更新數據,更新的數據會通知訂閱者去更新。java
在Angular中使用Store,組件和store的關係,以及數據和事件如何交互,就是如上圖所示。咱們就來看一下咱們怎樣才能用好Ngrx。chrome
在Ngrx中數據保存在store中,保存的數據叫state
,這個state能夠是一個樹狀結構,咱們能夠將樹狀結構的第一級做爲模塊,而後將每一個模塊裏面的數據對象也儘可能的按照數據自己的關係,以樹狀方式組織。數據庫
咱們來看一個簡單的實例,一個用戶中心頁,頁面的設計大體以下:
頁面上包含一些用戶信息,用戶所擁有的錢包的餘額、優惠券的餘額等信息,還有優惠券的列表等。編程
相應的,咱們的store裏面的數據結構,大體設計以下:
在這個結構下,咱們將整個app的state分紅幾個模塊,用戶信息、訂單、購物車、商品等,而後在user模塊裏,包含的數據有用戶信息、用戶消息、用戶地址、用戶的優惠券、錢包等等信息。前端框架
在這個例子當中,咱們把用戶的優惠券信息、錢包等信息放在用戶信息裏面,這些組件使用這些數據的方式和關係以下:
服務器
用戶的state是這樣設計:數據結構
export interface UserAccount { username: string other_fields: string vouchers: Array<any> wallet: any } export interface UserState { authenticated: boolean account: UserAccount messages: Array<any> addresses: Array<any> } const initialState: UserState = { authenticated: false, account: null, messages: [], addresses: [] }
咱們的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對象裏面的全部引用複製到一個新的對象裏。經過這種更新方式,咱們就能夠:
經過這樣的修改方式,再加上咱們從store裏select的數據是Observable
類型的,因此,只有被修改的數據的訂閱會被觸發,那麼咱們就能夠經過合理的設計咱們的state的數據結構和與相應的組件之間的數據關係,來更合理的處理咱們的數據的交互和處理。
在咱們上面的用戶信息的組件中,用戶state的每一個數據被修改,整個用戶的state的引用值就會被更新,可是,它裏面沒有被修改的那部分數據的引用值也不會被修改,從而它們的訂閱器也不會被觸發。
在這個實例中,咱們將用戶的優惠券、錢包數據放在了用戶基本信息的對象裏。實際上只是爲了演示這種樹狀的數據結構,並非說在這個例子中有什麼特別的用處。
有時候,咱們須要在一個數據被修改的時候,更新頁面上兩個地方。好比說不少應用中都會有"個人消息"頁面,用列表的方式顯示消息,在頁面的右上角也有一個用戶的未讀消息數。用戶能夠點一個消息,而後這個消息直接在頁面上展開閱讀,再點一下就收縮這條消息。當一個消息被閱讀的時候,右上角的消息數會減小1。
這個例子中,用戶的state中有一個messages:
export interface UserState { account: Account messages: Array<any> ... } const initialState: UserState = { account: null, messages: [], ... }
在咱們的reducer中,閱讀消息的時候,能夠更改這一條消息的是否已讀狀態,把全部的消息放到新的列表裏(由於到更新消息的引用值),或者直接從服務器從新得到消息列表。可是不管如何,消息列表的引用值會被修改。咱們爲了在頁面中2個地方更新消息數據,可使用2種方式:
我推薦是第一種方式,由於這樣咱們的組件就能夠儘可能的簡單,把有關數據和對數據的查詢操做放在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查詢被更新,並綁定到顯示組件上。
因此,咱們的數據從服務上獲取,到最終顯示到頁面上經歷幾個狀態:
而後,會有兩個對數據的操做:
經過這種方式,咱們就能讓咱們的模型,和咱們的展現的視圖之間更好的解耦,把對數據的查詢和轉換留在store的select裏面,讓顯示組件無需爲了顯示而處理數據。
有一點有關視圖模型須要特別說明的是,每當數據模型裏面的數據修改時,全部跟這個數據有關的視圖模型的訂閱也會被觸發。
舉個例子,仍是上面的用戶消息的例子。假設在咱們的消息數據中有一個屬性是「是否回覆」,也就是用戶回覆了一條消息後,標記爲true
。那麼,若是用戶打開一條以前已經讀過的消息,而後進行回覆。這時,用戶的messages
數據發生修改,那麼上面的2個select的訂閱器都會被觸發。可是,這時候,有關未讀消息數的這個數據實際上是沒有改變的,但仍是被從新計算了一次。若是咱們select的結果是一個對象,這時候對象的引用值發生改變,那麼在頁面上的相應組件也會被刷新。
因此,在使用視圖模型的時候必定要注意,你的select使用的數據必定要通過仔細設計,不能爲了頁面顯示方便,就一股腦的從根的state獲取好多數據並生成一個對象返回。這樣會嚴重影響性能。
咱們保存在store中的數據,除了業務數據,其實咱們也能夠把頁面狀態的數據保存到store中,也就是UI state。好比說一個典型的場景就是一個比較複雜的買票頁,我可能須要輸入購買數量,選擇購買票的座位,有一些演唱會或項目還要求按照購買數量輸入購買人的身份證號。若是咱們把這些數據也做爲一個UI state模塊,保存在store中,那麼當用戶因爲一些緣由跳到了其餘頁面,而後再回來這個購買頁的時候,以前輸入的信息都還在。這樣對用戶的交互體驗可能會更好,特別是在手機上。
使用UI state還有一個好處就是,咱們的store裏面的數據徹底可以肯定頁面的狀態,不論是用戶買票輸入的內容,仍是支付的時候選擇的支付方式等,都保存在store中。而後當咱們使用Ngrx的開發工具(chrome的DevTool插件)的時候,咱們能夠選擇任何一個歷史的store的狀態,這樣頁面就會按照這個時候的state來展現。這樣,當咱們進行了一些操做之後,經過選擇某一個時間點的state,就能重現當時那個時間的頁面狀態,這就是Ngrx裏面所說的 Time Travel。
那麼,哪些數據須要保存在store中?可使用下面兩個簡單的標準:
剛纔咱們把數據的展現過程當中對數據的處理,和組件直接作了解耦,也就是不在組件中轉換數據,而是在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<ApplicationState>){} 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<ApplicationState>, 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隱藏起來,只給組件暴露出了很友好的事件方法。