原文出處:https://dev.to/avatsaev/simple-state-management-in-angular-with-only-services-and-rxjs-41p8
軟體開發中最具挑戰性的事情之一是狀態管理。目前,Angular 應用程式有幾個狀態管理庫:NGRX、NGXS、Akita...它們都有不同的狀態管理風格,最受歡迎的是 NGRX,它幾乎遵循React 世界的FLUX/Redux 原則(基本上使用單向資料流和不可變資料結構,但使用RxJS可觀察流)。
但是,如果您不想學習、設定、使用整個狀態管理庫,也不想處理一個簡單專案的所有樣板文件,如果您只想使用您已經熟悉的工具來*管理狀態,該怎麼辦?Anular 開發人員 *,並且仍然獲得狀態管理庫提供的效能最佳化和一致性(推送變更偵測,一種不可變資料流)。
*免責聲明:這不是針對狀態管理庫的帖子。我們確實在工作中使用NGRX,它確實幫助我們在非常大和複雜的應用程式中管理非常複雜的狀態,但正如我常說的,NGRX 使簡單應用程式的事情變得複雜,並簡化了複雜應用程式的事情,請記住這一點。 *
在這篇文章中,我將向您展示一種僅使用 RxJS 和 依賴注入 來管理狀態的簡單方法,我們所有的元件樹都將使用 OnPush 更改檢測策略。
想像我們有一個簡單的 Todo 應用程式,我們想要管理它的狀態,我們已經設定了元件,現在我們需要一個服務來管理狀態,讓我們建立一個簡單的 Angular 服務:
// todos-store.service.ts
@Injectable({provideIn: 'root'})
export class TodosStoreService {
}
所以我們需要的是,一種提供待辦事項清單的方法,一種加入、刪除、過濾和完成待辦事項的方法,我們將使用 getter/setter 和 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 系統輕鬆地將其他儲存服務注入彼此,將其可觀察量與管道運算符結合起來以建立更複雜的可觀察量,並注入HttpClient 等服務以從伺服器提取資料例如。無需所有 NGRX 樣板或安裝其他狀態管理庫。盡可能保持簡單和輕鬆。
在 Twitter 上關注我,以了解更多有趣的 Angular 相關內容:https://twitter.com/avatsaev