NgRx的解释

100 阅读6分钟

[

Dennis

](medium.com/@thecodinga…)

丹尼斯

关注

6月13日

-

6分钟阅读

[

拯救

](medium.com/m/signin?ac…)

NgRx的解释

很多时候,当我们的angular应用程序增长时,我们的代码变得相当混乱,难以遵循。我们有多个组件,引用了无数的服务,而且我们需要创建各种通道,以便在子组件和父组件之间传递数据。这使得我们原本应该是用户界面的前端应用被逻辑所污染,代码变得更加难以理解。

就像IT解决方案一样,把所有数据的管理外包给一个框架是很自然的。在这样做的时候,我们不再将任何服务直接注入到我们的组件中,我们也不会为了传递数据而在父子组件之间创建不必要的事件发射器和输入。相反,我们只关注我们的用户界面中需要的东西,当我们需要数据时,我们只需引用框架。因此,我们有效地将 "后端 "部分从我们的前端应用程序中分离出来。

这给我们开发前端应用程序的方式带来了范式上的转变。我们不再让各个组件管理自己的数据,而是将整个应用程序中的所有组件所需的所有数据通过一个单一的store 。而我们把这些数据称为state

假设我们有一个用angular编写的简单的CRUD应用程序来管理书籍,它应该在一个表中显示一个书籍列表,当用户想要创建新的书籍,或更新任何现有的书籍时,它应该显示一个弹出窗口来显示书籍的详细信息。

我们通过运行npm install @ngrx/store --save ,将ngrx添加到应用程序中。

我们做的第一步是定义我们的状态,我们将需要哪些数据,以及它们各自的数据类型是什么。这就像我们前端程序的database 。因此,我们创建一个名为state 的文件夹,并在新创建的文件夹中创建一个新文件books.state.ts

就像所有的数据库一样,我们不直接访问数据,而是创建getter方法来访问state 。为了确保我们的组件总是拥有最新的state ,而不是静态的getter方法,我们创建selectors ,以返回 [Observables](https://rxjs.dev/guide/observable)的单个状态,并让我们的组件订阅它。这样一来,state 的变化就会自动更新到我们的组件上,它们就可以对它做出相应的反应。因此,创建一个新的books.selector.ts ,以整合选择器。

createFeatureSelector 方法是由 ngrx 提供的,帮助我们直接通过名称和通用参数类型来选择状态。这就是为什么我们要在一个AppState 下创建实际的状态bookStore ,这样我们就可以有一个名字来选择。createSelector 方法在第一个参数中接受其他选择器,然后最后一个参数接受一个方法,该方法将使用传入的选择器来获得较低层次的状态。虽然在例子中没有提到,但是请注意,如果你的选择器依赖于多个选择器来决定获取哪个状态,那么你最多可以在参数中传递8个选择器。

我们从不使用setter方法直接更新state 。相反,state 只会根据某些规定的事件来改变。所以我们有actions ,来定义哪些是可能发生的事件。然后,我们有reducers ,它定义了应用程序的初始状态,以及该状态将如何变化,取决于发生了什么行动,以及行动发生前的状态。

行动是由一个字符串来识别的,它的有效载荷将被传递给还原器。Ngrx提供了createAction 方法,该方法接受行动的识别字符串,以及通用参数中的有效载荷的数据类型。像往常一样,创建一个books.actions.ts 来合并动作。

缩减器是纯函数,在给定动作和当前状态的情况下,返回一个新的状态。因此,为了有一个可以工作的状态,它需要在ngrx提供的createReducer 方法的第一个参数中定义一个初始状态。然后为每个动作提供后续的on 方法。一个books.reducer.ts ,然后就会被创建成这样。

注意,这只减少了bookStore 状态,而不是最高级别的AppState 。这样,如果在这个bookStore 的同一层次上还有其他状态,就可以创建不同的还原文件。这有助于保持代码的简洁和易读。然后就是问题了--how then do we map the reducer to the state? 。我们在导入StoreModule时,在forRoot 中添加链接 -StoreModule.forRoot({bookStore: booksReducer}) 。因此,该模块将看起来像这样。

另外注意到,并不是所有的动作都有一个还原器,例如,loadBooks 动作就缺少一个还原器。这是因为reducer的目的仅仅是为了更新状态,如果动作不改变状态,就不需要reducer了。

现在,你可能已经注意到缺少了一些东西,我们在哪里调用我们的服务来获取、更新、创建、删除数据?这将由副作用来处理。是的,副作用!因为我们的还原器应该在任何时候都提供最新的状态,而且我们有组件在订阅它,我们不能在还原器中运行异步进程。这就是为什么我们分别有一个loadData 和一个dataLoaded 动作。就像我们为每个动作定义还原器一样,我们也可以定义副作用,这些副作用将基于哪个动作被触发而运行。这些副作用将处理调用服务的异步工作,当工作完成后,它将触发另一个动作。然后,新动作的还原器可以再次更新状态。

为了有副作用,我们需要用npm install @ngrx/effects --save 来单独安装它。像往常一样,我们创建books.effects.ts

由于我们导入了一个新的效果模块,我们需要将它添加到我们的ngModule的imports 数组中,并且我们必须在forRoot中传递效果类的列表 -EffectsModule.forRoot([BooksEffects]) 。所以我们更新的模块将变成这样。

我们已经完成了为我们的部分创建所有后台的东西,现在让我们来看看我们如何使用它。首先,我们需要将@ngrx\store 导入我们的组件,并在构造函数中注入它。

import {Store} from "@ngrx/store";

然后,我们在ngOnInit 中订阅我们在组件中需要的所有选择器。注意,我们使用this.store.select(<selector>) 来引用我们先前创建的选择器。

而我们的东西被触发了,比如当添加按钮被点击时,我们不需要发出一个事件或处理逻辑,而是简单地调用this.store.dispatch(<action>) 来触发动作。

addBook(): void {  this.store.dispatch(BookStoreActions.newBook())}

如果该动作被定义为带有有效载荷,我们只需在动作的参数中传递数据。

showBook(book: Book): void {  this.store.dispatch(BookStoreActions.showBook({book}))}

因此,当动作被触发时,还原器会更新状态,由于我们已经通过选择器订阅了状态变化,所以只要有状态变化,我们的组件就会被更新。从Selector.showDetail 的订阅中可以看出,当showDetail 的状态发生变化时,应用程序将运行showPopup()dismissPopup()

应用程序的数据流可以直观地表现为这样。

本文描述的示例应用程序可在github.com/thecodingan…

这篇文章最初发表在 https://thecodinganalyst.github.io/knowledgebase/ngrx-explained/