简单的讲,NgRx 是继 Redux 之后,结合 RxJs 的产物。可以说,它是 Angular 天生的“伴侣”,帮助 Angular 做状态管理。NgRx 主要有三种模式,分别为:@NgRx/Store、@NgRx/Effects 和 @NgRx/Entity。
这里我推荐大家结合后面三个实例(@ngrx/store、@ngrx/effects、@ngrx/entity)来理解。这三份文档都是英文版的,为了方便,我专门在另一篇文章中写了我的阅读笔记,大家可以参阅。
三种模式中,从 @NgRx/Store 到 @NgRx/Effects 再到 @NgRx/Entity,我们可以看做一个逐渐优化完善的过程。
在了解了三种模式的关系之后,我将详细为大家解释三种模式的代码结构和执行原理。
首先,@NgRx/Store 同样遵守 Redux 的三个基本原则:
单一数据源: 应用中所有的 state 都存储在对象树中,同时该对象树只能存在于唯一的 store 中。
State是只读的: state 只能通过 action 动作改变。
使用纯函数来执行修改: reducer 用来描述 action 如何改变状态树。
接下来,我将结合@ngrx/store上边的例子来解释一下,action 的触发到state的改变的过程: (建议读者对照实例来理解)
首先,在 article.component.ts 中,定义了一个 articles 的 Observable 观察者对象,用来接收Article[]类型的 state。同时,你可以看到在 article.component.html 中,通过 *ngFor 命令来遍历 articles 对象的值。
(循环中的 async 起到的是一个订阅的作用,当articles对象变化时,用来异步显示对象值)。
接下来,当 article.component.html 页面中的 “Java Articles” 按钮被点击时,会调用 article.component.ts 中的 showJavaArticles() 函数,触发函数中的 dispatch() 函数,将 JavaArticlesAction 抛出去。如此,article.actions.ts 中的 JavaArticlesAction 类就被触发了,其type为JAVA。
接着,article.reducer.ts 中的 reducer 函数接收到了 JAVA 类型的 action,执行相应的 case 语句,返回一个新的状态 JAVA_ARTICLES
最后,article.component.ts 中的 articles 对象接收返回的新状态,由 article.component.html 异步显示JAVA_ARTICLES(new state)的值。运行结果就是文档中的那个运行结果截图。
补充:
前边我们已经说到 @NgRx/Effects 是在 @NgRx/Store 基础上,添加了 Effect 。那么,问题来了:为什么要加 Effect,好处在哪里,必须要加 Effect 吗?接下来,我来慢慢给大家解释:
如果你有学过 Redux 的话,你应该知道 Redux 中的 reducer 是一个纯函数,执行过程中不能包含有副作用的 action。
在 Redux 的官方文档中有这样几句话:
Reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
1)修改传入参数;
2)执行有副作用的操作,如 API 请求和路由跳转;
3)调用非纯函数,如 Date.now() 或 Math.random()。
谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
那么,纯函数和副作用分别又是什么?
纯函数:一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
函数副作用:指函数被调用,完成了函数既定的计算任务,但同时因为访问了外部数据,尤其是因为对外部数据进行了写操作,从而一定程度地改变了系统环境。
到此,你应该已经明白为什么 reducer 中不能有包含副作用的 action 了吧。细细想一下,@NgRx/Effects 中加入 Effect 的原因也就清楚了。简单的讲,它就是为了处理这些包含副作用的 action 的。
下面我们结合 @ngrx/effects 来讲解一下 Effect 的作用。
(讲之前,我想给大家提个建议,我希望大家在读我下面的解释前,先对比一下 @ngrx/store 和 @ngrx/effects 实例代码,大概明白它们的差别即可,这样更有助于对 @NgRx/Effect 的理解。非常感谢您能够耐心的阅读!)
首先看 article.component.ts ,和 @NgRx/Store 中的例子很相似,先定义一个 Observable 的观察者对象 articles$,用来存储 select() 方法获取的缓存中的state。
当点击了 article.component.html 界面的 “Show All Articles” 按钮时,会调用 article.component.ts 中的 loadAllArticles() 方法,同时触发 dispatch() 函数,抛出 ShowAllAction 动作,对照 article.actions.ts,它的type是 SHOW_ALL。
接着 article.effects.ts 中的 @Effect() 装饰器检查 dispatch 的 action。将已经指定的 SHOW_ALL 动作类型捕捉到,然后执行 switchMap 操作,调用 articleService 服务,从 API 中获取 articles对象。如果获取成功,则生成一个新的 action -> .ShowAllSuccessAction,并将获取的 data 附加在.ShowAllSuccessAction 的参数 payload 上。
接下来由 article.reducer.ts 中的 reducer 函数接收新生成的action -> SHOW_ALL_SUCCESS,并将新的 state (即 ShowAllSuccessAction 携带的 payload)存到 store 中。
最后,article.component.ts 中,调用 store.select方法获取 store 中的数据,通过 async 异步加载到 article.component.html 上。
总结:
@NgRx/Effects 中提供了一个 @Effect() 装饰器,用来捕捉 Store.dispatch() 出来的特定的 Action(这些 Action 都是包含有副作用的),然后,调用服务后,根据返回值,生成新的 Action (新的 Action 不包含副作用),然后,由 Reducer 将新的 state 返回到store 中。
补充一些RxJs的操作符,如下:
switchMap:映射成 Observable,完成前一个内部 Observable,发出值。当使用 switchMap 时,源 Observable 发出值时,每个内部订阅都是完成的,只允许存在一个活动的内部订阅。
mergeMap(别名:flatMap):映射成 Observable 并发出值。mergeMap 允许同一时间存在多个活动的内部订阅。正因为如此,mergeMap 最常见的用例便是不会被取消的请求,可以将其考虑成写而不是读。flatMap 发射的数据集是无序的。
concatMap:将值映射成内部 Observable,并按顺序订阅和发出。concatMap 发射的数据集是有序的。
exhaustMap:映射成内部 Observable,忽略其他值直到该 Observable 完成。
do: 注册一个动作并可以执行不同的可观察生命周期事件。透明地执行操作或副作用,比如打印日志。
Debounce Time: 只有在特定的一段时间经过后并且没有发出另一个源值,才从源 Observable 中发出一个值。
map: 它将给定的功能应用于每个项目。对源 Observable 的每个值应用投射函数。
catch: 它可以观察序列处理错误。 我们需要从 catch 函数返回Observable。 我们可以使用函数的 RxJS 将一个值转换为 Observable。
在对比了 @NgRx/Store 和 @NgRx/Effects 之后,相应大家对它们的执行过程,以及它们之间的区别已经很清楚了。@NgRx/Entity 相对 @NgRx/Store 和 @NgRx/Effects,除了添加了实体管理之外,添加了 EntityState、EntityAdapter 接口 和 createEntityAdapter 方法。可以说,这些就是 @NgRx/Entity 的核心。
下面我直接结合 @ngrx/entity 实例来讲解它的核心。
首先,还是从 article.component.ts 和 article.component.html 来看,你会发现接收数据、触发 dispatch() 方法的方式和另两种模式是一样的。这里我们不在赘述。
接着,用来捕捉特定 Action 的 @Effect() 装饰器 也和之前 @NgRx/Effect 中的 Effect 完全一致。实例为了突出添加实体管理的好处,Effect 中只写了一个 @Effect() 装饰器,用来从 API 中获取所有 state。Effect 返回新生成的 action -> LoadArticlesSuccess。
接着,我们来看 article.adapter.ts,接收 reducer 返回的数据后,先经过 sortByCategory() 方法,依照对象的 category 属性来排序。再由 adapter.getSelectors() 中的4个 select 方法 (selectIds / selectEntities / selectAll / selectTotal) 对数据进行过滤。再由 article.reducer.ts 中的 createFeatureSelector() 和 createSelector() 方法缓存起来。
最后,依然是通过 article.component.ts 中的 Observable 对象接收 store.select() 获取来的数据,再由 article.component.html 中的 async 异步显示。
总结:
@NgRx/Entity 添加了状态管理之后,我们可以更加方便的管理 store 中的数据。还有许多细节的内容,大家可以参考 @ngrx/entity 实例。
特别注意:项目涉及每个实体都是唯一的,必须有唯一的 id 标识,即:在定义Model时,必须包含 id 字段,id 不可以写成 articleId 或其他形式。否则,数据无法正常获取显示。望读者注意
补充:
@NgRx/Entity 中增添了 Adapter,而 Adapter 包含了一套对实体操作的方法,如:(addOne, addMany, addAll)、(updateOne, updateMany)、(upsertOne, upsertMany)、(removeOne, removeMany, removeAll)。并且扩展了 Selectors 方法,添加了 selectAll、selectEntities、selectIds、selectTotal 等方法,方便对实体的操作。
Adapter 方法包含下面几种方式:
•addOne:在集合中添加一个实体
•addMany:添加几个实体
•addAll:添加所有的实体,用一个新集合替换整个集合
•removeOne:移除一个实体
•removeMany:删除几个实体
•removeAll:清除整个集合
•updateOne:更新一个现有实体
•updateMany:更新多个现有实体
•upsertOne:更新或插入一个实体
•upsertMany:更新或插入多个实体getSelectors 方法返回 NgRx EntitySelectors,它提供从实体集合中选择信息的功能。 EntitySelectors 的功能如下:
•selectIds:选择 id 数组。
•selectEntities:选择实体字典。 我们可以用它来获取 id 的实体。
•selectAll:选择所有实体的数组。
•selectTotal:选择实体的总数。
感谢您的阅读!