Angular应用架构设计-3:Ngrx Store

这是有关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

模块化、树状的state

在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对象里面的全部引用复制到一个新的对象里。经过这种更新方式,咱们就能够:

  1. 更新用户state的引用值。
  2. 将原先全部数据(除了被更改的)的引用复制到新的state中,这样就能保证没有被更改的数据的引用值没有修改。
  3. 被修改的数据,它的引用也会被修改。

经过这样的修改方式,再加上咱们从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种方式:

  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里面,让显示组件无需为了显示而处理数据。

视图模型的注意点

有一点有关视图模型须要特别说明的是,每当数据模型里面的数据修改时,全部跟这个数据有关的视图模型的订阅也会被触发。
举个例子,仍是上面的用户消息的例子。假设在咱们的消息数据中有一个属性是“是否回复”,也就是用户回复了一条消息后,标记为true。那么,若是用户打开一条以前已经读过的消息,而后进行回复。这时,用户的messages数据发生修改,那么上面的2个select的订阅器都会被触发。可是,这时候,有关未读消息数的这个数据实际上是没有改变的,但仍是被从新计算了一次。若是咱们select的结果是一个对象,这时候对象的引用值发生改变,那么在页面上的相应组件也会被刷新。

因此,在使用视图模型的时候必定要注意,你的select使用的数据必定要通过仔细设计,不能为了页面显示方便,就一股脑的从根的state获取好多数据并生成一个对象返回。这样会严重影响性能。

模型state和UI state

咱们保存在store中的数据,除了业务数据,其实咱们也能够把页面状态的数据保存到store中,也就是UI state。好比说一个典型的场景就是一个比较复杂的买票页,我可能须要输入购买数量,选择购买票的座位,有一些演唱会或项目还要求按照购买数量输入购买人的身份证号。若是咱们把这些数据也做为一个UI state模块,保存在store中,那么当用户因为一些缘由跳到了其余页面,而后再回来这个购买页的时候,以前输入的信息都还在。这样对用户的交互体验可能会更好,特别是在手机上。

使用UI state还有一个好处就是,咱们的store里面的数据彻底可以肯定页面的状态,不论是用户买票输入的内容,仍是支付的时候选择的支付方式等,都保存在store中。而后当咱们使用Ngrx的开发工具(chrome的DevTool插件)的时候,咱们能够选择任何一个历史的store的状态,这样页面就会按照这个时候的state来展现。这样,当咱们进行了一些操做之后,经过选择某一个时间点的state,就能重现当时那个时间的页面状态,这就是Ngrx里面所说的 Time Travel。

那么,哪些数据须要保存在store中?可使用下面两个简单的标准:

  1. 须要保存页面的状态。例如用户输入一些内容后,跳到其余页面,再回到以前页面,须要显示以前输入的内容。
  2. 须要频繁

进一步解耦组件跟数据模型

刚才咱们把数据的展现过程当中对数据的处理,和组件直接作了解耦,也就是不在组件中转换数据,而是在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隐藏起来,只给组件暴露出了很友好的事件方法。

相关文章
相关标签/搜索