title: Angular 状态管理方案调研 layout: post thread: 247 date: 2020-05-08 author: Joe Jiang categories: Document tags: [Angular, Redux, ngrx, ngxs, State, 状态管理, 前端, JavaScript]
RxJs + Service 组件内管理状态: 在组件中可以声明一个属性,作为组件的内存存储。每次操作时调用服务(service)中的方法,然后手动更新状态。
export class TodoComponent {
todos : Todo[] = []; // 在组件中建立一个内存TodoList数组
constructor(
@Inject('todoService') private service,
) {}
addTodo(){
this.service
.addTodo('test') // 通过服务新增数据到服务器数据库
.then(todo => { // 更新todos的状态
this.todos.push(todo); // 使用了可改变的数组操作方式
});
}
}
RxJs + Service 组件只需访问,状态在服务中存储管理:在服务中定义一个内存存储,然后在更新服务数据后手动更新内存存储,组件中只需要访问该属性即可。
export class TodoService {
private _todos: BehaviorSubject;
private dataStore: { // 我们自己实现的内存数据存储
todos: Todo[]
};
constructor() {
this.dataStore = { todos: [] };
this._todos = new BehaviorSubject([]);
}
get todos(){
return this._todos.asObservable();
}
addTodo(desc:string){
let todoToAdd = {};
this.http
.post(...)
.map(res => res.json() as Todo) //通过服务新增数据到服务器数据库
.subscribe(todo => {
this.dataStore.todos = [...this.dataStore.todos, todo];
//推送给订阅者新的内存存储数据
this._todos.next(Object.assign({}, this.dataStore).todos);
});
}
}
类 Redux 管理方案 - ngrx & ngxs
其他未调研产品 - Akita & mobX & Redux & Flux
ngrx/store的灵感来源于Redux,是一款集成RxJS的Angular状态管理库,由Angular的布道者Rob Wormald开发。它和Redux的核心思想相同,但使用RxJS实现观察者模式。它遵循Redux核心原则,但专门为Angular而设计。
Actions - Actions是信息的载体,它发送数据到reducer,然后reducer更新store。Actions是store能接受数据的唯一方式。在ngrx/store里,Action的接口是这样的:
export interface Action {
type: string;
payload?: any;
}
Reducers - Reducers规定了行为对应的具体状态变化。它是纯函数,通过接收前一个状态和派发行为返回新对象作为下一个状态的方式来改变状态,新对象通常用Object.assign和扩展语法来实现。
export const todoReducer = (state = [], action) => {
switch(action.type) {
case 'ADD_TODO':
return [...state, action.payload];
default:
return state;
}
}
Store - store中储存了应用中所有的不可变状态。ngrx/store中的store是RxJS状态的可观察对象,以及行为的观察者。我们可以利用Store来派发行为。当然,我们也可以用Store的select()方法获取可观察对象,然后订阅观察,在状态变化之后做出反应。
Effects - Redux 中的 Reducer 已经是一个纯函数,而且是完全的只对状态数据进行处理的纯函数。在发出某个 Action 之后,Reducer 会对状态数据进行处理然后返回。但一般来说,其实在执行 Action 后我们还是经常会可以称为 Effect 的动作,比如:进行 HTTP 请求,导航,写文件等等。而这些事情恰恰是 Redux 本身无法解决的,@ngrx/effects 用于解决这类场景,一个 http 请求的示例如下 https://gist.github.com/hijiangtao/d4def77867ff4aec2740ba6ab83b24bf
@Component({
template: `
<div *ngFor="let movie of movies$ | async">
{{ movie.name }}
</div>
`
})
export class MoviesPageComponent {
movies$: Observable<Movie[]> = this.store.select(state => state.movies);
constructor(private store: Store<{ movies: Movie[] }>) {}
ngOnInit() {
this.store.dispatch({ type: '[Movies Page] Load Movies' });
}
}
可以结合 Redux Dewvtools 实现在线状态调试
ngrx 存在版本更迭,不少中文教程采用老 API 演示,如 StoreModule.provideStore / StoreModule.forRoot 等,以官方文档为准
官网 https://github.com/ngrx/platform
https://github.com/hijiangtao/ngrx-store-example
在ngxs出来之前,angular有ngrx(来自redux的灵感),这很棒,但实际使用起来会非常费力,你会花大量的时间去为每一个action写reducer、effect。当然,付出这些代价的同时,我们的应用程序逻辑变得十分清晰,组件与组件的耦合变得更加松散,最内层的组件甚至只需要使用input和output负责展示数据,因此changedetection也可以使用onpush策略,整个组件也变得更加易于测试和维护。
ngxs更加活用了angular的特性,使用装饰器,并且隐藏了reducer的概念,鼓励程序员使用rxjs进行一系列的流式处理,这在一定程度上大大缩减了我们的代码量,使得一些中小项目使用状态管理框架的成本变得很低。
语法与 Angular 现有的写法及运作方式几乎是一样的,学习门槛变得很低。
NgxsModule.forRoot([ZoosState])
即可action 定义 - 基本与 ngrx 类似
export class AddAnimal {
static readonly type = '[Zoo] Add Animal';
constructor(public name: string) {}
}
model 定义 - 即 state interface 定义
export interface ZooStateModel {}
建立 state - 通过 @State
decorator 来描述 state 的内容,Interface 建议以 Model 结尾,例如
@State<ZooStateModel>({
name: 'zoo',
defaults: {
feed: false
}
})
@Injectable() // 也可以依赖注入
export class ZooState {
constructor(private zooService: ZooService) {}
@Action(FeedAnimals)
feedAnimals(ctx: StateContext<ZooStateModel>) {
const state = ctx.getState();
ctx.setState({
...state,
feed: !state.feed
});
}
}
派发 dispatch - 在 comoponent view 上注入 store,然后进行派发 dispatch,操作过程中需要注意的是 dispatch 返回是空,如果需要获取 state 可以使用 @Select 进行链式调用
import { Store, Select } from '@ngxs/store';
import { Observable } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { AddAnimal } from './animal.actions';
@Component({ ... })
export class ZooComponent {
@Select(state => state.animals) animals$: Observable<any>;
constructor(private store: Store) {}
addAnimal(name: string) {
this.store
.dispatch([new AddAnimal('Panda'), new AddAnimal('Zebra')])
.pipe(withLatestFrom(this.animals$))
.subscribe(([_, animals]) => {
// do something with animals
this.form.reset();
});
}
}
select - 选中 state 的部分内容,具体使用可见上例
store.snapshot()
store.reset()
示例略
ngxs vs ngrx 概念对比
.forFeature()
实现(lazy loading modules);store.dispatch(new AddTodo('title'))
调用对应的 Action
方法 , 充分利用了 Angular 和 TypeScript 的特质;单一 store;