제3부분.Pokédex 구축:create* 함수를 사용하여 NgRX 개선

이 글은 제가 NGRX를 어떻게 사용해서 초보자부터 닌자까지 Pokédex를 구축하는지 설명하는 일련의 글의 일부입니다. 더 많이 읽고 싶으면 다음 글을 읽으세요.
  • Part 1. Build your Pokédex: Introduction to NGRX
  • Part 2. Build your Pokédex: @ngrx/entity
  • Part 3. Build your Pokédex: Improve NgRX using create* functions
  • 제4부분.당신의 포켓몬 인덱스 만들기: @ngrx/data
  • 제5부분.포켓몬 만들기: NgRX 테스트

  • 소개


    본고에서 우리는 Angular 프레임워크와 NgRX 주 관리 라이브러리로 Pokédex를 개발할 것이다.NgRX 8에서 새로 만든 * 기능을 사용할 예정입니다.
    이 글을 정확하게 이해하기 위해서는 중간 단계에서 어떻게 Angular를 관리하는지, 그리고 상태 관리 라이브러리가 무엇인지 아는 것이 가장 좋다.이 시리즈에서 우리는 당신의 NgRX 학습을 보충할 수 있는 특정한 예시 (포켓몬) 를 개발하는 방법을 보여 드리겠습니다.
    먼저 아래의 GIF는 이러한 기둥을 따라 구축된 결과를 보여 줍니다.

    본 시리즈의 firstsecond 부분을 읽고 구축 중인 내용을 이해하는 것이 중요하다.본고에서 우리는 @ngrx/entity 패키지의create* 함수를 사용하여 이전에 이 시리즈에서 개발한 코드를 개선할 것이다. 이것은 동작, 감축기와 효과를 만드는 데 필요한 샘플 코드를 간소화할 것이다.

    작업 작성


    NgRX에서 작업을 작성하려면 많은 템플릿 코드가 필요합니다.매거진, 동작 유형, 클래스, 연합 유형을 만들어야 합니다.이 새 버전에서는 조작을 더욱 쉽게 만들 수 있습니다.
    NgRX 핵심 팀은 유명한 공장 기능 설계 모델을 사용하여 이 목표를 실현했다.공장 함수는createAction입니다.createAction 함수는 두 개의 매개 변수를 수신합니다.

  • 동작 유형.동작을 식별하는 데 사용되는 유명한 문자열입니다.

  • 도구.운영 메타데이터입니다.예를 들어, 유효 하중.
  • 두 가지를 비교하기 위해 다음 코드는 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 }>()),
    
    before 코드에서 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라는 함수를 되돌려주는 공장 함수입니다. 이 함수는 호출할 때 동작 대상을 되돌려줍니다.따라서 작업을 할당하려면 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 함수를 사용하여 효과 자체를 재구성합니다.

    createEffects


    NgRx8은 createEffect 방법을 제공했는데 이 방법은 @Effect() 장식기의 대체 방법이다.데코레이터가 아닌 사용createEffect의 주요 장점은 유형이 안전하다는 것이다.효과가 돌아오지 않으면 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> 을 되돌려야 하는 효과 제한을 없앨 수 있습니다.

    이경관


    새로운 createReducer 방법은 switch 문장을 사용하지 않는 상황에서 감속기를 만들 수 있습니다.동작 유형을 구분하고 새로운 상태 참조를 되돌려주는 새로운 on 방법이 있습니다.또 다른 흥미로운 사실은 감속기에서 처리되지 않은 조작을 위해 기본 상황을 처리할 필요가 없다는 것이다.
    따라서 전후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 효과와 동작을 자동으로 생성하고reduce 기능을 간소화합니다.
  • 이 시리즈의 다음 게시물에는 다음과 같은 흥미로운 주제가 포함됩니다.
  • 입면 패턴은 @ngrx/data 패키지를 통해 사용됩니다.
  • 응용 프로그램의 상태를 테스트합니다.
  • 이 글의 가장 중요한 부분은 기술이나 라이브러리가 아니라 전시의 개념이다.따라서 큰 각도에서 응용 프로그램을 시작하고 응용 구조 원칙을 필요로 하는 사람들에게 이 글은 지침이 되어야 한다.

    갈수록 많아져...

  • Announing NgRx 8
  • Angular Architecture Best Practices
  • Angular Architecture - ng-conf
  • Angular Architecture (official docs)
  • NGRX
  • RxJS
  • Facade Pattern
  • 이 직위의 GitHub 지점은 https://github.com/Caballerog/ngrx-pokedex/tree/ngrx-part3

    좋은 웹페이지 즐겨찾기