NgRx 스토리지 및 효과 작동 방법: 20 LoC 재구축

카탈로그
Action, State & Reducer
Where Does NgRx Store Data?
How NgRx Effects Works
Learning NgRx
NgRx 배후의 개념은 Flux 체계 구조와 가장 유명한 실현Redux 라이브러리의 계발을 받았다.이론적으로 말하자면, 이러한 개념들은 결코 그리 복잡하지 않지만, 실천에서 당신은 모든 것이 어떻게 결합되었는지 이해하기 어려울 것이다.그렇다면 엔진 뚜껑 아래에서 NgRx가 어떻게 작동하는지 신비로운 베일을 벗고 사용자 정의 실현을 제시해 보자. 우리가 진정으로 진실에 접근할 수 있는 줄이 이렇게 적다는 것을 놀라게 될 것이다.또한 우리는 간단한 todo 응용 프로그램을 실현하기 위해 NgRx 클론을 사용할 것입니다.

📖 I'm writing a book on NgRx and you can get it for free! Learn how to structure your state, write testable reducers and work with actions and effects from one well-crafted resource.


NgRx 상태 관리의 기본 요소는 다음과 같습니다.
단일 실제 소스: 애플리케이션 상태가 하나의 객체에 저장됨
상태는 읽기 전용입니다. 현재 상태는 변경할 수 없습니다. 작업을 스케줄링하고 새 상태만 생성할 수 있습니다.
변경은 순수 함수를 사용하여 이루어졌습니다. 다음 상태는 현재 상태와 스케줄링된 조작에 따라 생성됩니다. 부작용은 허용되지 않습니다.
이러한 원칙이 결합되어 상태 전환이 명확하고 확실하다는 것을 확보한다. 이것은 응용 프로그램의 상태가 어떻게 시간에 따라 변화하는지 쉽게 알 수 있다는 것을 의미한다.
principles

동작, 상태 및 감속기


우리의 사용자 정의 NgRx 저장 실현은 한 파일store.ts에 의해 표시될 것이며, 이 파일은 방금 언급한 원칙을 반영한다.또한 이 상점을 사용하는 모든 응용 프로그램은 실제 도서관에서 알 수 있는 같은 구조 블록을 사용할 수 있습니다.

행동


작업은 응용 프로그램에서 발생하는 이벤트를 참조하는 일반 JavaScript 객체입니다.동작은 유형에 따라 구분되지만 이벤트 정보를 포함하는 유효 부하로 사용할 수 있는 속성이 여러 개 있습니다.TypeScript 를 사용하여 동작 데이터 유형을 나타내는 인터페이스를 정의할 수 있습니다.
// store.ts
export interface Action {
  type: string
  [property: string]: any
}
이제 type 속성이 있는 모든 객체는 우리 응용 프로그램에서 사용할 수 있습니다.
const addTodoAction: Action = {
  type: 'ADD',
  text: 'Demystify NgRx',
}
우리는 개발을 간소화하기 위해 사용자 정의 동작 데이터 형식과 동작 창설자를 만들 수도 있다.이것은 기본적으로 NgRx의 index typescreateAction 기능에 의해 이루어진 것이지만 완전히 같은 유형의 안전성을 제공하지 않는다.
// todos.actions.ts
export interface AddAction extends Action {
  type: 'ADD'
  text: string
}

export function addTodo(text: string): AddAction {
  return {
    type: 'ADD',
    text,
  }
}

export interface ToggleAction extends Action {
  type: 'TOGGLE'
  index: number
}

export function toggleTodo(index: number): ToggleAction {
  return {
    type: 'TOGGLE',
    index,
  }
}
우리는 props 여기에 있을 수 있지만, 지금은 일을 복잡하게 하지 마라.

더 나은 유형 검사 구현 상태


일반 JavaScript 객체는 글로벌 응용 프로그램 상태를 저장합니다.실제 응용에서 그것은 많은 형상을 가질 수 있기 때문에 우리의 NgRx 실현에서 우리는 이를 S의 범형으로 간주한다.Sreducer를 입력하고 최종적으로 저장소를 초기화합니다.동시에, 우리의 todo 응용 프로그램의 상태는 다음과 같다.따라서 todo 응용 프로그램State에 대해 사용자 정의 NgRx 구현에서 언급한 S 은 대체될 것입니다S.
// todos.state.ts
export interface Todo {
  index: number
  text: string
  done: boolean
}

export interface State {
  todos: Todo[]
}
todo 응용 프로그램의 초기 상태는 빈 그룹만 포함됩니다.
// todos.state.ts
const initialState: State = { todos: [] }

감속기


감속기는 현재 상태와 동작을 매개 변수로 하고 다음 상태로 되돌려주는 순수한 함수이다.일반 상태 형식 S 과 작업 인터페이스를 사용하여 이 설명을 Reducer 형식 서명으로 변환할 수 있습니다.
// store.ts
export type Reducer<S> = (state: S, action: Action) => S
현재, 우리는 이러한 유형의 함수를 실현하여 todo 응용 프로그램의 감속기를 정의할 수 있다.여기서 우리는 를 사용하여 전송된 동작에 따라 새로운 상태를 생성한다.초기 상태spread syntax를 사용합니다.이렇게 하면 저장소에 초기 상태를 제공하기 위해 상태가 없는 상황에서reducer를 실행할 수 있습니다.
// todos.reducer.ts
const reducer = (state = initialState, action: Action) => {
  switch (action.type) {
    case 'ADD':
      return {
        todos: [
          ...state.todos,
          {
            index: state.todos.length,
            text: action.text,
            done: false,
          },
        ],
      }
    case 'TOGGLE':
      return {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return {
              ...todo,
              done: !todo.done,
            }
          }
          return todo
        }),
      }
    default:
      return state
  }
}
일반적으로 감속기는 default parametercreateReducer 함수를 사용하여 정의됩니다.그러나 엔진 뚜껑 아래에서는 동작 유형을 전환하는 것과 다르지 않다.사실 이것은 Angular와 NgRx 8 이전에 감축기를 작성하는 정상적인 방식이었다.

에 있다 NgRx는 어디에 데이터를 저장합니까?


NgRx는 응용 상태를 각도 서비스에서 관찰할 수 있는 RxJS에 저장합니다.동시에 이 서비스는 Observable 인터페이스를 실현한다.따라서 저장소에 가입할 때 서비스는 실제로 구독을 밑바닥의 관찰 가능한 대상에 전달합니다.
내부적으로 NgRx는 실제적으로 Store를 사용했는데 이것은 특수한 관찰물로 다음과 같은 특징을 가지고 있다.
  • 신규 청약자는 청약 시 현재 가치를 받는다
  • 초기 값이 필요합니다
  • 행위 주체가 반대로 전용이기 때문에 BehaviorSubject 그 위에 새 값을 보낼 수 있다
  • 를 사용하여 현재 값을 동기화할 수 있습니다.
  • 이러한 기능은 우리의 사용자 정의 저장 실현에도 매우 유용하다. 사용자 정의 저장 실현에서 우리는 BehaviorSubject를 사용하여 응용 프로그램 상태를 저장할 것이다.그러면 우리는 상응하는 종류를 정의하여 우리의 주입 가능한 각도 서비스subject.next()를 만들 수 있다.이것은 일반적인 상태 형식 subject.getValue() 을 사용하고, 그 구조 함수는 응용 프로그램에 특정한reducer를 받아들인다.NgRx의 Subject 동작과 같이 초기 동작과 전송된 감속기를 사용하여 초기 상태를 계산합니다.
    그 밖에 우리는 하나의 동작을 받아들이는 데 사용되는 Store 함수를 제공했다.이 함수는 현재 상태를 읽어들이고 reducer를 실행하며 BehaviorSubject를 통해 결과 상태를 보냅니다.
    최종적으로 행위 주체는 S를 통해 더욱 엄격한 undefined 유형의 형식으로 노출되기 때문에 스케줄링 동작을 통해 새로운 상태 발사를 일으킬 수 있다.
    이제 NgRx는 20줄 미만의 코드에 저장되어 다시 실행됩니다.
    // store.ts
    import { Injectable } from '@angular/core'
    import { Observable, BehaviorSubject } from 'rxjs'
    
    @Injectable()
    export class Store<S> {
      state$: Observable<S>
    
      private state: BehaviorSubject<S>
    
      constructor(private reducer: Reducer<S>) {
        const initialAction = { type: '@ngrx/store/init' }
        const initialState = reducer(undefined, initialAction)
        this.state = new BehaviorSubject<S>(initialState)
        this.state$ = this.state.asObservable()
      }
    
      dispatch(action: Action) {
        const state = this.state.getValue()
        const nextState = this.reducer(state, action)
        this.state.next(nextState)
      }
    }
    

    Anything unclear? Post a comment below or ping me on Twitter


    실제 NgRx는 여러 개의 감속기를 등록할 수 있지만, 간단하게 말하자면, 우리의 실현은 하나의 감속기만 받아들일 수 있습니다.어떤 방식이든 방법은 똑같다. 우리는 RxJS 행위 주체를 통해 상태를 관리한다. 예를 들어 Cory RylanINIT과 같은 여러 차례 묘사된 모델이다.그러나, 우리는 또한 조작을 통해 상태 전환을 현저하게 진행하고, 동시에 순수한 Reducer 함수를 사용하여 모든 상태를 읽기 전용으로 유지한다.
    현재 우리의 사용자 정의 저장소를 사용하여 todo 응용 프로그램을 실현하기 위해서, 우리는 이를 공급자로 등록하고, 응용 프로그램에 특정한reducer를 전달해야 합니다.이것은 다음과 같이 here로 완성할 수 있다.실제 NgRx는 거의 같은 일을 하고 있으며, 다른 모듈에만 포장되어 있다.
    // app.module.ts
    ...
    import { Store } from './store/store'
    import { State } from './store/todos.state'
    import { reducer } from './store/todos.reducer'
    
    @NgModule({
      ...
      providers: [
        {provide: Store, useValue: new Store<State>(reducer)}
      ],
      ...
    })
    export class AppModule { }
    
    그리고 우리는 구성 요소의 실제 NgRx 저장소처럼 우리의 저장소를 사용할 수 있다.
    // app.component.ts
    ...
    import { Store, Action } from "./store/store";
    import { Todo, State } from "./store/todos.state";
    import { addTodo, toggleTodo } from "./store/todos.actions";
    
    @Component({...})
    export class AppComponent  {
    
      state$: Observable<State>
    
      constructor(private store: Store<State>) {
        this.state$ = store.state$
      }
    
      add(text: string): void {
        this.store.dispatch(addTodo(text));
      }
    
      toggle(todo: Todo): void {
        this.store.dispatch(toggleTodo(todo.index));
      }
    }
    
    <!-- app.component.html -->
    <label for="text">Todo</label>
    <input #textInput type="text" id="text" />
    <button (click)="add(textInput.value)">Add</button>
    <ul *ngIf="state$ | async as state">
      <li *ngFor="let todo of state.todos">
        <span [class.done]="todo.done">{{ todo.text }}</span>
        <button (click)="toggle(todo)">
          {{ todo.done ? 'X' : '✓'}}
        </button>
      </li>
    </ul>
    

    Join my mailing list and follow me on Twitter for more in-depth Angular & RxJS knowledge


    가치 제공자 NgRx 효과의 작업 원리



    NgRx효과 관리 비동기 부작용으로 인해 동작이 상점으로 스케줄링됩니다.복원 프로그램은 순수 함수이기 때문에 부작용이 발생하지 않기 때문에 HTTP 요청 같은 것은 허용되지 않습니다.단, Todo를 서버에 저장하는 HTTP 요청의 결과로 언제든지 스케줄링을 할 수 있습니다.다음은 적절한 동작 정의입니다.
    // todos.actions.ts
    export interface SavedAction extends Action {
      type: 'SAVED'
      todo: Todo
    }
    
    export function savedTodo(todo: Todo): SavedAction {
      return {
        type: 'SAVED',
        todo,
      }
    }
    
    이것이 바로 HTTP 요청 후 이를 조정하는 방법입니다.
    import { savedTodo } from './store/todos.actions'
    import { Todo } from './store/todos.state'
    
    this.http.post<Todo>('/todos', todo).subscribe((saved) => {
      this.store.dispatch(savedTodo(saved))
    })
    
    그러나 현재 설정에서reducer가 실제 todo를 만들기 전에 이 호출을 실행할 수 없습니다.따라서 dispatch 작업이 처리되기를 기다려야 합니다.이를 위해, 우리는 이미 스케줄링된 모든 조작을 연결하는 방법이 필요하다.우리의 상점에 대한 조정을 통해 우리는 간단하게 일반적인 RxJS observables 을 통해 또 다른 관찰할 수 있는 조작을 공개할 수 있다.
    // store.ts
    import { Injectable } from '@angular/core'
    import { Observable, BehaviorSubject, Subject } from 'rxjs'
    
    @Injectable()
    export class Store<S> {
      state$: Observable<S>
      action$: Observable<Action> // NEW
    
      private state: BehaviorSubject<S>
    
      private action = new Subject<Action>() // NEW
    
      constructor(private reducer: Reducer<S>) {
        const initialAction = { type: '@ngrx/store/init' }
        const initialState = reducer(undefined, initialAction)
        this.state = new BehaviorSubject<S>(initialState)
        this.state$ = this.state.asObservable()
        this.action$ = this.action.asObservable() // NEW
        this.action.next(initialAction) // NEW
      }
    
      dispatch(action: Action) {
        const state = this.state.getValue()
        const nextState = this.reducer(state, action)
        this.state.next(nextState)
        this.action.next(action) // NEW
      }
    }
    
    이제 우리는 저장소에서 관찰한 Observable 를 사용하여 하나의 흐름을 구성할 수 있다. 이 흐름은 asObservable() 동작을 HTTP 요청에 비추고, HTTP 요청은 'ADD' 작업에 비추게 된다.이 흐름은 action$ 서비스에 존재할 수 있습니다.
    // todo.effects.ts
    import { Injectable } from '@angular/core'
    import { filter, mergeMap, map, withLatestFrom } from 'rxjs/operators'
    import { Store } from './store'
    import { State, Todo } from './todos.state'
    import { savedTodo, AddAction } from './todos.actions'
    
    @Injectable()
    export class TodoEffects {
      constructor(private store: Store<State>, private http: HttpClient) {
        this.store.action$
          .pipe(
            // equivalent to NgRx ofType() operator
            filter((action) => action.type === 'ADD'),
            // fetch the latest state
            withLatestFrom(this.store.state$),
            // wait for HTTP request
            mergeMap(([action, state]: [AddAction, State]) => {
              // (use some kind of ID in a real app or only add todo to state after 'SAVED')
              const todo = state.todos[state.todos.length - 1]
              return this.http.post<Todo>('/todos', todo)
            }),
            // map to 'SAVED' action
            map((todo) => savedTodo(todo.index))
          )
          .subscribe((action) => this.store.dispatch(action))
      }
    }
    
    이것은 거의 NgRx 효과의 전부이다.그러나 사용RxJS subject부터 createEffect까지 실제 NgRx는 이 실현 중의 두 가지 문제를 우아하게 처리했다.
  • 'ADD' 클래스는 구성 요소나 서비스에 주입되지 않았을 때 Angular에서 초기화되지 않습니다.
  • 저희가 구독을 처리합니다.이것은 중복된 것이니, 우리는 반드시 잘못을 처리해야 한다.NgRx 자동 재시도에 실패한 효과 흐름은 최대 10회입니다.
  • 최종적으로 우리는 감속기를 확장하여 동작 'SAVED' 을 처리할 수 있다.주의, 나는 TodoEffects 인터페이스에 새로운 볼 속성TodoEffects을 추가했다.일반적으로 이것은 일종의 ID와 같습니다. todo를 서버에 저장한 후에 상태에 추가하고 싶을 수도 있습니다 (참조register effects.
    // todos.reducer.ts
    case "SAVED":
      return {
        todos: state.todos.map((todo, index) => {
          if (index === action.index) {
            return {
              ...todo,
              saved: true
            };
          }
          return todo;
        })
      };
    

    낙관적 및 비관적 UI 구현 방법 학습 NgRx


    스스로 NgRxstore and effects를 실현하는 것은 재미있고 좋은 학습 체험이기도 하지만 공식 라이브러리를 사용하여 진정한 Angular 응용 프로그램을 개발해야 한다.이렇게 하면 당신은 테스트를 거친 유형의 안전한 실현을 얻을 수 있을 뿐만 아니라 더욱 많은 기능을 가지게 될 것입니다.

    If you want to learn solid NgRx foundations, you've come to the right place, because I'm writing a book on that and you can get it for free 📖


    나는 나의 모든 경험을 이 완전한 학습 자원에 쏟아부을 뿐만 아니라, 당신이 원하는 비용을 지불할 수 있도록 허락할 것이다. 나의 주요 목표는 사람들이 적당한 소프트웨어 개발 기술을 얻도록 돕는 것이다. 그러므로 그것을 좋아할 수 있는 모든 사람들과 이 책의 링크를 공유해 주십시오.
    어떤 방식이든 NgRx의 내부 작업 원리를 설명해서 이 라이브러리를 더욱 쉽게 사용할 수 있기를 바랍니다. 는 전면적인 실시를 나타냈다.

    좋은 웹페이지 즐겨찾기