Angular application architecture design -3: Ngrx Store

It is about Angular application architecture design of a series of articles in which this series, I will combine this experience of the past two years Angular, Ionic, and even Vuejs and other frameworks, summarize experience in application design and development process problems, and lessons learned, speaking about Angular application architecture design some of the problems associated with, including data exchange and communication between like component design, assembly, Ngrx Store of use, Rxjs use and responsive programming ideas. These design ideas and methods, not only for Angular, also applies to Vuejs, React other front-end framework.
Of course, application architecture design no one-size-fits-all standard, he can only be based on specific conditions. If you have a better idea, please share.

A portion of the presentation  using the Data Service mode to one-way data flow, event flow. This is in fact Redux mode, React, there Redux and Flux, in Angular, there are Ngrx. Let's take a combination of the previous one-way data flow of events, look Ngrx of components and their functions:
image description

After using Ngrx, all data on the Ngrx in the store, and by selectthe use of the way, selectout of the data is a subscription Observabledata objects; all modifications to the data, through a distribution actionby the reducerresponse to this event, the event the result of the process to update data inside the store, then it is by commitupdating data, updated data will inform the subscribers to update.

Angular relationship in Store, and store the components, and how the data and event interactions, is as shown in FIG. We look at how we can make good use Ngrx.

Modular, the state tree

Ngrx data in the data stored in the store, the save is called state, this state may be a tree, we can be the first stage of the tree structure as a module, and each module will also try to target the data inside the data in accordance with relationship itself, organized in a tree.

Let's look at a simple example, a user center page, the page design is as follows:
image description
contains some user information on the page, the balance owned by the user's wallet, coupons balances and other information, and a list of coupons and so on.

Accordingly, we store the data inside the structure, design roughly as follows:
image description
In this structure, we will state the entire app is divided into several modules, user information, order, shopping cart, merchandise, etc., then the user module, the included data user information, user information, user address, user coupons, wallets and so forth.

In this example, we put the user coupon information, wallets and other information on the user information inside these components using the methods and the relationship of these data is as follows:
image description

The state is so designed that the user:

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: []
}

We select this:

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

From this we can see select, select all of the root of the entire store are from the beginning, that is AppState. According to the tree down and then select a level, such as user information is state.user.account. When the data is modified inside the store, we are thus modified:

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
      })
    }
    ...
  }
}

We can see that this method reducer from this, Ngrx updated store the data in time, in the original state (state user module) on the update that object to update a reference to all the objects inside the state copy the reference to a new object. By way of this update, we can:

  1. Update the user state reference value.
  2. All the original data (in addition to being changed) references to the new state in order to assure that data has not been changed reference value is not modified.
  3. Modified data, its reference will be modified.

通过这样的修改方式,再加上我们从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} } } 

So that we UserServiceas a store and direct bridge components, the store's action to hide, only to components exposed very friendly event method.

Guess you like

Origin www.cnblogs.com/reaf/p/11274067.html