官方网址:https://v7.ngrx.io/guide/store
@ngrx/store是基于RxJS的状态管理库,其灵感来源于Redux。
Actions是NgRx中的主要模块之一。Actions表示从组件和服务中分派的唯一事件。从用户与页面的交互、通过网络请求的外部交互、与API的交互等等都可用action来描述。(可理解为客户端的数据库)
reducers 通过根据actions type来处理app中从一种状态到下一种状态的转换。reducers是纯函数,没有副作用,其返回值类型是由其输入值的类型决定的。可以同步处理每个状态的转换。每一个reducer都会处理最新的action dispatch,即当前的状态,并决定返回一个新修改的状态还是原始状态。(可理解为数据库中的数据表)
选择器是用于选择、派生和组合状态块的纯函数。
使用 createSelector和createFeatureSelector函数时,@ngrx/store会track调用 selector function的最新参数。因为selectors是纯函数,所以当参数匹配时可以返回最后一个结果,而无需重新调用selector function。这就带来了性能优势。这种做法被称为记忆法。
npm install @ngrx/store --save
如果你用的是Angular cli 6+ ,你可以使用以下命令
ng add @ngrx/store
Optional ng add
flags
- path - path to the module that you wish to add the import for the
StoreModule
to.- project - name of the project defined in your
angular.json
to help locating the module to add theStoreModule
to.- module - name of file containing the module that you wish to add the import for the
StoreModule
to. Can also include the relative path to the file. For example,src/app/app.module.ts
;- statePath - The file path to create the state in. By default, this is
reducers
.- stateInterface - The type literal of the defined interface for the state. By default, this is
State
.
此命令会自动添加以下:
- Update
package.json
>dependencies
with@ngrx/store
.- Run
npm install
to install those dependencies.- Create a
src/app/reducers
folder, unless thestatePath
flag is provided, in which case this would be created based on the flag.- Create a
src/app/reducers/index.ts
file with an emptyState
interface, an emptyreducers
map, and an emptymetaReducers
array. This may be created under a different directory if thestatePath
flag is provided.- Update your
src/app/app.module.ts
>imports
array withStoreModule.forRoot(reducers, { metaReducers })
. If you provided flags then the command will attempt to locate and update module found by the flags.
下面的例子是管理计数器的状态以及计数器在页面的显示。
开始之前,先确保你安装了angular的运行环境。在此就不赘述angular环境的安装了。
//src/app/counter.actions.ts
import {Action} from '@ngrx/store';
export enum ActionTypes{
Increment = '[Counter Component] Increment',
Decrement = '[Counter Component] Decrement',
Reset = '[Counter Component] Reset',
}
export class Increment implements Action{
readonly type = ActionTypes.Increment;
}
export class Decrement implements Action {
readonly type = ActionTypes.Decrement;
}
export class Reset implements Action {
readonly type = ActionTypes.Reset;
}
3. 创建一个reducer ,根据actions来处理计数器值的变化
// src/app/counter.reducer.ts
import { Action } from '@ngrx/store';
import { ActionTypes } from './counter.actions';
export const initialState = 0;
export function counterReducer(state = initialState,action:Action){
switch(action.type){
case ActionTypes.Increment:
return state + 1 ;
case ActionTypes.Decrement:
return state - 1;
case ActionTypes.Reset:
return 0;
default:
return state;
}
}
4. import StoreModule to app.module.ts
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
5. 在AppModule中的imports中添加 StoreModule.forRoot
// src/app/app.module.ts (StoreModule)
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ count: counterReducer })
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
6. 创建一个my-counter的组件
// src/app/my-counter/my-counter.component.html
Current count:{{count$ | async}}
// src/app/my-counter/my-counter.component.ts
import { Component } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Increment, Decrement, Reset } from '../counter.actions';
@Component({
selector: 'app-my-counter',
templateUrl: './my-counter.component.html',
styleUrls: ['./my-counter.component.css'],
})
export class MyCounterComponent {
count$:Observable;
constructor(private store:Store<{count:number}>){
this.count$ = store.pipe(select('count'))
}
increment(){
this.store.dispatch(new Increment());
}
decrement() {
this.store.dispatch(new Decrement());
}
reset() {
this.store.dispatch(new Reset());
}
}
7. 添加MyCounterComponent 到AppComponent template中
8.ok了,点击increment,decrement,reset就可以更改counter的状态了。
@ngrx/effects 提供一套API(装饰器@effect和action)来帮助检查store.dispatch()发出来的action。将特定类型的action过滤出来进行处理,监听特定的action,当特定的action发出之后,自动执行某些操作,然后将处理的结果重新发送回给store中。
npm install @ngrx/effects --save
以下例子通过比较service-based 和有effect的做一下对比
1. 传统的service-based的component:
//movies-page.component.ts
@Component({
template: `
{{ movie.name }}
`
})
export class MoviesPageComponent {
movies: Movie[];
constructor(private movieService: MoviesService) {}
ngOnInit() {
this.movieService.getAll().subscribe(movies => this.movies = movies);
}
}
// movies.service.ts
@Injectable({
providedIn: 'root'
})
export class MoviesService {
constructor (private http: HttpClient) {}
getAll() {
return this.http.get('/movies');
}
}
2. effects处理外部数据和交互。使用effects重构一下代码:
// movies-page.component.ts
@Component({
template: `
{{ movie.name }}
`
})
export class MoviesPageComponent {
movies$: Observable = this.store.select(state => state.movies);
constructor(private store: Store<{ movies: Movie[] >}) {}
ngOnInit() {
this.store.dispatch({ type: '[Movies Page] Load Movies' });
}
}
解析:the movies 任然是通过moviesService获取,但是component不在care movie是如何获取和加载的。component只负责声明加载movie的意图和使用select来访问movie。获取movie的异步操作是在effects中实现的。
3. 写effects
// movie.effects.ts
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
@Injectable()
export class MovieEffects {
@Effect()
loadMovies$ = this.actions$
.pipe(
ofType('[Movies Page] Load Movies'),
mergeMap(() => this.moviesService.getAll()
.pipe(
map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
catchError(() => EMPTY)
))
)
);
constructor(
private actions$: Actions,
private moviesService: MoviesService
) {}
}
解析:loadMovies$ effects 通过actions stream 监听所有的派发的actions,但是使用ofType之后,只对 [Movies Page] Load Movies 感兴趣。然后使用mergeMap()来将action stream映射到一个新的observable对象。this.moviesService.getAll()会返回一个observable,这个observable会将movies映射到一个新的action [Movies API] Movies Loaded Success,若错误发生,返回一个空对象。如果需要进行状态更改,这个action会被派发到store,由reducer处理。
4. 注册root effect 和 feature effect
// app.module.ts
import { EffectsModule } from '@ngrx/effects';
import { MovieEffects } from './effects/movie.effects';
@NgModule({
imports: [
EffectsModule.forRoot([MovieEffects])
],
})
export class AppModule {}
注:EffectsModule.forRoot( )
必须在AppModule下注册,如果不需要注册任何根级别的Effect,可以Provider一个空数组。
//admin.module.ts
import { EffectsModule } from '@ngrx/effects';
import { MovieEffects } from './effects/movie.effects';
@NgModule({
imports: [
EffectsModule.forFeature([MovieEffects])
],
})
export class MovieModule {}
注:通过forRoot 或forFeature 多次运行一个effect class,并不会导致effect会运行多次。forRoot和forFeature的加载效果在功能上是没有区别的,二者的区别在于forRoot设置了effects 所需的providers。