NgRx是用于在Angular中构建反应式应用程序的框架。NgRx提供状态管理,副作用隔离,实体集合管理,路由器绑定,代码生成以及开发人员工具,这些工具可增强开发人员在构建许多不同类型的应用程序时的体验。

为什么要使用NgRx进行状态管理?

NgRx通过存储单个状态和使用操作来表达状态更改,从而提供了用于创建可维护的显式应用程序的状态管理。

  • 可序列化
    通过规范化状态变化并将其传递给可观察对象,NgRx提供了可序列化性,并确保状态可预测地存储。这样可以将状态保存到外部存储,例如localStorage。
    此外,它还允许从Store Devtools检查,下载,上传和调度操作。

  • 类型安全
    依靠TypeScript编译器来保证程序正确性,从而在整个体系结构中提高了类型安全性。

  • 封装
    使用NgRx Effects和Store,可以将与外部资源副作用(例如网络请求,Web套接字和任何业务逻辑)的任何交互都与UI隔离。这种隔离允许使用更多纯净和简单的组件,并保持单一职责原则。

  • 可测试的
    由于Store使用纯函数来更改状态和从状态中选择数据,并且能够将副作用与UI隔离,因此测试变得非常简单。NgRx还提供诸如provideMockStore和provideMockActions用于隔离测试的测试设置,并提供更好的测试体验。

  • 性能
    存储建立在单个不变的数据状态上,使用OnPush策略使更改检测变成一项非常容易的任务。NgRx还由可记忆的选择器功能提供支持,这些选择器功能可优化状态查询计算。

何时应使用NgRx进行状态管理

当管理服务中的状态不再足够时,在构建具有大量用户交互和多个数据源的应用程序时,可能会使用NgRx。
SHARI原则可以回答“我何时需要NgRx”这个问题:

  • Shared:由许多组件和服务的访问的状态。
  • Hydrated:从外部存储持久化。
  • Available:状态,需要时可用重新进入路线。
  • Retrieved:必须附带副作用的状态。
  • Impacted:受其他来源的行动影响的状态。

@ngrx/store

Store是受Redux启发的RxJS支持的Angular应用程序状态管理。 Store是一个受控状态容器,旨在帮助在Angular上编写高性能,一致的应用程序。

关键概念

  • Actions: 动作描述从组件和服务调度的唯一事件
  • State: 状态更改由称为简化器的纯函数处理,这些函数采用当前状态和最新操作来计算新状态。
  • Selectors: 选择器是用于选择,导出和组成状态块的纯函数。
  • State是通过Store访问的,状态是可观察的,行为是观察者。

状态流

下图表示NgRx中应用程序状态的总体一般流程:

安装

// use npm 
npm install @ngrx/store --save
// use yarn 
yarn add @ngrx/store
// use ng add
ng add @ngrx/store

示例

以下教程向您展示如何管理计数器的状态,以及如何在Angular组件中选择和显示它:
1、创建一个名为counter.actions.ts的新文件,以描述递增,递减和重置其值的计数器动作。

// src/app/counter.actions.ts
import { createAction } from '@ngrx/store';

export const increment = createAction('[Counter Component] Increment');
export const decrement = createAction('[Counter Component] Decrement');
export const reset = createAction('[Counter Component] Reset');

2、根据提供的操作对reducer函数进行细化以处理计数器值的更改

// src/app/counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';

export const initialState = 0;

const _counterReducer = createReducer(initialState,
  on(increment, state => state + 1),
  on(decrement, state => state - 1),
  on(reset, state => 0),
);

export function counterReducer(state, action) {
  return _counterReducer(state, action);
}

3、从@ ngrx / store和counter.reducer文件导入StoreModule。

// src/app/app.module.ts (imports)

import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';

4、在AppModule的imports数组中添加StoreModule.forRoot函数,其中包含一个对象,该对象包含计数和管理计数器状态的counterReducer。 StoreModule.forRoot()方法注册在整个应用程序中访问商店所需的全局提供程序。

// src/app/app.module.ts (StoreModule)
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ count: counterReducer })
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

5、在app文件夹中创建一个名为my-counter的新组件。将存储服务注入到组件中以分派计数器动作,并使用select运算符从状态中选择数据。
使用按钮更新MyCounterComponent模板,以调用递增,递减和重置方法。使用异步管道订阅可观察的count。

// src/app/my-counter/my-counter.component.html
<button id="increment" (click)="increment()">Increment</button>

<div>Current Count: {{ count$ | async }}</div>

<button id="decrement" (click)="decrement()">Decrement</button>

<button id="reset" (click)="reset()">Reset Counter</button>

使用用于计数的选择器和用于调度Increment,Decrement和Reset操作的方法更新MyCounterComponent类

// src/app/my-counter/my-counter.component.ts
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset } from '../counter.actions';

@Component({
  selector: 'app-my-counter',
  templateUrl: './my-counter.component.html',
  styleUrls: ['./my-counter.component.css'],
})
export class MyCounterComponent {
  count$: Observable<number>;

  constructor(private store: Store<{ count: number }>) {
    this.count$ = store.pipe(select('count'));
  }

  increment() {
    this.store.dispatch(increment());
  }

  decrement() {
    this.store.dispatch(decrement());
  }

  reset() {
    this.store.dispatch(reset());
  }
}

6、将MyCounter组件添加到您的AppComponent模板中。

// src/app/app.component.html
<app-my-counter></app-my-counter>

单击递增,递减和重置按钮以更改计数器的状态。

让我们介绍一下您所做的事情:

  • 定义的事件表达动作。
  • 定义了一个reducer功能来管理计数器的状态。
  • 注册了整个应用程序中可用的全局状态容器。
  • 注入了存储服务以调度动作并选择计数器的当前状态。

通过actions、reducers和selectors了解NgRx应用程序的体系结构

Actios

Actions是NgRx中的主要构建块之一。 Actions表示在整个应用程序中发生的独特事件。 从用户与页面的交互,通过网络请求的外部交互以及与设备API的直接交互,这些以及更多事件均通过actions进行了描述。
NgRx中的Action由一个简单的接口组成:

// Action Interface
interface Action {
  type: string;
}

接口具有单个属性,即类型,表示为字符串。 type属性用于描述将在您的应用程序中分派的操作。 该类型的值以[Source] Event的形式出现,用于提供有关它是什么actions类别以及从哪里调度动作的上下文。 您向actions添加属性以为操作提供其他上下文或元数据。
让我们看一下启动登录请求的示例Action:

// login-page.actions.ts
import { createAction, props } from '@ngrx/store';

export const login = createAction(
  '[Login Page] Login',
  props<{ username: string; password: string }>()
);

createAction函数返回一个函数,该函数在被调用时将以Action接口的形式返回一个对象。 props方法用于定义处理动作所需的任何其他元数据。
Action创建者提供了一种一致的,类型安全的方式来构造要分派的Action。
使用Action创建者在分派时返回Action。

// login-page.component.ts
onSubmit(username: string, password: string) {
  store.dispatch(login({ username: username, password: password }));
}

login Action创建者将收到一个用户名和密码的对象,并返回一个普通的JavaScript对象,其类型属性为[Login Page] Login,并将用户名和密码作为附加属性。
返回的Action具有非常具体的上下文,关于操作来自何处以及发生了什么事件:

  • 操作的类别捕获在方括号[]中,该类别用于对特定区域的操作进行分组,无论是组件页面,后端API还是浏览器API。
  • 类别后的登录文本是有关此操作发生了什么事件的描述。在这种情况下,用户单击登录页面上的登录按钮以尝试使用用户名和密码进行身份验证。

Reducers

NgRx中的reducer负责处理应用程序中从一个状态到下一个状态的转换。 Reducer纯函数通过根据操作的类型确定要处理的操作来处理这些转换。
由Reducer管理的每个状态都有一些一致的部分:

定义state形状的接口或类型
参数包括初始状态或当前状态以及当前操作
处理state更改的功能及其相关

以下是一组处理记分板state的操作示例以及相关的reducer功能:
首先,定义一些与state交互的action。

// scoreboard-page.actions.ts
import { createAction, props } from '@ngrx/store';

export const homeScore = createAction('[Scoreboard Page] Home Score');
export const awayScore = createAction('[Scoreboard Page] Away Score');
export const resetScore = createAction('[Scoreboard Page] Score Reset');
export const setScores = createAction('[Scoreboard Page] Set Scores', props<{game: Game}>());

接下来,创建一个reducer文件,该文件导入action并为state块定义形状。

1、定义state形状

每个reducer函数都是一个action监听器。上面定义的记分板action描述了reducer可能处理的过渡。导入多组操作以处理reducer中的其他状态转换。

// scoreboard.reducer.ts
import { Action, createReducer, on } from '@ngrx/store';
import * as ScoreboardPageActions from '../actions/scoreboard-page.actions';

export interface State {
  home: number;
  away: number;
}
2.设置初始state值

初始状态为状态提供一个初始值,如果当前状态未定义,则提供一个值。 您将默认状态设置为所需状态属性的初始状态。

创建并导出变量以捕获具有一个或多个默认值的初始状态。

// scoreboard.reducer.ts
export const initialState: State = {
  home: 0,
  away: 0,
};
3.创建recucer函数

reducer功能的职责是以不变的方式处理状态转换。创建一个reducer函数,该函数处理使用createReducer函数管理计分板state的action。

const scoreboardReducer = createReducer(
  initialState,
  on(ScoreboardPageActions.homeScore, state => ({ ...state, home: state.home + 1 })),
  on(ScoreboardPageActions.awayScore, state => ({ ...state, away: state.away + 1 })),
  on(ScoreboardPageActions.resetScore, state => ({ home: 0, away: 0 })),
  on(ScoreboardPageActions.setScores, (state, { game }) => ({ home: game.home, away: game.away }))
);

export function reducer(state: State | undefined, action: Action) {
  return scoreboardReducer(state, action);
}

导出的reducer函数是必需的,因为AOT编译器不支持函数调用。

4、注册root state

应用程序的state被定义为一个大对象。注册reducer函数以管理部分state时,只会在对象中定义具有关联值的键。要在您的应用程序中注册全局store,请使用StoreModule.forRoot()方法以及定义您的state的键/值对的映射。 StoreModule.forRoot()注册应用程序的全局提供程序,包括您注入到组件和服务中的Store服务,以分派操作并选择状态

// app.module.ts

import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import * as fromScoreboard from './reducers/scoreboard.reducer';

@NgModule({
  imports: [
    StoreModule.forRoot({ game: fromScoreboard.reducer })
  ],
})
export class AppModule {}

使用StoreModule.forRoot()注册state可确保在应用程序启动时定义state。通常,您注册的root state始终需要立即对应用程序的所有区域可用。

6.注册功能state

功能state的行为与root state相同,但是允许您在应用程序中使用特定功能区域定义它们。 您的root state是一个大对象,而功能state会在该对象中注册其他键和值。

让我们从一个空的state对象开始。

import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';

@NgModule({
  imports: [
    StoreModule.forRoot({})
  ],
})
export class AppModule {}

这会将您的应用程序注册为root state为空的对象。

{}

现在,将记分板reducer与名为ScoreboardModule的NgModule功能一起使用,以注册其他state

// scoreboard.reducer.ts
export const scoreboardFeatureKey = 'game';


// scoreboard.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import * as fromScoreboard from './reducers/scoreboard.reducer';

@NgModule({
  imports: [
    StoreModule.forFeature(fromScoreboard.scoreboardFeatureKey, fromScoreboard.reducer)
  ],
})
export class ScoreboardModule {}

将ScoreboardModule添加到AppModule以加载state。

// app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { ScoreboardModule } from './scoreboard/scoreboard.module';

@NgModule({
  imports: [
    StoreModule.forRoot({}),
    ScoreboardModule
  ],
})
export class AppModule {}

一旦记分板模块被加载,game键就成为root state对象中的一个属性,并且现在处于state中进行管理

{
  game: { home: 0, away: 0 }
}

立即加载state还是延迟加载state取决于应用程序的需求。您可以使用功能state随着时间推移并通过不同的功能模块来构建state对象。

Selectors

Selectors是用于获取存储state切片的纯函数。 @ ngrx / store提供了一些帮助程序功能来优化此选择。选择state切片时,选择器提供许多功能:
可移植性 记忆化 组成 可测性 类型安全。
使用createSelector和createFeatureSelector函数时,@ ngrx / store会跟踪调用选择器函数的最新参数。 因为选择器是纯函数,所以当参数匹配时可以返回最后的结果,而无需重新调用选择器函数。 这可以提供性能优势,尤其是对于执行昂贵计算的选择器而言。 这种做法称为记忆。
对一个state使用选择器:

import { createSelector } from '@ngrx/store';

export interface FeatureState {
  counter: number;
}

export interface AppState {
  feature: FeatureState;
}

export const selectFeature = (state: AppState) => state.feature;

export const selectFeatureCount = createSelector(
  selectFeature,
  (state: FeatureState) => state.counter
);

对多个状态使用选择器:
createSelector可用于基于同一状态的多个切片从状态中选择一些数据。
createSelector函数最多可以使用8个选择器函数,以进行更完整的状态选择。
例如,假设您在状态中有一个selectedUser对象。 您还具有book对象的allBooks数组。
您想显示当前用户的所有书籍。
您可以使用createSelector来实现。 即使您在allBooks中对其进行了更新,您的可见图书也将始终是最新的。 如果选择了一本,它们将始终显示属于您用户的图书,而当未选择任何用户时,它们将显示所有图书。
结果将只是状态的另一部分过滤掉您的某些状态。 而且它将永远是最新的。

import { createSelector } from '@ngrx/store';

export interface User {
  id: number;
  name: string;
}

export interface Book {
  id: number;
  userId: number;
  name: string;
}

export interface AppState {
  selectedUser: User;
  allBooks: Book[];
}

export const selectUser = (state: AppState) => state.selectedUser;
export const selectAllBooks = (state: AppState) => state.allBooks;

export const selectVisibleBooks = createSelector(
  selectUser,
  selectAllBooks,
  (selectedUser: User, allBooks: Book[]) => {
    if (selectedUser && allBooks) {
      return allBooks.filter((book: Book) => book.userId === selectedUser.id);
    } else {
      return allBooks;
    }
  }
);

将选择器与props一起使用:
要根据store中不可用的数据选择state,可以将props传递给选择器功能。 这些props通过每个选择器和投影仪功能传递。 为此,我们必须在组件内部使用选择器时指定这些props。
例如,如果我们有一个计数器,并且想将其值相乘,则可以将相乘因子添加为prop:
选择器或投影仪的最后一个参数是props参数,在我们的示例中,它看起来如下:

export const getCount = createSelector(
  getCounterValue,
  (counter, props) => counter * props.multiply
);

在组件内部,我们可以定义props:

ngOnInit() {
  this.counter = this.store.pipe(select(fromRoot.getCount, { multiply: 2 }))
}

以下是使用以ID区分的多个计数器的示例。

export const getCount = () =>
  createSelector(
    (state, props) => state.counter[props.id],
    (counter, props) => counter * props.multiply
  );

组件的选择器现在正在调用工厂函数来创建不同的选择器实例:

ngOnInit() {
  this.counter2 = this.store.pipe(select(fromRoot.getCount(), { id: 'counter2', multiply: 2 }));
  this.counter4 = this.store.pipe(select(fromRoot.getCount(), { id: 'counter4', multiply: 4 }));
  this.counter6 = this.store.pipe(select(fromRoot.getCount(), { id: 'counter6', multiply: 6 }));
}