Angular 官方框架本身提供了较为全面的功能,但是却缺少诸如 Redux,Vuex 之类的状态管理框架,而 NgRx 就是弥补这个缺陷的一个第三方框架。参考了 Redux 的思想,结合了 RxJs 流的处理

1、@ngrx/store

  • 基本使用
  1. // 1、创建 Action
  2. import { createAction } from '@ngrx/store';
  3. export const increment = createAction('[Counter Component] Increment');
  4. export const decrement = createAction('[Counter Component] Decrement');
  5. export const reset = createAction('[Counter Component] Reset');
  6. // 2、创建 Reducer
  7. import { createReducer, on } from '@ngrx/store';
  8. import { increment, decrement, reset } from './actions';
  9. export const initialState = 0;
  10. const _counterReducer = createReducer(initialState,
  11. on(increment, state => state + 1),
  12. on(decrement, state => state - 1),
  13. on(reset, state => 0),
  14. );
  15. export function counterReducer(state, action) {
  16. return _counterReducer(state, action);
  17. }
  18. // 3、AppModule 中引入
  19. import { BrowserModule } from '@angular/platform-browser';
  20. import { NgModule } from '@angular/core';
  21. import { AppComponent } from './app.component';
  22. import { StoreModule } from '@ngrx/store';
  23. import { counterReducer } from './reducer';
  24. @NgModule({
  25. declarations: [AppComponent],
  26. imports: [
  27. BrowserModule,
  28. StoreModule.forRoot({ count: counterReducer })
  29. ],
  30. providers: [],
  31. bootstrap: [AppComponent],
  32. })
  33. export class AppModule {}
  34. // 4、组件中调用
  35. import { Component } from '@angular/core';
  36. import { Store, select } from '@ngrx/store';
  37. import { Observable } from 'rxjs';
  38. import { increment, decrement, reset } from '../actions';
  39. @Component({
  40. selector: 'app-my-counter',
  41. templateUrl: './my-counter.component.html',
  42. styleUrls: ['./my-counter.component.css'],
  43. })
  44. export class MyCounterComponent {
  45. count$: Observable<number>;
  46. constructor(private store: Store<{ count: number }>) { //依赖注入
  47. this.count$ = store.pipe(select('count')); // 使用 select 函数取值
  48. }
  49. increment() {
  50. this.store.dispatch(increment());
  51. }
  52. decrement() {
  53. this.store.dispatch(decrement());
  54. }
  55. reset() {
  56. this.store.dispatch(reset());
  57. }
  58. }
  59. <button id="increment" (click)="increment()">Increment</button>
  60. <div>Current Count: {{ count$ | async }}</div> // 管道符取值
  61. <button id="decrement" (click)="decrement()">Decrement</button>
  62. <button id="reset" (click)="reset()">Reset Counter</button>
  • 进阶

1、createAction 第二个函数参数

  1. import { createAction, props } from '@ngrx/store';
  2. export const login = createAction(
  3. '[Login Page] Login',
  4. props<{ username: string; password: string }>()
  5. );
  6. onSubmit(username: string, password: string) {
  7. store.dispatch(login({ username: username, password: password }));
  8. }

2、feature State

  1. // 定义一个子状态
  2. import { createSelector, createFeatureSelector } from '@ngrx/store';
  3. export const featureKey = 'feature';
  4. export interface FeatureState {
  5. counter: number;
  6. }
  7. export interface AppState {
  8. feature: FeatureState;
  9. }
  10. export const selectFeature = createFeatureSelector<AppState, FeatureState>(featureKey);
  11. export const selectFeatureCount = createSelector(
  12. selectFeature,
  13. (state: FeatureState) => state.counter
  14. );
  15. // 2、子模块上注册子状态
  16. import { NgModule } from '@angular/core';
  17. import { StoreModule } from '@ngrx/store';
  18. import * as fromScoreboard from './reducers/scoreboard.reducer';
  19. @NgModule({
  20. imports: [
  21. StoreModule.forFeature(fromScoreboard.featureKey, fromScoreboard.reducer)
  22. ],
  23. })
  24. export class ScoreboardModule {}
  25. // 2、在根模块注册
  26. import { NgModule } from '@angular/core';
  27. import { StoreModule } from '@ngrx/store';
  28. import { ScoreboardModule } from './scoreboard/scoreboard.module';
  29. @NgModule({
  30. imports: [
  31. StoreModule.forRoot({}),
  32. ScoreboardModule
  33. ],
  34. })
  35. export class AppModule {}

3、createSelector 参数函数的第二个参数

  1. export const getCount = createSelector(
  2. getCounterValue,
  3. (counter, props) => counter * props.multiply
  4. );
  5. ngOnInit() {
  6. this.counter$ = this.store.pipe(select(fromRoot.getCount, { multiply: 2 }))
  7. }

4、meta-reducer
本质上相当于 Redux 的中间价

  1. import { StoreModule, ActionReducer, MetaReducer } from '@ngrx/store';
  2. import { reducers } from './reducers';
  3. // console.log all actions
  4. export function debug(reducer: ActionReducer<any>): ActionReducer<any> {
  5. return function(state, action) {
  6. console.log('state', state);
  7. console.log('action', action);
  8. return reducer(state, action);
  9. };
  10. }
  11. export const metaReducers: MetaReducer<any>[] = [debug];
  12. @NgModule({
  13. imports: [
  14. StoreModule.forRoot(reducers, { metaReducers })
  15. ],
  16. })
  17. export class AppModule {}

2、@ngrx/effects

effects 用于监听 action,是一种异步 action 处理方案,类似 redux-saga

1、基础

  1. // 1、创建 effect
  2. import { Injectable } from '@angular/core';
  3. import { Actions, createEffect, ofType } from '@ngrx/effects';
  4. import { of } from 'rxjs';
  5. import { map, mergeMap, catchError } from 'rxjs/operators';
  6. import { MoviesService } from './movies.service';
  7. @Injectable()
  8. export class MovieEffects {
  9. loadMovies$ = createEffect(() =>
  10. this.actions$.pipe(
  11. ofType('[Movies Page] Load Movies'),
  12. mergeMap(() => this.moviesService.getAll()
  13. .pipe(
  14. map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),
  15. catchError(() => of({ type: '[Movies API] Movies Loaded Error' }))
  16. )
  17. )
  18. )
  19. );
  20. constructor(
  21. private actions$: Actions,
  22. private moviesService: MoviesService
  23. ) {}
  24. }
  25. // 2、注册 effect
  26. import { EffectsModule } from '@ngrx/effects';
  27. import { MovieEffects } from './effects/movie.effects';
  28. @NgModule({
  29. imports: [
  30. EffectsModule.forRoot([MovieEffects])
  31. ],
  32. })
  33. export class AppModule {}
  34. // 或者注册 feature effect
  35. import { EffectsModule } from '@ngrx/effects';
  36. import { MovieEffects } from './effects/movie.effects';
  37. @NgModule({
  38. imports: [
  39. EffectsModule.forFeature([MovieEffects])
  40. ],
  41. })
  42. export class MovieModule {}

2、进阶

a、处理不 dispatch action 的 effect

  1. import { Injectable } from '@angular/core';
  2. import { Actions, createEffect } from '@ngrx/effects';
  3. import { tap } from 'rxjs/operators';
  4. @Injectable()
  5. export class LogEffects {
  6. constructor(private actions$: Actions) {}
  7. logActions$ = createEffect(() =>
  8. this.actions$.pipe(
  9. tap(action => console.log(action))
  10. ), { dispatch: false }); // 这里 dispatch:false 表示该action不会 dispatch
  11. }

b、effect 错误处理: 关闭 resubscriptions

  1. import { Injectable } from '@angular/core';
  2. import { Actions, ofType, createEffect } from '@ngrx/effects';
  3. import { of } from 'rxjs';
  4. import { catchError, exhaustMap, map } from 'rxjs/operators';
  5. import {
  6. LoginPageActions,
  7. AuthApiActions,
  8. } from '../actions';
  9. import { AuthService } from '../services/auth.service';
  10. @Injectable()
  11. export class AuthEffects {
  12. logins$ = createEffect(
  13. () =>
  14. this.actions$.pipe(
  15. ofType(LoginPageActions.login),
  16. exhaustMap(action =>
  17. this.authService.login(action.credentials).pipe(
  18. map(user => AuthApiActions.loginSuccess({ user })),
  19. catchError(error => of(AuthApiActions.loginFailure({ error })))
  20. )
  21. )
  22. // Errors are handled and it is safe to disable resubscription
  23. ),
  24. { useEffectsErrorHandler: false }
  25. );
  26. constructor(
  27. private actions$: Actions,
  28. private authService: AuthService
  29. ) {}
  30. }

c、自定义 effect 错误处理
EFFECTS_ERROR_HANDLER

3、其它

  • @ngrx/store-devtools
    调试工具
  • @ngrx/router-store
    跟 Angular 路由结合
  • @ngrx/entity
    方便 reducer 操作 state
  • @ngrx/data
    将 action、reducer、entity、effect 等进行了高度组合抽象,提供了一系列更新 entity 的 API。就是说数据的更新得用它的api来,不是很自由
  1. import { Component, OnInit } from '@angular/core';
  2. import { Observable } from 'rxjs';
  3. import { Hero } from '../../core';
  4. import { HeroService } from '../hero.service';
  5. @Component({
  6. selector: 'app-heroes',
  7. templateUrl: './heroes.component.html',
  8. styleUrls: ['./heroes.component.scss']
  9. })
  10. export class HeroesComponent implements OnInit {
  11. loading$: Observable<boolean>;
  12. heroes$: Observable<Hero[]>;
  13. constructor(private heroService: HeroService) {
  14. this.heroes$ = heroService.entities$;
  15. this.loading$ = heroService.loading$;
  16. }
  17. ngOnInit() {
  18. this.getHeroes();
  19. }
  20. add(hero: Hero) {
  21. this.heroService.add(hero);
  22. }
  23. delete(hero: Hero) {
  24. this.heroService.delete(hero.id);
  25. }
  26. getHeroes() {
  27. this.heroService.getAll();
  28. }
  29. update(hero: Hero) {
  30. this.heroService.update(hero);
  31. }
  32. }

参考

https://ngrx.io/docs