Angular 状态管理Angular 中仅使用服务和 RxJS 进行简单的状态管理
软件开发中最具挑战性的事情之一就是状态管理。目前,Angular 应用有多个状态管理库:NGRX、NGXS、Akita……它们都有不同的状态管理风格,其中最流行的是NGRX,它基本遵循React 世界的FLUX/Redux原则(基本上使用单向数据流和不可变数据结构,但使用 RxJS 可观察流)。
但是,如果您不想学习、设置、使用整个状态管理库,也不想处理简单项目的所有样板文件,如果您只想使用您作为 Angular 开发人员已经熟悉的工具来管理状态,并且仍然获得状态管理库提供的性能优化和一致性(推送更改检测、单向不可变数据流)。
免责声明:本文并非反对状态管理库。我们确实在工作中使用 NGRX,它确实帮助我们在非常大型且复杂的应用程序中管理非常复杂的状态,但正如我常说的,NGRX 会让简单的应用程序变得复杂,而会让复杂的应用程序变得简单,请记住这一点。
在本文中,我将向您展示一种仅使用RxJS和依赖注入来管理状态的简单方法,我们所有的组件树都将使用 OnPush 变化检测策略。
想象一下,我们有一个简单的 Todo 应用程序,我们想要管理它的状态,我们已经设置了组件,现在我们需要一个服务来管理状态,让我们创建一个简单的 Angular 服务:
// todos-store.service.ts
@Injectable({provideIn: 'root'})
export class TodosStoreService {
}
因此,我们需要一种提供待办事项列表的方法,一种添加、删除、过滤和完成待办事项的方法,我们将使用 getters/setters 和 RxJS 的行为主题来实现:
首先我们创建在 todos 中读写的方法:
// todos-store.service.ts
@Injectable({provideIn: 'root'})
export class TodosStoreService {
// - We set the initial state in BehaviorSubject's constructor
// - Nobody outside the Store should have access to the BehaviorSubject
// because it has the write rights
// - Writing to state should be handled by specialized Store methods (ex: addTodo, removeTodo, etc)
// - Create one BehaviorSubject per store entity, for example if you have TodoGroups
// create a new BehaviorSubject for it, as well as the observable$, and getters/setters
private readonly _todos = new BehaviorSubject<Todo[]>([]);
// Expose the observable$ part of the _todos subject (read only stream)
readonly todos$ = this._todos.asObservable();
// the getter will return the last value emitted in _todos subject
get todos(): Todo[] {
return this._todos.getValue();
}
// assigning a value to this.todos will push it onto the observable
// and down to all of its subsribers (ex: this.todos = [])
private set todos(val: Todo[]) {
this._todos.next(val);
}
addTodo(title: string) {
// we assaign a new copy of todos by adding a new todo to it
// with automatically assigned ID ( don't do this at home, use uuid() )
this.todos = [
...this.todos,
{id: this.todos.length + 1, title, isCompleted: false}
];
}
removeTodo(id: number) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
}
现在让我们创建一个允许我们设置待办事项完成状态的方法:
// todos-store.service.ts
setCompleted(id: number, isCompleted: boolean) {
let todo = this.todos.find(todo => todo.id === id);
if(todo) {
// we need to make a new copy of todos array, and the todo as well
// remember, our state must always remain immutable
// otherwise, on push change detection won't work, and won't update its view
const index = this.todos.indexOf(todo);
this.todos[index] = {
...todo,
isCompleted
}
this.todos = [...this.todos];
}
}
最后,可观察的源将仅向我们提供已完成的待办事项:
// todos-store.service.ts
// we'll compose the todos$ observable with map operator to create a stream of only completed todos
readonly completedTodos$ = this.todos$.pipe(
map(todos => todos.filter(todo => todo.isCompleted))
)
现在,我们的待办事项存储看起来像这样:
// todos-store.service.ts
@Injectable({providedIn: 'root'})
export class TodosStoreService {
// - We set the initial state in BehaviorSubject's constructor
// - Nobody outside the Store should have access to the BehaviorSubject
// because it has the write rights
// - Writing to state should be handled by specialized Store methods (ex: addTodo, removeTodo, etc)
// - Create one BehaviorSubject per store entity, for example if you have TodoGroups
// create a new BehaviorSubject for it, as well as the observable$, and getters/setters
private readonly _todos = new BehaviorSubject<Todo[]>([]);
// Expose the observable$ part of the _todos subject (read only stream)
readonly todos$ = this._todos.asObservable();
// we'll compose the todos$ observable with map operator to create a stream of only completed todos
readonly completedTodos$ = this.todos$.pipe(
map(todos => todos.filter(todo => todo.isCompleted))
)
// the getter will return the last value emitted in _todos subject
get todos(): Todo[] {
return this._todos.getValue();
}
// assigning a value to this.todos will push it onto the observable
// and down to all of its subsribers (ex: this.todos = [])
private set todos(val: Todo[]) {
this._todos.next(val);
}
addTodo(title: string) {
// we assaign a new copy of todos by adding a new todo to it
// with automatically assigned ID ( don't do this at home, use uuid() )
this.todos = [
...this.todos,
{id: this.todos.length + 1, title, isCompleted: false}
];
}
removeTodo(id: number) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
setCompleted(id: number, isCompleted: boolean) {
let todo = this.todos.find(todo => todo.id === id);
if(todo) {
// we need to make a new copy of todos array, and the todo as well
// remember, our state must always remain immutable
// otherwise, on push change detection won't work, and won't update its view
const index = this.todos.indexOf(todo);
this.todos[index] = {
...todo,
isCompleted
}
this.todos = [...this.todos];
}
}
}
现在我们的智能组件可以访问商店并轻松操作它:
(PS:我建议使用ImmutableJS而不是手动管理不变性)
// app.component.ts
export class AppComponent {
constructor(public todosStore: TodosStoreService) {}
}
<!-- app.component.html -->
<div class="all-todos">
<p>All todos</p>
<app-todo
*ngFor="let todo of todosStore.todos$ | async"
[todo]="todo"
(complete)="todosStore.setCompleted(todo.id, $event)"
(remove)="todosStore.removeTodo($event)"
></app-todo>
</div>
以下是完整的最终结果:
StackBlitz 上具有真实 REST API 的完整示例
这也是一种可扩展的状态管理方式,您可以使用 Angular 强大的 DI 系统轻松地将其他 store 服务相互注入,并使用管道运算符组合它们的可观察对象以创建更复杂的可观察对象,以及注入 HttpClient 等服务以从服务器提取数据。无需所有 NGRX 样板或安装其他状态管理库。尽可能保持其简洁轻量。
在 Twitter 上关注我,了解更多有趣的 Angular 相关内容:https://twitter.com/avatsaev
文章来源:https://dev.to/avatsaev/simple-state-management-in-angular-with-only-services-and-rxjs-41p8