5

背景

在当前angular项目中尝试使用状态管理。下面先来了解状态管理。

什么是状态管理

状态管理是一个十分广泛的概念,因为状态无处不在。服务端也有状态,Spring 等框架会管理状态,手机 App 也会把数据保存到手机内存里。

在前端技术中, 状态管理可以帮助你管理“全局”状态 - 那些应用程序的许多部分都需要的状态。比如组件之间共享的数据、页面需要及时更新的数据等等。

为什么需要状态管理

我们先来看一个 facebook 的例子。

在 Facebook 没有 Flux 这种状态管理架构以前,就有很多状态未同步的 bug:

在 Facebook 逛着逛着,突然来了几条通知,但是点进去之后竟然没有了,过了一会儿,通知又来了,点进去看到了之前的消息,但是新的还是没有来。

这种问题就跟他们在 Flux 介绍 中所描述的一样:

为了更好地描述Flux,我们将其与经典的MVC结构比较。在客户端MVC应用程序中,数据流大概如下

  1. 用户交互控制器,
  2. 控制器更新model,
  3. 视图读取model的新数据,更新并显示给用户

image.png


但是在多个控制器,模型,父子组件等添加下,依赖变得越来越复杂。

控制器不仅需要更新当前组件,也需要更新父子组件中的共享数据。

如下图,仅添加三个视图,一个控制器和一个模型,就已经很难跟踪依赖图。

image.png


这时,facebook 提出了 Flux架构 ,它的重要思想是:

单一数据源和单向数据流

单一数据源要求: 客户端应用的关键数据都要从同一个地方获取
单向数据流要求:应用内状态管理的参与者都要按照一条流向来获取数据和发出动作,不允许双向交换数据

image.png

举个例子:

一个页面中渲染了数据列表 books,这个 books 变量是通过 store 获取的,符合单向数据流。

但是这时候要添加一本书,我们调用了一个 api 请求 createBook.

通常有人为了方便,会在组件里面通过 http 调用请求后直接把新增的 book 加到列表里面。这就违反了单一数据源和单向数据流

假设这时另一个组件也在渲染数据列表 books, 这时候我们就需要手动更改这个组件中的数据。或者再次请求一次后台。

当这样的情况一多,意味着应用内的状态重新变成七零八落的情况,重归混沌,同步失效,容易bug从生。


在同一时期,Facebook 的 React 跟 Flux 一起设计出来,React 开始逐渐流行,同时在 Flux 的启发下,状态管理也正式走进了众多开发者的眼前,状态管理开始规范化地走进了人们的眼前。

状态模式的模式和库

Vue与React都有较为简单的全局状态管理策略,比如简单的store模式,以及第三方状态管理库,比如Rudux,Vuex等

Store

最简单的处理就是把状态存到一个外部变量里面,当然也可以是一个全局变量

var store = {
  state: {
    message: 'Hello!'
  },
  setMessageAction (newValue) {
    this.state.message = newValue
  },
  clearMessageAction () {
    this.state.message = ''
  }
}

image.png

但是当你认真观察上面的代码的时候,你会发现:你可以直接修改 store 里的 state.

万一组件瞎胡修改,不通过 action,那我们也没法跟踪这些修改是怎么发生的。

所以就需要规定一下,组件不允许直接修改属于 store 实例的 state,

也就是说,组件里面应该执行 action 来分发 (dispatch) 事件通知 store 去改变。

这样进化了一下,一个简单的 Flux 架构就实现了。

Flux 架构

Flux其实是一种思想,就像MVC,MVVM之类的。

image.png

Flux 把一个应用分成四部分:

  • View:视图层
  • Action:动作,即数据改变的消息对象。
  • Dispatcher:派发器,接收 Actions ,发给所有的 Store
  • Store:数据层,存放应用状态与更新状态的方法,一旦发生变动,就提醒 Views 更新页面

这样定义之后,我们修改store里的数据只能通过 Action , 并让 Dispatcher 进行派发。

可以发现,Flux的最大特点就是数据都是单向流动的。

同时也有一个特点 : 多Store

Redux

受 Facebook 的 Flux 启发, Redux 状态管理库演变出来,Dan Abramov 与 Andrew Clark于2015年创建。

React 的状态管理库中,Redux基本占据了最主流位置。

它和 Flux 有什么不同呢?

image.png

Redux 相比 Flux 有以下几个不同的点:

  • Redux是 单Store, 整个应用的 State 储存在单个 Store 的对象树中。
  • 没有 Dispatcher

Redux 的组成:

  • Store
  • Action
  • Reducer

store

Redux 里面只有一个 Store,整个应用的数据都在这个大 Store 里面。

import { createStore } from 'redux';
const store = createStore(fn);

Reducer

Redux 没有 Dispatcher 的概念,因为 Store 里面已经集成了 dispatch 方法。

import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
  type: 'ADD_TODO',
  payload: 'Learn Redux'
});

那 Reducer 负责什么呢?

负责计算新的 State

这个计算的过程就叫 Reducer。

它需要两个参数:

  • PreviousState,旧的State
  • action,行为

工作流如下
image.png

简单走一下工作流:

1.用户通过 View 发出 Action:
store.dispatch(action);

2.然后 Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。Reducer 会返回新的 State 。

let nextState = xxxReducer(previousState, action);

3.Store 返回新的 State 给组件

// 或者使用订阅的模式
let newState = store.getState();
component.setState(newState);   

总结

FluxRedux
store单store多store
dispatcher无,依赖reducer来替代事件处理器,store和dispatcher合并

这两种模式刚好对应着 angular 匹配的两种状态管理库: @ngxs/store , @ngrx/platform

下面会提到。

其他的模式和库

还有一些其他的库, 比如 Vue 用的 Vuex , Mobx等。这里就不一一介绍了。

Angular 需要状态管理吗

如果你是一个 Angular 的开发者,貌似没有状态管理框架也可以正常开发,并没有发现缺什么东西。那么在 Angular 中如何管理前端状态呢?

首先在 Angular 中有个 Service的概念。

Angular 区别于其他框架的一个最大的不同就是:基于 DI 的 service。也就是,我们基本上,不实用任何状态管理工具,也能实现类似的功能。

比如,如下代码就很简单地实现数据的管理:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { User } from './type';

export interface UserState {
  users: User[];
}

@Injectable({
  providedIn: 'root'
})
export class UserStateService {
  userState$: Observable<UserState>;

  private _userState$ = new BehaviorSubject<UserState>(null);
  constructor() {
    this.userState$ = this._userState$.asObservable();
  }

  get userState(): UserState {
    return this._userState$.getValue();
  }

  addUser(user: User): void {
    const users = [
      ...this.userState.users,
      user
    ];

    this._userState$.next({
      users
    })
  }
}

这样的状态流其实也很清晰,简单易维护,基本上不需要复杂的状态管理框架。


假如我们作为大型项目的 angular 开发者

如果我们想引入一个简单的第三方状态管理框架统一管理起来呢,应该选什么库?

Redux 是React社区提供的。angular 能用,但是总感觉是为了Redux而生的。
Vuex 是Vue 特有的状态管理库。
MobX 在大型项目中,代码难以维护

实际上,我们搜索与 angular 匹配的状态库,大多数都是这两种:

其中, @ngrx/platform 是Angular 的Redux 执行, 也就是说,和 Redux 很像。

而 @ngxs/store 更像是 Flux

为了进一步了解两个状态管理库,下面我简单对这两个管理库,尝试写了一些demo。

@ngrx/platform

NgRx 是在 Angular 中使用 RxJS 的 状态管理库,灵感來自 Redux(React 阵营被应用最广泛的状态管理工具),官方文档

实际上, Ngrx 比原生的 Redux 概念更多一些,比如 effect等。

工作流如下:
image.png

下面我们通过写一个demo,理解一下工作流。

假设有以下使用场景:

假设我们把 民族 这个字段单独拿出来创建一个表,而这个表交给用户维护,可以点 + 号来弹窗添加民族。
此时我们希望:弹窗添加后的民族,能够实时的更新到 民族列表 组件中。

示例

1.定义实体

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

2.定义actions

// 定义获取 nation 列表 的三种行为,获取,获取成功,获取失败
export const GET_NATIONS = '[Nation] GET_NATIONS';
export const GET_NATIONS_SUCCESS = '[Nation] GET_NATIONS_SUCCESS';
export const GET_NATIONS_ERROR = '[Nation] GET_NATIONS_ERROR';

/**
 * get All
 */
export class GetAllNations implements Action {
  readonly type = GET_NATIONS;
}

export class GetAllNationsSuccess implements Action {
  readonly type = GET_NATIONS_SUCCESS;

  constructor(public payload: Nation[]) {
  }
}

export class GetAllNationsError implements Action {
  readonly type = GET_NATIONS_ERROR;

  constructor(public payload: any) {
  }
}

3.定义Reducer
当收到 上面定义的 三种 actions 时,通过 swith case, 分别进行不同的处理, 更新State

// 定义 State接口
export interface State {
  data: Nation[];
  selected: Nation;
  loading: boolean;
  error: string;
}
// 初始化数据
const initialState: State = {
  data: [],
  selected: {} as Nation,
  loading: false,
  error: ''
};

export function reducer(state = initialState, action: AppAction ): State {
  switch (action.type) {
    case actions.GET_NATIONS:
      return {
        ...state,
        loading: true
      };
    case actions.GET_NATIONS_SUCCESS:
      return {
        ...state,
        data: action.payload,
        loading: true
      };
    case actions.GET_NATIONS_ERROR:
      return {
        ...state,
        loading: true,
        error: action.payload
      };
  }
  return state;
}

/*************************
 * SELECTORS
 ************************/
export const getElementsState = createFeatureSelector<ElementsState>('elements');

const getNationsState = createSelector(getElementsState, (state: ElementsState) => state.nation);

export const getAllNations = createSelector(getNationsState, (state: State) => {
    return state.loading ? state.data : [];
  }
);
export const getNation = createSelector(getNationsState, (state: State) => {
  return state.loading ? state.selected : {};
});

4.定义service

@Injectable()
export class NationService {
  protected URL = 'http://localhost:8080/api/nation';

  constructor(protected http: HttpClient) {
  }
  // 获取列表
  public findAll(params?: any): Observable<Nation[]> 
    return this.http.get<Nation[]>(this.URL, {params});
  }
  // 新增
  public save(data: Nation): Observable<Nation> {
    let headers = new HttpHeaders();
    headers = headers.set('Content-Type', 'application/json; charset=utf-8');
    
    return this.http.post<Nation>(this.URL, data, {headers});
  }
}

5.定义Effect,用于侦听 ation, 并调用api

@Injectable()
export class NationEffect {
  constructor(private actions$: Actions,
              private nationsService: NationService) {
  }

 // 侦听 GET_NATIONS, 并调用获取列表的service方法
  getAllNations$ = createEffect(() => {
    return this.actions$.pipe(
      tap((data) => console.log(data)),
      ofType(actions.GET_NATIONS),
      switchMap(() => this.nationsService.findAll()),
      map(response => new GetAllNationsSuccess(response)),
      catchError((err) => [new GetAllNationsError(err)])
    );
  });
}
  // 侦听 CREATE_NATION, 并调用新增的service方法
  createNations$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(actions.CREATE_NATION),
    map((action: AddNation) => action.payload),
    switchMap(newNation => this.nationsService.save(newNation)),
    map((response) => new AddNationSuccess(response.id)),
    catchError((err) => [new AddNationError(err)])
  );
  });
  1. 组件使用
@Component({
  selector: 'app-index',
  templateUrl: './index.component.html',
  styleUrls: ['./index.component.css']
})
export class IndexComponent implements OnInit {
  nations: Nation[] | undefined;

  show = false;

  model = {} as Nation;

  constructor(private store: Store<ElementsState>) {
  }

  ngOnInit(): void {
    // 初始化,执行 GetAllNations aciton,并分发
    this.store.dispatch(new GetAllNations());
    // 通过selector,获取 state中的数据
    this.store.select(getAllNations).subscribe((data) => {
      this.nations = data;
    });
  }


  save(): void {
    if (confirm('确认要新增吗?')) {
      const nation = this.model;
      // 执行新增 action
      this.store.dispatch(new AddNation(nation));
      this.show = false;
      this.model = {} as Nation;
    }
  }

}

全局的Store只有一个, 那我们怎么知道去哪个 State中获取呢?

答案是通过 selector进行选择。

7.定义selector

/**
 * nation的selector, 获取nation的 state
 */
export const getElementsState = createFeatureSelector<ElementsState>('elements');

const getNationsState = createSelector(getElementsState, (state: ElementsState) => state.nation);

export const getAllNations = createSelector(getNationsState, (state: State) => {
    return state.loading ? state.data : [];
  }
);
export const getNation = createSelector(getNationsState, (state: State) => {
  return state.loading ? state.selected : {};
});

走一遍工作流

image.png

1.组件执行 GetAllNations 的action,获取数据列表

ngOnInit(): void {
    this.store.dispatch(new GetAllNations());
 }

2.reducer 获取到 action, 返回 State, 这时候 State刚初始化,数据为空。

 switch (action.type) {
    case actions.GET_NATIONS:
      return {
        ...state,
        loading: true,
      };
}

3.同时 Effect 也获取到该 action

向service请求,获取后台数据后,执行 GetAllNationsSuccess action,表示成功获取到后台数据。

getAllNations$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(actions.GET_NATIONS),
      switchMap(() => this.nationsService.findAll()),
      map(response => new GetAllNationsSuccess(response)),
      catchError((err) => [new GetAllNationsError(err)])
    );
});

4.reducer 获取到 GetAllNationsSuccess action

将后台返回的数据更新到 State中。

case actions.GET_NATIONS_SUCCESS:
      return {
        ...state,
        data: action.payload,
        loading: true,
      };

5.组件利用 selector 获取 State 中的数据

this.store.select(getAllNations).subscribe((data) => {
      this.nations = data;
});

这时候,就完成了对 State 的订阅,数据的更新能够及时地收到。

从而实现了单一数据流和单向数据流。

新增操作也是类似的原理,可查看我写的 demo: https://stackblitz.com/edit/node-zpcxsq?file=src/app/nation/i...

我的感受

参考了几个谷歌上的demo写的。 实际使用起来感觉很臃肿。几点感觉:

1.对于一种aciton,比如获取数据列表,需要定义三种aciton, 分别是获取、获取成功、获取失败。

2.reducer 里大量使用了switch case, 页面代码很长。

3.需要定义Effect, 数据流多了一层,感觉不便。

4.代码需要有一定理解,新手上手会有点懵。

当然也有可能因为我没有对比多个状态管理库。下面我再尝试一下ngxs/Stroe.

ngxs/Stroe

NGXS是Angular的状态管理模式+库。

NGXS是以Redux和NgRx等库中普遍实施的CQRS模式为蓝本的,但是通过使用现代的TypeScript功能(例如类和装饰器)减少了样板。

image.png

NGXS有4个主要概念:

  • 存储(Store): 全局的状态容器, 动作调度和选择最终都体现它上面。
  • 动作(Actions): 描述要动作具体要干什么的类,以及其相关的元数据。
  • 状态(State): 定义状态的类
  • 选择(Selects): 状态选择器

开发过程:

1.定义actions, 新建action.ts文件

export const NationActionTypes = {
  getNations : '[Nation] getNations',
  getNation : '[Nation] getNationById',
  addNation : '[Nation] addNation',
  removeNation : '[Nation] removeNation',
  updateNation : '[Nation] updateNation',
}

export class GetAllNations {
  static  readonly type = NationActionTypes.getNations;
  // 查询参数
  constructor(public payload: {name: string}) {
  }
}

export class GetNationById {
  static readonly type = NationActionTypes.getNation;

  constructor(public payload: number) {
  }
}
...

2.新建nation.state.ts文件, 初始化State

export interface NationStateModel {
  nations: Nation[];
}

export const NationStateDefaults: NationStateModel = {
  nations: []
};

3.编写Action

@State<NationStateModel>({
  name: 'nation',
  defaults: NationStateDefaults
})
@Injectable()
export class NationState {

  constructor(private nationService: NationService) {
  }

  @Selector()
  static getNations(state: NationStateModel): Nation[] {
    return state.nations;
  }

  // 分页
  @Action(GetAllNations)
  page(
    stateContext: StateContext<NationStateModel>,
    action: GetAllNations
  ) {
    const param = action.payload;
    return this.nationService.findAll(param).pipe(tap(nations => {
      stateContext.patchState({
        nations: nations
      })
    }))
  }
 // 新增
 @Action(AddNation)
  addNation(
    stateContext: StateContext<NationStateModel>,
    action: AddNation
  ) {
    const toAddNation = action.payload;
    return this.nationService.save(toAddNation).pipe(tap(nation => {
      const state = stateContext.getState();
      stateContext.patchState({
        nations: _.concat(state.nations, nation)
      })
    }))
  }

@Action注解定义动作,接受 stateContext 上下文,以及payload。
并在调用service后,用patchState 来更新状态。

@Selector注解,定义State选择器。返回状态

4.组件调用

export class IndexComponent implements OnInit {

  @Select(NationState.getNations) nationPage$: Observable<Nation[]> | undefined;

  nations: Nation[] | undefined

  show = false;

  model = {} as Nation;

  constructor(private store: Store) {
  }

  ngOnInit(): void {
    this.store.dispatch(new GetAllNations({name: ''}));
    this.nationPage$?.subscribe((nations) => {
      this.nations = nations;
    })
  }

  delete(id: number): void {
    if (confirm('确认要删除吗?')) {
      this.store.dispatch(new RemoveNation(id));
    }
  }

  save(): void {
    if (confirm('确认要新增吗?')) {
      const nation = this.model;
      this.store.dispatch(new AddNation(nation));
      this.show = false;
      this.model = {} as Nation;
    }
  }
}

首先利用了全局的store方法来调用action,获取数据列表。
然后利用@Select注解获取选择器,并订阅,来实时地更新数据列表。

工作流如图所示
image.png

感受

上手难度比ngrx小不少,逻辑也很清晰,代码写起来感受良好

demo示例: https://stackblitz.com/edit/node-bgccni?file=README.md

参考:
https://zhuanlan.zhihu.com/p/140073055
https://cloud.tencent.com/developer/article/1815469
http://fluxxor.com/what-is-flux.html
https://zhuanlan.zhihu.com/p/45121775
https://juejin.cn/post/6890909726775885832


weiweiyi
1k 声望123 粉丝