第 3 部分。构建你的 Pokédex:使用 create* 函数改进 NgRX 简介 createAction createEffects Reducers 结论更多、更多、更多……

2025-06-08

第 3 部分:构建你的 Pokédex:使用 create* 函数改进 NgRX

介绍

创建动作

创建效果

Reducers

结论

更多,更多,更多……

这篇文章是系列文章的一部分,我将在其中描述如何使用 NGRX 从初学者到忍者构建你的 Pokédex ,如果你想阅读更多内容,你可以阅读以下文章:


封面-1

介绍

在本文中,我们将使用Angular框架和NgRX作为状态管理库来开发一个神奇宝贝图鉴。我们将使用 NgRX 8 中新增的 create* 函数。

为了正确理解本文,建议您先了解如何在中级水平上管理 Angular,以及什么是状态管理库。在本系列中,我们将展示如何开发一个具体示例(Pokédex),以补充您对 NgRX 的学习。

首先,沿着这些柱子建造的结果如下面的 GIF 所示。

为了理解正在构建的内容,请务必阅读本系列文章的第一部分和第二@ngrx/entity部分。在本文中,我们将使用包中的 create* 函数改进本系列之前开发的代码,这将简化创建 action、reducer 和 effect 所需的样板代码。

创建动作

在 NgRX 中,创建操作需要大量的样板代码。您经常需要创建枚举、操作类型、类和联合类型。在新版本中,您可以更轻松地创建操作。

NgRX 核心团队使用了著名的工厂函数设计模式来实现这一目标。这个工厂函数是createAction。 createAction 函数接收两个参数:

  1. 动作类型。是用于标识动作的著名字符串。
  2. props。是动作元数据。例如,payload

为了比较两者,下面的代码说明了如何在我们的 Pokédex 中使用新的createAction函数。


export class LoadPokemon implements Action {
  readonly type = PokemonActionTypes.LOAD_POKEMONS;

  constructor() {}
}

export class LoadPokemonSuccess implements Action {
  readonly type = PokemonActionTypes.LOAD_POKEMONS_SUCCESS;

  constructor(public payload: Array<Pokemon>) {}
}


loadPokemonFailed = createAction(
   PokemonActionTypes.LOAD_POKEMONS_FAILED,
   props<{ message: string }>()
 ),

add: createAction(PokemonActionTypes.ADD, props<{ pokemon: Pokemon }>()),

在之前的代码中,您需要创建一个实现接口的类Action,定义type属性并payload使用构造函数。另一方面,在之后的代码中,您只需要使用函数创建操作createAction,其中第一个参数是type,第二个参数是props属性(在我们的上下文中,它将是有效载荷)。

尽管核心团队表示不需要使用枚举,但在我的特定编码风格中,我更喜欢定义一个动作枚举来了解动作集。

因此,前后情况如下 pokemon.action.ts

import { Action } from '@ngrx/store';
import { Pokemon } from '@models/pokemon.interface';

export enum PokemonActionTypes {
  ADD = '[Pokemon] Add',
  ADD_SUCCESS = '[Pokemon] Add success',
  ADD_FAILED = '[Pokemon] Add failed',
  LOAD_POKEMONS = '[Pokemon] Load pokemon',
  LOAD_POKEMONS_SUCCESS = '[Pokemon] Load pokemon success',
  LOAD_POKEMONS_FAILED = '[Pokemon] Load pokemon failed',
  UPDATE = '[Pokemon] Update',
  UPDATE_SUCCESS = '[Pokemon] Update success',
  UPDATE_FAILED = '[Pokemon] Update failed',
  DELETE = '[Pokemon] Delete',
  DELETE_SUCCESS = '[Pokemon] Delete success',
  DELETE_FAILED = '[Pokemon] Delete failed'
}

export class LoadPokemon implements Action {
  readonly type = PokemonActionTypes.LOAD_POKEMONS;

  constructor() {}
}

export class LoadPokemonSuccess implements Action {
  readonly type = PokemonActionTypes.LOAD_POKEMONS_SUCCESS;

  constructor(public payload: Array<Pokemon>) {}
}
export class LoadPokemonFailed implements Action {
  readonly type = PokemonActionTypes.LOAD_POKEMONS_FAILED;

  constructor(public message: string) {}
}

export class Add implements Action {
  readonly type = PokemonActionTypes.ADD;

  constructor(public pokemon: Pokemon) {}
}

export class AddSuccess implements Action {
  readonly type = PokemonActionTypes.ADD_SUCCESS;

  constructor(public pokemon: Pokemon) {}
}
export class AddFailed implements Action {
  readonly type = PokemonActionTypes.ADD_FAILED;

  constructor(public message: string) {}
}

export class Delete implements Action {
  readonly type = PokemonActionTypes.DELETE;

  constructor(public id: number) {}
}
export class DeleteSuccess implements Action {
  readonly type = PokemonActionTypes.DELETE_SUCCESS;

  constructor(public id: number) {}
}
export class DeleteFailed implements Action {
  readonly type = PokemonActionTypes.DELETE_FAILED;

  constructor(public message: string) {}
}

export class Update implements Action {
  readonly type = PokemonActionTypes.UPDATE;

  constructor(public pokemon: Pokemon) {}
}
export class UpdateSuccess implements Action {
  readonly type = PokemonActionTypes.UPDATE_SUCCESS;

  constructor(public pokemon: Pokemon) {}
}
export class UpdateFailed implements Action {
  readonly type = PokemonActionTypes.UPDATE_FAILED;

  constructor(public message: string) {}
}

export type PokemonActions =
  | LoadPokemonSuccess
  | Add
  | AddSuccess
  | AddFailed
  | Delete
  | DeleteSuccess
  | DeleteFailed
  | Update
  | UpdateSuccess
  | UpdateFailed;
import { createAction, props } from '@ngrx/store';

import { Pokemon } from '@models/pokemon.interface';

export enum PokemonActionTypes {
  ADD = '[Pokemon] Add',
  ADD_SUCCESS = '[Pokemon] Add success',
  ADD_FAILED = '[Pokemon] Add failed',
  LOAD_POKEMONS = '[Pokemon] Load pokemon',
  LOAD_POKEMONS_SUCCESS = '[Pokemon] Load pokemon success',
  LOAD_POKEMONS_FAILED = '[Pokemon] Load pokemon failed',
  UPDATE = '[Pokemon] Update',
  UPDATE_SUCCESS = '[Pokemon] Update success',
  UPDATE_FAILED = '[Pokemon] Update failed',
  REMOVE = '[Pokemon] Delete',
  REMOVE_SUCCESS = '[Pokemon] Delete success',
  REMOVE_FAILED = '[Pokemon] Delete failed'
}

export const actions = {
  loadPokemon: createAction(PokemonActionTypes.LOAD_POKEMONS),
  loadPokemonSuccess: createAction(
    PokemonActionTypes.LOAD_POKEMONS_SUCCESS,
    props<{ pokemons: Pokemon[] }>()
  ),
  loadPokemonFailed: createAction(
    PokemonActionTypes.LOAD_POKEMONS_FAILED,
    props<{ message: string }>()
  ),
  add: createAction(PokemonActionTypes.ADD, props<{ pokemon: Pokemon }>()),
  addSuccess: createAction(
    PokemonActionTypes.ADD_SUCCESS,
    props<{ pokemon: Pokemon }>()
  ),
  addFailed: createAction(
    PokemonActionTypes.ADD_FAILED,
    props<{ message: string }>()
  ),
  remove: createAction(PokemonActionTypes.REMOVE, props<{ id: number }>()),
  removeSuccess: createAction(
    PokemonActionTypes.REMOVE_SUCCESS,
    props<{ id: number }>()
  ),
  removeFailed: createAction(
    PokemonActionTypes.REMOVE_FAILED,
    props<{ message: string }>()
  ),
  update: createAction(
    PokemonActionTypes.UPDATE,
    props<{ pokemon: Pokemon }>()
  ),
  updateSuccess: createAction(
    PokemonActionTypes.UPDATE_SUCCESS,
    props<{ pokemon: Pokemon }>()
  ),
  updateFailed: createAction(
    PokemonActionTypes.UPDATE_FAILED,
    props<{ message: string }>()
  )
};

我已经导出了一个actionconst,它是一个字典,包含动作名称作为键,动作本身作为值。

createAction是一个工厂函数,它返回一个 ActionCreator 函数,该函数在调用时返回一个 action 对象。因此,当你需要 dispatch 一个 action 时,你必须调用 ActionCreator 函数:

this.store.dispatch(addSuccess(pokemon: Pokemon));

不再需要创建与动作类相关联的对象,现在您可以直接调用该函数。

因此,必须对创建动作的所有效果应用以下重构:


@Effect()
loadAllPokemon$: Observable<any> = this.actions$.pipe(
  ofType(PokemonActions.PokemonActionTypes.LOAD_POKEMONS),
  switchMap(() =>
    this.pokemonService.getAll().pipe(
    map(pokemons => new PokemonActions.LoadPokemonSuccess(pokemons)),
      catchError(message => of(new PokemonActions.LoadPokemonFailed(message)))
    )
  )
);


@Effect()
loadAllPokemon$: Observable<any> = this.actions$.pipe(
    ofType(PokemonActions.loadPokemon),
    switchMap(() =>
      this.pokemonService.getAll().pipe(
        map(pokemons => PokemonActions.loadPokemonSuccess({ pokemons })),
        catchError(message => of(PokemonActions.loadPokemonFailed({ message }))
          )
        )
      )
    )
  );

下一节中,我们将使用createEffects函数重构效果本身。

创建效果

NgRx 8 提供了装饰器createEffect替代方法@Effect()。使用装饰器替代方法的主要优势createEffect在于类型安全。也就是说,如果 effect 没有返回,Observable<Action>则会抛出编译错误。

下面的代码片段将展示loadAllPokemon$应用新方法前后的效果createEffect。迁移过程非常简单。


@Effect()
loadAllPokemon$: Observable<any> = this.actions$.pipe(
    ofType(PokemonActions.loadPokemon),
    switchMap(() =>
      this.pokemonService.getAll().pipe(
        map(pokemons => PokemonActions.loadPokemonSuccess({ pokemons })),
        catchError(message => of(PokemonActions.loadPokemonFailed({ message }))
          )
        )
      )
    )
  );


loadAllPokemon$ = createEffect(() =>
    this.actions$.pipe(
      ofType(PokemonActions.loadPokemon),
      switchMap(() =>
        this.pokemonService.getAll().pipe(
          map(pokemons => PokemonActions.loadPokemonSuccess({ pokemons })),
          catchError(message =>
            of(PokemonActions.loadPokemonFailed({ message }))
          )
        )
      )
    )
  );

因此,前后情况如下 pokemon.effects.ts


import * as PokemonActions from '@states/pokemon/pokemon.actions';

import { Actions, Effect, ofType } from '@ngrx/effects';
import { Observable, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';

import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Pokemon } from '@shared/interfaces/pokemon.interface';
import { PokemonService } from '@services/pokemon.service';

@Injectable()
export class PokemonEffects {
  constructor(
    private actions$: Actions,
    private pokemonService: PokemonService,
    public snackBar: MatSnackBar
  ) {}

  POKEMON_ACTIONS_SUCCESS = [
    PokemonActions.PokemonActionTypes.ADD_SUCCESS,
    PokemonActions.PokemonActionTypes.UPDATE_SUCCESS,
    PokemonActions.PokemonActionTypes.DELETE_SUCCESS,
    PokemonActions.PokemonActionTypes.LOAD_POKEMONS_SUCCESS
  ];

  POKEMON_ACTIONS_FAILED = [
    PokemonActions.PokemonActionTypes.ADD_FAILED,
    PokemonActions.PokemonActionTypes.UPDATE_FAILED,
    PokemonActions.PokemonActionTypes.DELETE_FAILED,
    PokemonActions.PokemonActionTypes.LOAD_POKEMONS_FAILED
  ];

  @Effect()
  loadAllPokemon$: Observable<any> = this.actions$.pipe(
    ofType(PokemonActions.PokemonActionTypes.LOAD_POKEMONS),
    switchMap(() =>
      this.pokemonService.getAll().pipe(
        map(response => new PokemonActions.LoadPokemonSuccess(response)),
        catchError(error => of(new PokemonActions.LoadPokemonFailed(error)))
      )
    )
  );

  @Effect()
  addPokemon$: Observable<any> = this.actions$.pipe(
    ofType(PokemonActions.PokemonActionTypes.ADD),
    switchMap((action: any) =>
      this.pokemonService.add(action.pokemon).pipe(
        map((pokemon: Pokemon) => new PokemonActions.AddSuccess(pokemon)),
        catchError(error => of(new PokemonActions.AddFailed(error)))
      )
    )
  );

  @Effect()
  deletePokemon$: Observable<any> = this.actions$.pipe(
    ofType(PokemonActions.PokemonActionTypes.DELETE),
    switchMap(({ id }) =>
      this.pokemonService.delete(id).pipe(
        map(() => new PokemonActions.DeleteSuccess(id)),
        catchError(error => of(new PokemonActions.DeleteFailed(error)))
      )
    )
  );

  @Effect()
  updatePokemon$: Observable<any> = this.actions$.pipe(
    ofType(PokemonActions.PokemonActionTypes.UPDATE),
    switchMap(({ pokemon }) =>
      this.pokemonService.update(pokemon).pipe(
        map(() => new PokemonActions.UpdateSuccess(pokemon)),
        catchError(error => of(new PokemonActions.UpdateFailed(error)))
      )
    )
  );

  @Effect({ dispatch: false })
  successNotification$ = this.actions$.pipe(
    ofType(...this.POKEMON_ACTIONS_SUCCESS),
    tap(() =>
      this.snackBar.open('SUCCESS', 'Operation success', {
        duration: 2000
      })
    )
  );
  @Effect({ dispatch: false })
  failedNotification$ = this.actions$.pipe(
    ofType(...this.POKEMON_ACTIONS_FAILED),
    tap(() =>
      this.snackBar.open('FAILED', 'Operation failed', {
        duration: 2000
      })
    )
  );
}


import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, switchMap, tap } from 'rxjs/operators';

import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { Pokemon } from '@shared/interfaces/pokemon.interface';
import { actions as PokemonActions } from '@states/pokemon/pokemon.actions';
import { PokemonService } from '@services/pokemon.service';
import { of } from 'rxjs';

@Injectable()
export class PokemonEffects {
  constructor(
    private actions$: Actions,
    private pokemonService: PokemonService,
    public snackBar: MatSnackBar
  ) {}

  POKEMON_ACTIONS_SUCCESS = [
    PokemonActions.addSuccess,
    PokemonActions.updateSuccess,
    PokemonActions.removeSuccess,
    PokemonActions.loadPokemonSuccess
  ];

  POKEMON_ACTIONS_FAILED = [
    PokemonActions.addFailed,
    PokemonActions.updateFailed,
    PokemonActions.removeFailed,
    PokemonActions.loadPokemonFailed
  ];

  loadAllPokemon$ = createEffect(() =>
    this.actions$.pipe(
      ofType(PokemonActions.loadPokemon),
      switchMap(() =>
        this.pokemonService.getAll().pipe(
          map(pokemons => PokemonActions.loadPokemonSuccess({ pokemons })),
          catchError(message =>
            of(PokemonActions.loadPokemonFailed({ message }))
          )
        )
      )
    )
  );

  addPokemon$ = createEffect(() =>
    this.actions$.pipe(
      ofType(PokemonActions.add),
      switchMap((action: any) =>
        this.pokemonService.add(action.pokemon).pipe(
          map((pokemon: Pokemon) => PokemonActions.addSuccess({ pokemon })),
          catchError(message => of(PokemonActions.addFailed({ message })))
        )
      )
    )
  );

  deletePokemon$ = createEffect(() =>
    this.actions$.pipe(
      ofType(PokemonActions.remove),
      switchMap(({ id }) =>
        this.pokemonService.delete(id).pipe(
          map(() => PokemonActions.removeSuccess({ id })),
          catchError(message => of(PokemonActions.removeFailed({ message })))
        )
      )
    )
  );

  updatePokemon$ = createEffect(() =>
    this.actions$.pipe(
      ofType(PokemonActions.update),
      switchMap(({ pokemon }) =>
        this.pokemonService.update(pokemon).pipe(
          map(() => PokemonActions.updateSuccess({ pokemon })),
          catchError(message => of(PokemonActions.updateFailed(message)))
        )
      )
    )
  );

  successNotification$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(...this.POKEMON_ACTIONS_SUCCESS),
        tap(() =>
          this.snackBar.open('SUCCESS', 'Operation success', {
            duration: 2000
          })
        )
      ),
    { dispatch: false }
  );

  failedNotification$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(...this.POKEMON_ACTIONS_FAILED),
        tap(() =>
          this.snackBar.open('FAILED', 'Operation failed', {
            duration: 2000
          })
        )
      ),
    { dispatch: false }
  );
}

请注意,之前dispatch: false传递给每个效果的参数现在是传递给方法的第二个参数createEffect。请记住,此选项{ dispatch: false }用于不分派新操作的效果,添加此选项还会消除效果必须返回 的限制Observable<Action>

Reducers

createReducer方法允许创建无需语句的 Reducer switch。新增了一个on区分 Action 类型的方法,并返回一个新的状态引用。另一个有趣的事实是,Reducer 中无需处理未处理 Action 的默认情况。

因此,前后情况如下 pokemon.reducers.ts


import { PokemonActionTypes, PokemonActions } from './pokemon.actions';
import { PokemonState, pokemonAdapter } from './pokemon.adapter';

export function pokemonInitialState(): PokemonState {
  return pokemonAdapter.getInitialState();
}

export function pokemonReducer(
  state: PokemonState = pokemonInitialState(),
  action: PokemonActions
): PokemonState {
  switch (action.type) {
    case PokemonActionTypes.LOAD_POKEMONS_SUCCESS:
      return pokemonAdapter.addAll(action.payload, state);

    case PokemonActionTypes.ADD_SUCCESS:
      return pokemonAdapter.addOne(action.pokemon, state);

    case PokemonActionTypes.DELETE_SUCCESS:
      return pokemonAdapter.removeOne(action.id, state);

    case PokemonActionTypes.UPDATE_SUCCESS:
      const { id } = action.pokemon;
      return pokemonAdapter.updateOne(
        {
          id,
          changes: action.pokemon
        },
        state
      );

    default:
      return state;
  }
}


import { Action, createReducer, on } from '@ngrx/store';
import { PokemonState, pokemonAdapter } from './pokemon.adapter';

import { actions as PokemonActions } from './pokemon.actions';

export function pokemonInitialState(): PokemonState {
  return pokemonAdapter.getInitialState();
}

const pokemonReducer = createReducer(
  pokemonInitialState(),
  on(PokemonActions.loadPokemonSuccess, (state, { pokemons }) =>
    pokemonAdapter.addAll(pokemons, state)
  ),
  on(PokemonActions.addSuccess, (state, { pokemon }) =>
    pokemonAdapter.addOne(pokemon, state)
  ),
  on(PokemonActions.removeSuccess, (state, { id }) =>
    pokemonAdapter.removeOne(id, state)
  ),
  on(PokemonActions.updateSuccess, (state, { pokemon }) =>
    pokemonAdapter.updateOne({ id: pokemon.id, changes: pokemon }, state)
  )
);

export function reducer(state: PokemonState | undefined, action: Action) {
  return pokemonReducer(state, action);
}

请注意,该createReducer方法接收一个参数列表:
第一个参数是初始状态,第二个参数是方法列表on。在该on方法中,第一个参数是相关操作。就我而言,我保留了这些操作,enum因为我喜欢这种数据结构。当然,您可以直接导出操作,而无需使用枚举。该on方法的第二个参数是一个回调函数,用于接收statepayload。之后,我们就可以使用强大的函数EntityAdapter来执行最常见的操作了。

结论

在本文中,我们利用该@ngrx/entity包的create*函数重构了我们的 Pokédex。使用 create* 函数可以减少应用程序状态管理中不必要的复杂性。此外,适配器用于执行最常见的操作(CRUD)。

因此,在这篇文章中我们讨论了以下主题:

  • 由于状态创建非常重复,因此可以使用@ngrx/entity 来实现状态创建的自动化。
  • 使用 自动创建效果、动作并简化减少功能@ngrx/entity

本系列的以下文章将涵盖一些有趣的主题,例如:

  • 将通过封装使用 Facade 模式@ngrx/data
  • 测试应用程序的状态。

这篇文章最重要的部分是所展示的概念,而不是所使用的技术或库。因此,对于那些刚开始构建大型 Angular 应用程序并需要应用架构原则的人来说,这篇文章应该作为指南。

更多,更多,更多……

这篇文章的GitHub 分支https://github.com/Caballerog/ngrx-pokedex/tree/ngrx-part3

鏂囩珷鏉ユ簮锛�https://dev.to/angular/part-3-build-your-pokedex-improve-ngrx-using-create-functions-2kj4
PREV
预加载所有 Angular Bundle
NEXT
在 TypeScript 中管理键值常量