总结了 Angular 的状态管理方法
这篇文章是Angular Advent Calendar 2020的第21天的文章。
在近些年,状况管理已经成为与SPA密不可分的概念。然而,对于初学者而言,这个概念首先就是困难的。特别是在Angular中,它与RxJS紧密结合在一起,如果不懂RxJS,甚至无法进行状况管理,对于初学者来说是非常严苛的规定。
然而,一旦理解了Angular和RxJS特有的特性,就能够用简洁而优雅的代码来实现功能。
在这里,我想综合介绍一下Angular中状况管理的方法以及各种库的特点,以简单的待办事项应用为例。
这篇文章的目标读者
-
- Angularチュートリアルは一通りやってみたけど、やっぱりrxjsの概念が分からないよ…という方
-
- 状態管理したいけど、どうやってやればいいのかわからないという方
- ライブラリがありすぎてどれ選べばいいのかわからないという方
让我们了解RxJS的概念
鉴于状态管理是主题,我认为这是一个前言性的存在,但有些人可能不明白,所以我想在这里简要解释一下RxJS在做什么。
RxJS是一个JavaScript库,它的核心是Observable类,它可以用于保持状态和以响应式方式传递状态。它是Angular中各种状态管理库的核心组成部分,可说是非常重要的。
在这里,我将简单介绍BehaviorSubject和Observable。

首先,Observable是一个负责“订阅(Subscribe)某个数据源流动值”的类。在上述图中,数据源是BehaviorSubject,初始状态是BehaviorSubject中设置了{“hoge”: true}的值,但当组件上的按钮等触发更新事件时,通过BehaviorSubject.next()方法将数据源的值更改为{“hoge”: false}。然后,更改后的值{“hoge”: false}也会流向订阅它的组件中。
一方面,BehaviorSubject 是一个负责将值传递给 Observable 的数据源的类。它可以通过从外部调用 next 函数来更新值,并且更新后的值会实时传递给所有订阅了它的 Observable。与之类似的类还有 Subject,但是 BehaviorSubject 会“保持最后一次更新的值”,而 Subject 不会保持值。此外,在 Angular 中,用于父子组件之间进行数据交互的 EventEmitter 与 Subject 几乎相同。
如果用简单的源代码来表示这个,大致如下。
<pre><code>{{state$ | async | json}}</code></pre> <!-- asyncパイプを使うことにより購読 -->
<button (click)="setState({ hoge: true })">true</button>
<button (click)="setState({ hoge: false })">false</button>
import { RxjsExampleService } from './rxjs-example.service';
@Component({
selector: 'app-rxjs-example',
templateUrl: './rxjs-example.component.html',
styleUrls: ['./rxjs-example.component.scss'],
})
export class RxjsExampleComponent implements OnInit {
state$ = this.rxjsExampleService.state$.asObservable(); // Observableとして読み取り専用で参照する
constructor(private readonly rxjsExampleService: RxjsExampleService) {}
ngOnInit(): void {
this.state$.subscribe((state) => console.log(state)); // 購読
}
setState(state: { hoge: boolean }): void {
this.rxjsExampleService.state$.next(state); // BehaviorSubjectに新しい値を設定する
}
}
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class RxjsExampleService {
constructor() {}
state$ = new BehaviorSubject<{ hoge: boolean }>({ hoge: true });
}
点击按钮时,可以确认值会以反应方式进行变化。示例可以在此处找到:https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/rxjs-example
状态管理
好的,既然简单介绍了RxJS的概念,现在我想要介绍一些在Angular中进行状态管理的方法。
行为主题
总结
刚才我们介绍了BehaviorSubject可以“保持最后更新的值”,利用这个特性可以简单地将其作为存储使用。
不需要完全为存储创建一个新的服务类,也可以将存储功能添加到现有的服务类中。通过使用@Injectable({providedIn: ‘root’})将其指定为全局存储,使用@Component({providers: [SomeState]})将其指定为组件存储。
长处
-
- コンポーネントストア/グローバルストア両方で使える
-
- 追加のライブラリインストールが必要ない
- シンプル
弱点 suǒ)
-
- あくまで簡易的なものであることに留意しなければならない
- シンプルがゆえに凝ったことをしようとするとコード量が増える
实现示例
这里是示例。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/rxjs-state
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Todo } from '../types/todo';
import { v4 as uuid } from 'uuid';
@Injectable({
providedIn: 'root',
})
export class RxjsStateService {
private _todos$: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([
{
id: uuid(),
title: 'あ',
done: true,
},
{
id: uuid(),
title: 'い',
done: false,
},
]);
todos$: Observable<Todo[]> = this._todos$.asObservable();
constructor() {}
add(todo: Todo): void {
const current = this._todos$.getValue();
this._todos$.next([...current, todo]);
}
remove(todo: Todo): void {
const current = this._todos$.getValue();
const removed = current.filter((o) => o.id !== todo.id);
this._todos$.next(removed);
}
markAsDone(todo: Todo): void {
const current = this._todos$.getValue();
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = true;
this._todos$.next([...current]);
}
markAsUndone(todo: Todo): void {
const current = this._todos$.getValue();
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = false;
this._todos$.next([...current]);
}
}
private _todos$: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([
...
]);
todos$: Observable<Todo[]> = this._todos$.asObservable();
这部分的行为类似于一个存储器。通过使用asObservable(),它会将其转换为只能订阅的Observable类。当订阅这个Observable时,每次执行this._todos$.next()时,值会以响应式的方式改变。
如果需要非响应式的快照,则可以使用getValue()方法。
由于只使用了RxJS的基本功能进行实现,所以代码简洁且能减小构建大小,但在大规模开发或多人开发等情况下,代码风格可能会有所变化,因此最好事先明确定义代码规范。
另外,BehaviorSubject提供的只是“保持并向订阅者传递最后输入的值”的功能,所以所有其他方便的功能,如添加、更新、删除等,都需要手动编写。
import { Todo } from '../types/todo';
@Injectable()
export class RxjsStateComponentService {
private _list$: BehaviorSubject<Todo[]> = new BehaviorSubject<Todo[]>([]);
list$: Observable<Todo[]> = this._list$.asObservable();
set list(todos: Todo[]) {
this._list$.next(todos);
}
private _onlyViewNotDone$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
false
);
onlyViewNotDone$: Observable<boolean> = this._onlyViewNotDone$.asObservable();
set onlyViewNotDone(bool: boolean) {
this._onlyViewNotDone$.next(bool);
}
constructor() {}
}
这是一个组件存储区。
虽然它的结构几乎相同,但它只是简单地保存要显示的列表,所以使用setter函数而不是函数来更新值。
在全局存储区和组件存储区中,几乎可以使用相同的写法,这可能是一个优点,具体取决于情况。
NgRx –
NgRx 是一个用于管理应用程序状态和进行响应式编程的库。
总结
我认为在选择 Angular 状态管理库时,首先选择的应该是 NgRx。NgRx 在状态管理领域的介绍文章等资料非常丰富,已经成为事实上的标准。
虽然它主要用于全局状态管理,但最近也加入了组件状态管理的扩展,可以全面应用。
然而,由于受到 Redux 的影响,NgRx 变成了一个相当复杂的库,学习难度相对较高,这是一个缺点。
优点
-
- グローバルストア/コンポーネントストア両方で使える
-
- Redux・Vuexの使用経験があれば、比較的理解しやすい
-
- 厳格な設計なので、実装パターンに迷うことがない
- Redux devtoolsに対応していて、ストアの現在の状況をブラウザで簡単に表示可能
缺点
-
- いろんな要素全部入りのライブラリなので、使いこなすには苦労する+npm読みで1.31MBと重い
-
- ファイル数が多くなり見通しが悪くなりがち
- 学習難易度高し
NgRx根据用途分为多个子库,本次示例应用程序实现中使用了核心库@ngrx/store以及@ngrx/entity、@ngrx/store-devtools、@ngrx/component-store、@ngrx/schematics。
实施样本
这里给出了样本。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/ngrx-state
import { Update } from '@ngrx/entity';
import { createAction, props } from '@ngrx/store';
import { Todo } from 'src/app/types/todo';
export const loadNgrxStates = createAction('[NgrxState] Load NgrxStates');
export const loadNgrxStatesSuccess = createAction(
'[NgrxState] Load NgrxStates Success',
props<{ data: any }>()
);
export const loadNgrxStatesFailure = createAction(
'[NgrxState] Load NgrxStates Failure',
props<{ error: any }>()
);
export const addTodo = createAction(
'[NgrxState] Add Todo',
props<{ todo: Todo }>()
);
export const updateTodo = createAction(
'[NgrxState] Update Todo',
props<{ todos: Update<Todo> }>()
);
export const removeTodo = createAction(
'[NgrxState] Remove Todo',
props<{ id: string }>()
);
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { createReducer, on } from '@ngrx/store';
import { Todo } from 'src/app/types/todo';
import { addTodo, removeTodo, updateTodo } from './ngrx-state.actions';
export interface State extends EntityState<Todo> {}
export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>();
export const initialState: State = adapter.getInitialState();
export const reducer = createReducer(
initialState,
on(addTodo, (state, { todo }) => {
return adapter.addOne(todo, state);
}),
on(updateTodo, (state, { todos }) => {
return adapter.updateOne(todos, state);
}),
on(removeTodo, (state, { id }) => {
return adapter.removeOne(id, state);
})
);
const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = adapter.getSelectors();
export const selectTodoIds = selectIds;
export const selectTodoEntities = selectEntities;
export const selectAllTodos = selectAll;
export const selectTodoTotal = selectTotal;
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ngrxStateFeatureKey } from '.';
import * as fromNgrxState from './ngrx-state.reducer';
const getState = createFeatureSelector<fromNgrxState.State>(
ngrxStateFeatureKey
);
export const selectAllTodos = createSelector(
getState,
fromNgrxState.selectAllTodos
);
商店的编写方式与Redux/Vuex非常相似。在Actions中定义要对商店的值执行的操作,在Reducer中实现每个操作的具体行为,在Selector中实现从商店中获取值的处理程序,这是基本的流程。
由于编写方式非常规范,因此可以说无论谁编写,代码都会相同,这可能是一个优点。但代价是文件数量增多,视野不清晰,并且内部完全成为了黑盒子,所以对于不理解Flux架构的人来说,很难理解在做什么。
当然,并不需要手动编写所有这些,已经准备了用于自动生成模板的schematics,所以相对来说,工作量并不多。
此外,NgRx v10 中新增了一个名为 @ngrx/component-store 的子库,专为组件存储而设计。
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Todo } from 'src/app/types/todo';
type State = {
list: Todo[];
onlyViewNotDone: boolean;
};
@Injectable()
export class NgrxStateComponentStore extends ComponentStore<State> {
readonly list$ = this.select((state) => state.list);
readonly onlyViewNotDone$ = this.select((state) => state.onlyViewNotDone);
readonly setList = this.updater((state, todos: Todo[]) => ({
...state,
list: todos,
}));
readonly setOnlyViewNotDone = this.updater((state, bool: boolean) => ({
...state,
onlyViewNotDone: bool,
}));
constructor() {
super({ list: [], onlyViewNotDone: false });
}
}
可以像Akita那样,实现以组件为单位的状态管理,取代先前使用BehaviorSubject坚持不懈的实现方式。这种实现方式与Flux的风格有所不同。

秋田犬 tǎ
简而言之
这是一个简单的状态管理库,适用于Store/Query的两种类。它同时支持全局存储和组件存储,并且还有用于Redux Devtools的扩展库。与NgRx相比,它的知名度较低,npm的每周下载量只有NgRx的十分之一,但官方文档内容丰富,看一看就能大致了解大部分内容,尽管介绍文章较少。该库的设计更贴近Angular的功能,其中一些代表性功能/插件有「管理Angular Router的历史状态」「管理ReactiveForm的状态」「使用LocalStorage进行持久化存储」等。
优点
-
- グローバルストア/コンポーネントストア両方で使える
-
- 基本的な状態管理だけなら2クラスで完結するシンプルさ
-
- 設計の適度な緩さ
-
- 追加機能/プラグインが豊富
-
- Redux devtoolsに対応していて、ストアの現在の状況をブラウザで簡単に表示可能
- アイコンの秋田犬がかわいい
缺点
-
- npm読みで2.74MBと重い(NgRxと異なり、サブライブラリに分かれていないため)
-
- 圧倒的知名度の低さ
- 検索性が悪い(Akitaで検索すると秋田県が出てきます)
实施示例
这里是示例。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/akita-state
import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { Todo } from 'src/app/types/todo';
export interface TodosState extends EntityState<Todo, string> {}
@StoreConfig({ name: 'todos' })
@Injectable({ providedIn: 'root' })
export class AkitaStateStore extends EntityStore<TodosState> {
constructor() {
super();
}
markAsDone(todo: Todo): void {
this.update(todo.id, (entity) => ({
...entity,
done: true,
}));
}
markAsUndone(todo: Todo): void {
this.update(todo.id, (entity) => ({
...entity,
done: false,
}));
}
}
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { AkitaStateStore, TodosState } from './akita-state.store';
@Injectable({ providedIn: 'root' })
export class AkitaStateQuery extends QueryEntity<TodosState> {
todos$ = this.selectAll();
constructor(protected store: AkitaStateStore) {
super(store);
}
}
为了实现存储功能,只需要上述的两个类,非常简单。
名为EntityStore的类已经实现了各种便利函数来操作集合,只需要扩展它就可以用作存储。当然,也可以在Store类中自己实现函数,而且所有的便利函数都以公共函数的形式定义,所以可以直接从组件中使用。(我认为这是有争议的,所以在团队中制定统一的使用方式非常重要)
查询方面,只是扩展了一个名为QueryEntity的类,它已经实现了方便的函数来获取数据。
Akita的特点是具有”简约性”和相对容易理解的”适度弹性”,这使得即使是Angular新手也能够轻松上手。
虽然NgRx的设计是以函数为主,但Akita的设计仍然是以类为主,对于某些人来说可能会被视为”过时”,但考虑到与Angular的Service类的兼容性,我认为这是一个非常合理的设计。
import { Injectable } from '@angular/core';
import { guid, Query, Store } from '@datorama/akita';
type State = { onlyViewNotDone: boolean };
@Injectable()
export class AkitaStateComponentStore extends Store<State> {
constructor() {
super(
{
onlyViewNotDone: false,
},
{ name: `AkitaState-${guid()}` }
);
}
setOnlyViewNotDone(bool: boolean): void {
this.update({ onlyViewNotDone: bool });
}
}
@Injectable()
export class AkitaStateComponentQuery extends Query<State> {
onlyViewNotDone$ = this.select('onlyViewNotDone');
constructor(protected store: AkitaStateComponentStore) {
super(store);
}
}
组件存储也只需要按照全局存储相同的方式创建两个类。与全局存储的区别在于,由于@Injectable()不是在根级别使用,所以实例会被多次生成,因此在super()中为存储名称分配了一个唯一的ID。由于全局/组件之间的接口相同,所以在实现时不会产生困惑。

@药方角/国家
总结
这个库与以往的库有很大不同,它是为了实现”响应式和更智能的组件存储实现”而开发的。从这个角度来看,对RxJS有深入的理解是必要条件,但作为代替,它有一个无法替代的优点,那就是完全摆脱了Angular中最复杂的之一:Subscription管理的负担。
这个库最近成为热门话题,尽管仍然在发展中,但它已经具备Subscription管理无负担的能力,而且已经在初始状态下进行了Observable性能调优等工作,因此在目前已经成为一个非常强大的库。
优点 suǒ)
-
- 小難しいコードなしでパフォーマンスの高いコンポーネントストアを作れる
-
- Subscriptionを自分で管理する必要なし(!)
@Input() @Output()といったイベントとの親和性も高い
npm読みで891KB
缺点 (quē
-
- RxJSへの深い理解が必須
-
- グローバルストアとしても一応使えるが、使い勝手は悪い
- ストアの状態をブラウザ等で確認することはできない
示例实施
以下是示例:
请点击这里查看样例。
https://github.com/kaito3desuyo/angular-state-example/tree/master/src/app/rx-angular-state
由于这是一个以组件存储为主要的库,所以我们先介绍组件存储部分。
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { RxState } from '@rx-angular/state';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { v4 as uuid } from 'uuid';
import { Todo } from '../types/todo';
import {
GLOBAL_RX_STATE,
RxAngularGlobalState,
} from './store/rx-angular-state.state';
interface State {
list: Todo[];
onlyViewNotDone: boolean;
}
@Component({
selector: 'app-rx-angular-state',
templateUrl: './rx-angular-state.component.html',
styleUrls: ['./rx-angular-state.component.scss'],
providers: [RxState],
})
export class RxAngularStateComponent implements OnInit {
readonly list$ = this.state.select('list');
readonly onlyViewNotDone$ = this.state.select('onlyViewNotDone');
form = this.fb.group({
title: [''],
});
constructor(
private readonly fb: FormBuilder,
@Inject(GLOBAL_RX_STATE)
private readonly globalState: RxState<RxAngularGlobalState>,
private readonly state: RxState<State>
) {
this.state.set({
list: [],
onlyViewNotDone: false,
});
this.state.connect(
'list',
combineLatest([
this.globalState.select('todos'),
this.onlyViewNotDone$,
]).pipe(
map(([todos, onlyViewNotDone]) => {
if (onlyViewNotDone) {
return todos.filter((o) => !o.done);
} else {
return todos;
}
})
)
);
}
ngOnInit(): void {}
onClickAdd(): void {
const current = this.globalState.get('todos') ?? [];
this.globalState.set({
todos: [
...current,
{
id: uuid(),
title: this.form.get('title').value,
done: false,
},
],
});
this.form.reset({
title: '',
});
}
onClickRemove(todo: Todo): void {
this.globalState.set({
todos: [
...this.globalState
.get('todos')
.filter((o) => o.id !== todo.id),
],
});
}
onClickMarkAsDone(todo: Todo): void {
const current = this.globalState.get('todos') ?? [];
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = true;
this.globalState.set({
todos: [...current],
});
}
onClickMarkAsUndone(todo: Todo): void {
const current = this.globalState.get('todos') ?? [];
const index = current.findIndex((o) => o.id === todo.id);
current[index].done = false;
this.globalState.set({
todos: [...current],
});
}
onChangeOnlyViewNotDone(bool: boolean): void {
this.state.set({ onlyViewNotDone: bool });
}
}
虽然如此,没有必要创建单独的服务类,您只需要将RxState指定给要使用的组件的providers数组即可。初始化值等可以在constructor()中设置。
this.state.connect(
'list',
combineLatest([
this.globalState.select('todos'),
this.onlyViewNotDone$,
]).pipe(
map(([todos, onlyViewNotDone]) => {
if (onlyViewNotDone) {
return todos.filter((o) => !o.done);
} else {
return todos;
}
})
)
);
特徵的部分是這部分。this.state.connect()能夠”連接”到所創建的組件存儲的特定屬性,並自動訂閱並將來自指定的Observable的值設置到存儲中。RxState還會自動處理Observable的Subscription的銷毀,因此在處理Hot Observable時也很安全。
此外,我們還可以自動設置常用的Observable性能調優操作,例如distinctUntilChanged()和shareReplay(),從而創建高性能的組件存儲。
然而,方便用於作為存儲的函數的實現只有最基本的,所以在更新或刪除等操作方面,我們需要像管理BehaviorSubject一樣自己編寫代碼。這是因為(幾乎)不考慮將其用作全局存儲。
import { InjectionToken } from '@angular/core';
import { Todo } from 'src/app/types/todo';
import { RxState } from '@rx-angular/state';
export interface RxAngularGlobalState {
todos: Todo[];
}
export const GLOBAL_RX_STATE = new InjectionToken<
RxState<RxAngularGlobalState>
>('GLOBAL_RX_STATE');
@NgModule({
declarations: [AppComponent],
imports: [
...
],
providers: [
{
provide: GLOBAL_RX_STATE,
useFactory: () => new RxState<RxAngularGlobalState>(),
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
import { Component, Inject } from '@angular/core';
import { RxState } from '@rx-angular/state';
import {
GLOBAL_RX_STATE,
RxAngularGlobalState,
} from './rx-angular-state/store/rx-angular-state.state';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'angular-state-example';
constructor(
@Inject(GLOBAL_RX_STATE) private state: RxState<RxAngularGlobalState>
) {
this.state.set({ todos: [] });
}
}
虽然可以按照上述方式创建全球商店,但个人不建议,因为感觉效果不太好。
即使在官方文档中,创建全球商店也只被当作额外的功能处理,所以最好引入其他库,如NgRx或Akita库来处理全球商店。这些库本来就是为这样的使用场景而设计的。
概况
- ストアとして高機能なのは?
NgRx = Akita >>>> @rx-angular/state >> BehaviorSubject
- シンプルなのは?
BehaviorSubject >> Akita > @rx-angular/state >>>>>>>>>> NgRx
- 軽いのは?
BehaviorSubject >>>>> @rx-angular/state >>> NgRx(コアライブラリのみ) >> Akita
个人觉得,对于小到中等规模的开发来说,Akita可能已经足够了。NgRx作为基于Flux的强大设计,在多人团队中进行大规模开发时可能会有优势,但在小中等规模的开发中,Flux的Store则可能只会带来麻烦。
在这一点上,我觉得Akita非常简单但功能强大,并且对Angular的文化有很好的理解,因此对初学者来说,从BehaviorSubject Store升级非常易懂。不过,参考资料相对较少,需要边看官方网站边理解。
@rx-angular/state不仅仅是创建Store,更像是连接组件和全局Store的桥梁,并且非常出色。虽然使用起来有一些特点,但对于性能至关重要的应用程序可能会成为必不可少的。
从综合角度来判断,我个人喜欢Akita + @rx-angular/state,但我希望读者们能考虑各个库的优缺点、团队的技能水平以及产品所需的规范等因素,找到最适合的组合。