第 3 部分:构建你的 Pokédex:使用 create* 函数改进 NgRX
介绍
创建动作
创建效果
Reducers
结论
更多,更多,更多……
这篇文章是系列文章的一部分,我将在其中描述如何使用 NGRX 从初学者到忍者构建你的 Pokédex ,如果你想阅读更多内容,你可以阅读以下文章:
- 第一部分:构建你的宝可梦图鉴:NGRX 简介
- 第 2 部分:构建你的 Pokédex:@ngrx/entity
- 第 3 部分:构建你的 Pokédex:使用 create* 函数改进 NgRX
- 第 4 部分:构建你的 Pokédex:@ngrx/data
- 第 5 部分:构建你的 Pokédex:测试 NgRX
介绍
在本文中,我们将使用Angular框架和NgRX作为状态管理库来开发一个神奇宝贝图鉴。我们将使用 NgRX 8 中新增的 create* 函数。
为了正确理解本文,建议您先了解如何在中级水平上管理 Angular,以及什么是状态管理库。在本系列中,我们将展示如何开发一个具体示例(Pokédex),以补充您对 NgRX 的学习。
首先,沿着这些柱子建造的结果如下面的 GIF 所示。
为了理解正在构建的内容,请务必阅读本系列文章的第一部分和第二@ngrx/entity
部分。在本文中,我们将使用包中的 create* 函数改进本系列之前开发的代码,这将简化创建 action、reducer 和 effect 所需的样板代码。
创建动作
在 NgRX 中,创建操作需要大量的样板代码。您经常需要创建枚举、操作类型、类和联合类型。在新版本中,您可以更轻松地创建操作。
NgRX 核心团队使用了著名的工厂函数设计模式来实现这一目标。这个工厂函数是createAction。 createAction 函数接收两个参数:
- 动作类型。是用于标识动作的著名字符串。
- 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 }>()
)
};
我已经导出了一个action
const,它是一个字典,包含动作名称作为键,动作本身作为值。
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
方法的第二个参数是一个回调函数,用于接收state
和payload
。之后,我们就可以使用强大的函数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