背景
在当前angular项目中尝试使用状态管理。下面先来了解状态管理。
什么是状态管理
状态管理是一个十分广泛的概念,因为状态无处不在。服务端也有状态,Spring 等框架会管理状态,手机 App 也会把数据保存到手机内存里。
在前端技术中, 状态管理可以帮助你管理“全局”状态 - 那些应用程序的许多部分都需要的状态。比如组件之间共享的数据、页面需要及时更新的数据等等。
为什么需要状态管理
我们先来看一个 facebook
的例子。
在 Facebook 没有 Flux 这种状态管理架构以前,就有很多状态未同步的 bug:
在 Facebook 逛着逛着,突然来了几条通知,但是点进去之后竟然没有了,过了一会儿,通知又来了,点进去看到了之前的消息,但是新的还是没有来。
这种问题就跟他们在 Flux 介绍 中所描述的一样:
为了更好地描述Flux,我们将其与经典的MVC结构比较。在客户端MVC应用程序中,数据流大概如下
- 用户交互控制器,
- 控制器更新model,
- 视图读取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 匹配的状态库,大多数都是这两种:
其中, @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)])
);
});
- 组件使用
@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
NGXS是Angular的状态管理模式+库。
NGXS是以Redux和NgRx等库中普遍实施的CQRS模式为蓝本的,但是通过使用现代的TypeScript功能(例如类和装饰器)减少了样板。
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注解获取选择器,并订阅,来实时地更新数据列表。
工作流如图所示
感受
上手难度比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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。