使用 RxJS 在 Angular 中进行简单而强大的状态管理
TLDR让我们仅使用 RxJS/BehaviorSubject 创建我们自己的状态管理类(受到一些众所周知的状态管理库的启发)。
使用 RxJS BehaviorSubject 管理状态
Angular 中有很多优秀的状态管理库,例如 NgRx、Akita 或 NgXs。它们都有一个共同点:它们基于 RxJS Observable,并且状态存储在一种特殊的 Observable 中:BehaviorSubject。
为什么选择 RxJS Observables?
- 可观察对象是 Angular 中的“一等公民”。Angular 的许多核心功能都由 RxJS 实现(例如 HttpClient、Forms、Router 等等)。使用可观察对象管理状态可以与 Angular 生态系统的其他部分完美集成。
- 使用 Observable 对象可以轻松地将状态变化通知给组件。组件可以订阅保存状态的 Observable 对象。这些“State” Observable 对象会在状态发生变化时发出新的值。
BehaviorSubject 有什么特别之处?
- BehaviorSubject 将其最后发出的值发送给新的/迟到的订阅者
- 它有一个初始值
- 可以通过getValue方法访问其当前值
- next可以使用方法发出新值
- BehaviorSubject 是多播的:它内部保存着所有订阅者的列表。所有订阅者共享同一个 Observable 执行。当 BehaviorSubject 发出新值时,相同的值会被推送给所有订阅者。
使用 BehaviorSubject 进行我们自己的状态管理
因此,如果所有大型状态管理库都使用 RxJS BehaviorSubject,并且 Angular 开箱即用 RxJS......我们可以仅使用 Angular Services 和 BehaviorSubject 创建自己的状态管理吗?
让我们创建一个简单但功能强大的状态管理类,可以通过 Angular 服务进行扩展。
主要目标是:
- 能够定义状态接口并设置初始状态
- 直接使用 API 来更新状态和选择状态:setState,select
- 选中状态应以 Observable 形式返回。选中状态发生变化时,Observable 会发出相应信号。
- 能够ChangeDetectionStrategy.OnPush在我们的组件中使用以获得更好的性能(在此处阅读有关 OnPush 的更多信息:“Angular onPush 变化检测策略综合指南”)。
解决方案:
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
export class StateService<T> {
  private state$: BehaviorSubject<T>;
  protected get state(): T {
    return this.state$.getValue();
  }
  constructor(initialState: T) {
    this.state$ = new BehaviorSubject<T>(initialState);
  }
  protected select<K>(mapFn: (state: T) => K): Observable<K> {
    return this.state$.asObservable().pipe(
      map((state: T) => mapFn(state)),
      distinctUntilChanged()
    );
  }
  protected setState(newState: Partial<T>) {
    this.state$.next({
      ...this.state,
      ...newState,
    });
  }
}
让我们仔细看看上面的代码:
- StateService 需要一个T表示状态接口的泛型类型。此类型在扩展 StateService 时传递。
- get state()返回当前状态快照
- 构造函数采用初始状态并初始化 BehaviorSubject。
- select- state$接受一个回调函数。该函数在发出新状态时被调用。在 RxJS 中- map,回调函数将返回一个状态片段。- distinctUntilChanged将跳过状态的发出,直到所选状态片段拥有新的值/对象引用。- this.state$.asObservable()确保该- select方法返回一个 Observable(而不是- AnonymousSubject)。
- setState接受一个部分类型。这允许我们偷懒,只传递更大状态接口的部分属性。在- state$.next方法内部,部分状态会与完整状态对象合并。最终,BehaviorSubject- this.state$会发出一个全新的状态对象。
用法
必须管理某些状态的 Angular 服务可以简单地扩展 StateService 来选择和更新状态。
世界上只有一件事需要管理:TODOS!:) 让我们创建一个 TodosStateService。
interface TodoState {
  todos: Todo[];
  selectedTodoId: number;
}
const initialState: TodoState = {
  todos: [],
  selectedTodoId: undefined
};
@Injectable({
  providedIn: 'root'
})
export class TodosStateService extends StateService<TodoState>{
  todos$: Observable<Todo[]> = this.select(state => state.todos);
  selectedTodo$: Observable<Todo> = this.select((state) => {
    return state.todos.find((item) => item.id === state.selectedTodoId);
  });
  constructor() {
    super(initialState);
  }
  addTodo(todo: Todo) {
    this.setState({todos: [...this.state.todos, todo]})
  }
  selectTodo(todo: Todo) {
    this.setState({ selectedTodoId: todo.id });
  }
}
我们来看一下 TodosStateService 代码:
- TodosStateService 扩展StateService并传递状态接口TodoState
- 构造函数需要调用super()并传递初始状态
- 公共的可观察对象todos$,并将selectedTodo$相应的状态数据暴露给感兴趣的消费者,如组件或其他服务
- 公共方法addTodo并selectTodo公开公共 API 来更新状态。
与组件和后端 API 的交互
让我们看看如何将 TodosStateService 与 Angular 组件和后端 API 集成:
- 组件调用 TodosStateService 的公共方法来更新状态
- 对状态感兴趣的组件只需订阅 TodosStateService 公开的相应公共 Observable。
- API 调用与状态密切相关。API 响应通常会直接更新状态。因此,API 调用由 TodosStateService 触发。API 调用完成后,可以使用以下方式立即更新状态: setState
演示
查看使用 TodosStateService 的完整 TODOs 应用程序:
Stackblitz - Angular State Manager
笔记
不可变数据
为了在组件中受益,ChangeDetectionStrategy.OnPush我们必须确保不改变状态。
 我们有责任始终将新对象传递给setState方法。如果我们要更新包含对象/数组的嵌套属性,那么我们也必须分配一个新的对象/数组。
有关不可变状态更新的更多示例,请参阅完整的TodosStateService(在 Stackblitz 上) 。
仅供参考,
 有一些库可以帮助您保持状态数据不变:
Immer 
ImmutableJS
具有双向数据绑定的模板驱动表单
关于不可变数据……在将状态推送到表单输入使用 的模板驱动表单时,我们必须小心[(ngModel)]。当用户更改表单输入值时,状态对象将直接变异……
 但我们希望保持不变,并且仅使用 显式更改状态setState。因此,使用响应式表单是更好的选择。如果必须使用模板驱动表单,那么仍然有一个很好的折衷方案:单向数据绑定[ngModel]。另一个选择是(深度)克隆表单数据……在这种情况下,您仍然可以使用[(ngModel)]。
  async订阅管道
大多数情况下,组件应该使用模板中的管道订阅“State”Observable  async 。异步管道会为我们订阅,并在组件销毁时自动处理取消订阅。
异步管道还有一个好处:
 当组件使用 OnPush 变更检测策略时,它们只会在以下情况下自动更新其视图:
- 如果@Input收到一个新的值/对象引用
- 如果组件或其子组件之一触发了 DOM 事件
在某些情况下,组件既没有 DOM 事件,也没有发生更改的 @Input。如果该组件订阅的状态在组件类内部发生变化,那么 Angular 变更检测将无法知道在观察到的状态发出后,视图需要更新。
您可以使用 来修复它ChangeDetectorRef.markForCheck()。它告诉 ChangeDetector 无论如何都要检查状态变化(在当前或下一个变更检测周期中),并在必要时更新视图。
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoShellComponent {
  todos: Todo[];
  constructor(
    private todosState: TodosStateService,
    private cdr: ChangeDetectorRef
  ) {
    this.todosState.todos$.subscribe(todos => {
      this.todos = todos;
      this.cdr.markForCheck(); // Fix View not updating
    });
  }
}
但我们也可以使用async模板中的管道。它会调用ChangeDetectorRef.markForCheck我们。请参阅 Angular 源代码:async_pipe
更短更漂亮:
<todo-list [todos]="todos$ | async"></todo-list>
异步管道功能强大,订阅、取消订阅、markForCheck 等功能应有尽有。让我们尽可能地利用它。
在演示中查看异步管道的实际运行:todo-shell.component.html
  select回调经常被调用
我们应该意识到,传递给该select方法的回调函数需要在每次调用时执行setState。
 因此,select 回调函数不应该包含繁重的计算。
多播已消失
如果该方法返回的 Observable 有多个订阅者,select那么我们会看到一些有趣的现象:BehaviorSubject 的多播功能消失了……select当状态发生变化时,传递给该方法的回调函数会被多次调用。Observable 会针对每个订阅者执行。
 这是因为我们使用 将 BehaviorSubject 转换为 Observable。Observablethis.state$.asObservable()不支持多播。
幸运的是,RxJS 提供了一个(多播)操作符来使 Observable 进行多播:shareReplay。
我建议只在需要的地方使用 shareReplay 操作符。假设 Observable 有多个订阅者todos$,那么我们可以像这样进行多播:
todos$: Observable<Todo[]> = this.select(state => state.todos).pipe(
    shareReplay({refCount: true, bufferSize: 1})
);
使用它refCount: true可以避免内存泄漏,这一点很重要。bufferSize: 1它将确保延迟订阅者仍然能够获得最后发出的值。
在此处阅读有关多播运算符的更多信息:RXJS 共享运算符的魔力及其差异
外观模式
还有一件好事。状态管理服务促进了外观模式:select和setState是受保护的函数。因此它们只能在 内部调用TodosStateService。这有助于保持组件的精简和整洁,因为它们无法直接使用setState/select方法(例如在注入的 TodosStateService 中)。状态实现细节保留在 TodosStateService 内部。
 外观模式使得将 TodosStateService 重构为其他状态管理解决方案(例如 NgRx)变得很容易——如果你愿意的话 :)
谢谢
特别感谢您审阅此博客文章:
对我有所启发的文章:
- 仅使用 Services 和 RxJS 在 Angular 中进行简单的状态管理,作者:Aslan Vatsaev
- 非常相似的方法:Ben Nadel在Angular 6.1.10 中使用 RxJS BehaviorSubject 创建一个简单的 setState() 存储
MiniRx
我将自己在响应式状态管理方面的经验融入到这个非常酷的库中:MiniRx Store。
 你可以轻松地将本文中的 StateService 重构为 MiniRx Feature Store。MiniRx Feature Store也支持setState和select,但你还可以获得额外的好处,例如 Redux DevTools 支持、撤销、强制不变性、效果、记忆化选择器等等。
 后端开发教程 - Java、Spring Boot 实战 - msg200.com
            后端开发教程 - Java、Spring Boot 实战 - msg200.com
          