关于angular:浅谈前端的状态管理以及anguar的状态管理库

36次阅读

共计 9088 个字符,预计需要花费 23 分钟才能阅读完成。

背景

在以后 angular 我的项目中尝试应用状态治理。上面先来理解状态治理。

什么是状态治理

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

在前端技术中,状态治理能够帮忙你治理“全局”状态 – 那些应用程序的许多局部都须要的状态。比方组件之间共享的数据、页面须要及时更新的数据等等。

为什么须要状态治理

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

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

在 Facebook 逛着逛着,忽然来了几条告诉,然而点进去之后居然没有了,过了一会儿,告诉又来了,点进去看到了之前的音讯,然而新的还是没有来。

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

为了更好地形容 Flux,咱们将其与经典的 MVC 构造比拟。在客户端 MVC 应用程序中,数据流大略如下

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

然而在多个控制器,模型,父子组件等增加下,依赖变得越来越简单。

控制器不仅须要更新以后组件,也须要更新父子组件中的共享数据。

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


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

繁多数据源和单向数据流

繁多数据源要求:客户端利用的 要害数据都要从同一个中央获取
单向数据流要求:利用内状态治理的参与者都要 依照一条流向来获取数据和收回动作,不容许双向替换数据

举个例子:

一个页面中渲染了数据列表 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 = ''}
}

然而当你认真察看下面的代码的时候,你会发现:你能够间接批改 store 里的 state.

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

所以就须要规定一下,组件不容许间接批改属于 store 实例的 state,

也就是说,组件外面应该执行 action 来散发 (dispatch) 事件告诉 store 去扭转。

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

Flux 架构

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

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 有什么不同呢?

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,行为

工作流如下

简略走一下工作流:

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);   

总结

Flux Redux
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 匹配的状态库,大多数都是这两种:

  • @ngxs/store
  • @ngrx/platform

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

而 @ngxs/store 更像是 Flux

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

@ngrx/platform

NgRx 是在 Angular 中应用 RxJS 的 状态治理库,灵感來自 Redux(React 营垒被利用最宽泛的状态管理工具),官网文档

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

工作流如下:

上面咱们通过写一个 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 : {};
});

走一遍工作流

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

待更新。。。。


参考:
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

正文完
 0