NgRx

1. 概述

NgRxAngular 应用中实现全局状态管理的 Redux 架构解决方案

  1. @ngrx/store:全局状态管理模块
  2. @ngrx/effects:处理副作用
  3. @ngrx/store-devtools:浏览器调试工具,需要依赖 Redux Devtools Extension
  4. @ngrx/schematics:命令行工具,快速生成 NgRx 文件
  5. @ngrx/entity:提高开发者在 Reducer 中操作数据的效率
  6. @ngrx/router-store:将路由状态同步到全局 Store

2. 快速开始

  1. 下载 NgRx

    npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/router-store @ngrx/store-devtools @ngrx/schematics
    
  2. 配置 NgRx CLI

    ng config cli.defaultCollection @ngrx/schematics
    
    // angular.json
    "cli": {
      "defaultCollection": "@ngrx/schematics"
    }
    
  3. 创建 Store

    ng g store State --root --module app.module.ts --statePath store --stateInterface AppState
    
  4. 创建 Action

    ng g action store/actions/counter --skipTests
    
    import { createAction } from "@ngrx/store"
    
    export const increment = createAction("increment")
    export const decrement = createAction("decrement")
    
  5. 创建 Reducer

    ng g reducer store/reducers/counter --skipTests --reducers=../index.ts
    
    import { createReducer, on } from "@ngrx/store"
    import { decrement, increment } from "../actions/counter.actions"
    
    export const counterFeatureKey = "counter"
    
    export interface State {
      count: number
    }
    
    export const initialState: State = {
      count: 0
    }
    
    export const reducer = createReducer(
      initialState,
      on(increment, state => ({ count: state.count + 1 })),
      on(decrement, state => ({ count: state.count - 1 }))
    )
    
  6. 创建 Selector

    ng g selector store/selectors/counter --skipTests
    
    import { createFeatureSelector, createSelector } from "@ngrx/store"
    import { counterFeatureKey, State } from "../reducers/counter.reducer"
    import { AppState } from ".."
    
    export const selectCounter = createFeatureSelector<AppState, State>(counterFeatureKey)
    export const selectCount = createSelector(selectCounter, state => state.count)
    
  7. 组件类触发 Action、获取状态

    import { select, Store } from "@ngrx/store"
    import { Observable } from "rxjs"
    import { AppState } from "./store"
    import { decrement, increment } from "./store/actions/counter.actions"
    import { selectCount } from "./store/selectors/counter.selectors"
    
    export class AppComponent {
      count: Observable<number>
      constructor(private store: Store<AppState>) {
        this.count = this.store.pipe(select(selectCount))
      }
      increment() {
        this.store.dispatch(increment())
      }
      decrement() {
        this.store.dispatch(decrement())
      }
    }
    
  8. 组件模板显示状态

    <button (click)="increment()">+</button>
    <span>{{ count | async }}</span>
    <button (click)="decrement()">-</button>
    

3. Action Payload

  1. 在组件中使用 dispatch 触发 Action 时传递参数,参数最终会被放置在 Action 对象中

    this.store.dispatch(increment({ count: 5 }))
    
  2. 在创建 Action Creator 函数时,获取参数并指定参数类型。

    import { createAction, props } from "@ngrx/store"
    export const increment = createAction("increment", props<{ count: number }>())
    
    export declare function props<P extends object>(): Props<P>;
    
  3. Reducer 中通过 Action 对象获取参数。

    export const reducer = createReducer(
      initialState,
      on(increment, (state, action) => ({ count: state.count + action.count }))
    )
    

4. MetaReducer

metaReducerAction -> Reducer 之间的钩子,允许开发者对 Action 进行预处理 (在普通 Reducer 函数调用之前调用)

function debug(reducer: ActionReducer<any>): ActionReducer<any> {
  return function (state, action) {
    return reducer(state, action)
  }
}

export const metaReducers: MetaReducer<AppState>[] = !environment.production
  ? [debug]
  : []

5. Effect

需求:在页面中新增一个按钮,点击按钮后延迟一秒让数值增加

  1. 在组件模板中新增一个用于异步数值增加的按钮,按钮被点击后执行 increment_async 方法

    <button (click)="increment_async()">async</button>
    
  2. 在组件类中新增 increment_async 方法,并在方法中触发执行异步操作的 Action

    increment_async() {
      this.store.dispatch(increment_async())
    }
    
  3. 在 Action 文件中新增执行异步操作的 Action

    export const increment_async = createAction("increment_async")
    
  4. 创建 Effect,接收 Action 并执行副作用,继续触发 Action

    ng g effect store/effects/counter --root --module app.module.ts --skipTests
    

    Effect 功能由 @ngrx/effects 模块提供,所以在根模块中需要导入相关的模块依赖

    import { Injectable } from "@angular/core"
    import { Actions, createEffect, ofType } from "@ngrx/effects"
    import { increment, increment_async } from "../actions/counter.actions"
    import { mergeMap, map } from "rxjs/operators"
    import { timer } from "rxjs"
    
    // createEffect
    // 用于创建 Effect, Effect 用于执行副作用.
    // 调用方法时传递回调函数, 回调函数中返回 Observable 对象, 对象中要发出副作用执行完成后要触发的 Action 对象
    // 回调函数的返回值在 createEffect 方法内部被继续返回, 最终返回值被存储在了 Effect 类的属性中
    // NgRx 在实例化 Effect 类后, 会订阅 Effect 类属性, 当副作用执行完成后它会获取到要触发的 Action 对象并触发这个 Action
    
    // Actions
    // 当组件触发 Action 时, Effect 需要通过 Actions 服务接收 Action, 所以在 Effect 类中通过 constructor 构造函数参数的方式将 Actions 服务类的实例对象注入到 Effect 类中
    // Actions 服务类的实例对象为 Observable 对象, 当有 Action 被触发时, Action 对象本身会作为数据流被发出
    
    // ofType
    // 对目标 Action 对象进行过滤.
    // 参数为目标 Action 的 Action Creator 函数
    // 如果未过滤出目标 Action 对象, 本次不会继续发送数据流
    // 如果过滤出目标 Action 对象, 会将 Action 对象作为数据流继续发出
    
    @Injectable()
    export class CounterEffects {
      constructor(private actions: Actions) {
        // this.loadCount.subscribe(console.log)
      }
      loadCount = createEffect(() => {
        return this.actions.pipe(
          ofType(increment_async),
          mergeMap(
            () => timer(1000)
            .pipe(
              map(() => increment({ count: 10 }))
            )
          )
        )
      })
    }
    

6. Entity

6.1 概述

Entity 译为实体,实体就是集合中的一条数据

NgRx 中提供了实体适配器对象,在实体适配器对象下面提供了各种操作集合中实体的方法,目的就是提高开发者操作实体的效率

6.2 核心

  1. EntityState:实体类型接口

    /*
      {
        ids: [1, 2],
        entities: {
          1: { id: 1, title: "Hello Angular" },
          2: { id: 2, title: "Hello NgRx" }
        }
      }
    */
    export interface State extends EntityState<Todo> {}
    
  2. createEntityAdapter: 创建实体适配器对象

  3. EntityAdapter:实体适配器对象类型接口

    export const adapter: EntityAdapter<Todo> = createEntityAdapter<Todo>()
    // 获取初始状态 可以传递对象参数 也可以不传
    // {ids: [], entities: {}}
    export const initialState: State = adapter.getInitialState()
    

6.3 实例方法

6.4 选择器

// selectTotal 获取数据条数
// selectAll 获取所有数据 以数组形式呈现
// selectEntities 获取实体集合 以字典形式呈现
// selectIds 获取id集合, 以数组形式呈现
const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors();
export const selectTodo = createFeatureSelector<AppState, State>(todoFeatureKey)
export const selectTodos = createSelector(selectTodo, selectAll)

7. Router Store

7.1 同步路由状态

  1. 引入模块

    import { StoreRouterConnectingModule } from "@ngrx/router-store"
    
    @NgModule({
      imports: [
        StoreRouterConnectingModule.forRoot()
      ]
    })
    export class AppModule {}
    
  2. 将路由状态集成到 Store

    import * as fromRouter from "@ngrx/router-store"
    
    export interface AppState {
      router: fromRouter.RouterReducerState
    }
    export const reducers: ActionReducerMap<AppState> = {
      router: fromRouter.routerReducer
    }
    

7.2 创建获取路由状态的 Selector

// router.selectors.ts
import { createFeatureSelector } from "@ngrx/store"
import { AppState } from ".."
import { RouterReducerState, getSelectors } from "@ngrx/router-store"

const selectRouter = createFeatureSelector<AppState, RouterReducerState>(
  "router"
)

export const {
  // 获取和当前路由相关的信息 (路由参数、路由配置等)
  selectCurrentRoute,
  // 获取地址栏中 # 号后面的内容
  selectFragment,
  // 获取路由查询参数
  selectQueryParams,
  // 获取具体的某一个查询参数 selectQueryParam('name')
  selectQueryParam,
  // 获取动态路由参数
  selectRouteParams,
  // 获取某一个具体的动态路由参数 selectRouteParam('name')
  selectRouteParam,
  // 获取路由自定义数据
  selectRouteData,
  // 获取路由的实际访问地址
  selectUrl
} = getSelectors(selectRouter)
// home.component.ts
import { select, Store } from "@ngrx/store"
import { AppState } from "src/app/store"
import { selectQueryParams } from "src/app/store/selectors/router.selectors"

export class AboutComponent {
  constructor(private store: Store<AppState>) {
    this.store.pipe(select(selectQueryParams)).subscribe(console.log)
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容