Работали ли вы с Angular в прошлом и хотите получить быстрый обзор того, как ngrx / store вписывается в общую картину? Это идеальное место для начала.

Я не буду тратить время на разговоры о том, почему вы можете захотеть использовать ngrx / store, откуда он появился и почему он стал таким популярным. Я просто собираюсь сразу перейти к тому, как это вписывается в приложение angular и как потоки данных, на простом примере. Идея состоит в том, чтобы вывести вас с нуля до «Я понимаю», как как можно быстрее. Я сохраню остальную часть обсуждения для другого поста. Я предполагаю, что не понимаю популярную библиотеку redux, которая очень похожа.

Давайте рассмотрим сценарий функции, которую я хочу добавить в свое приложение, и рассмотрим шаги, необходимые для ее работы.

Сценарий: добавьте кнопку в один из компонентов нашего приложения, которая увеличивает счетчик, и вторую кнопку, чтобы уменьшить его. Отобразите счетчик на другом компоненте нашего приложения.

Если вы хотите просмотреть приложение полностью, загляните в github здесь.

Для простоты в нашем приложении всего 3 компонента: AppComponent, MainDashboardComponent и CounterComponent.

Мы начнем с наиболее фундаментальной концепции redux и ngrx / store: все состояние приложения хранится в одном центральном объекте. Этот объект называется store , и может иметь любую структуру, которая нам нравится. Он неизменен, что означает, что он никогда не меняется; но новые магазины могут быть созданы на лету, чтобы заменить его.

Чтобы наш счетчик работал, нам нужно начать с получения текущего числа счетчиков в магазине и настроить его на обновление при каждом нажатии кнопки. Давайте начнем.

Шаг 1: зафиксируйте взаимодействие с пользователем

MainDashboardComponent будет там, где находятся наши кнопки, поэтому первым шагом в реализации этой функции будет вызов функции в нашем компоненте, когда пользователь нажимает первую кнопку, с другой функцией для обработки щелчка по второй кнопке.

Обе эти функции будут EventEmitters, поэтому, когда они вызываются в MainDashboardComponent, они могут быть «услышаны» нашим AppComponent. Код ниже. Обратите внимание, что пока в этом нет ничего, что отличалось бы от стандартного приложения Angular.

<!-- main-dashboard.component.html -->
<div>
<h2>Main dashboard</h2>
<button (click)="increment.emit()">increment</button>
<button (click)="decrement.emit()">decrement</button>
</div>

Примечание: при использовании redux или ngrx / store это помогает различать «умные» компоненты (иногда называемые контейнерами), которые напрямую взаимодействуют с хранилищем, и «тупые» компоненты, которые не взаимодействуют с хранилищем. MainDashboardComponent - простой компонент.

// main-dashboard.component.ts
import { Component, OnInit, EventEmitter, Output } from '@angular/core';
@Component({
  selector: 'app-main-dashboard',
  templateUrl: './main-dashboard.component.html',
  styleUrls: ['./main-dashboard.component.scss']
})
export class MainDashboardComponent implements OnInit {
  @Output() increment = new EventEmitter();
  @Output() decrement = new EventEmitter();
  constructor() { }
  ngOnInit() { }
}

Шаг 2: отправьте событие в контейнер

Теперь, когда наш немой компонент MainDashboardComponent фиксирует нажатия кнопок пользователя и генерирует события, нам нужно настроить наш контейнер AppComponent для прослушивания этих событий. Как только контейнер получит входные данные, мы будем близки к тому, чтобы разместить его в нашем магазине.

// app.component.html
<app-main-dashboard
  (increment)="incrementCounter()"
  (decrement)="decrementCounter()">
</app-main-dashboard>
<app-counter></app-counter>

Как видите, действия увеличения и уменьшения из MainDashboardComponent запускают методы incrementCounter и DecmentCounter в нашем AppComponent.

Шаг 3. Отправьте действие редуктору

Теперь мы настроим эти два метода для отправки действий. В ngrx / store и redux действия - это сигналы хранилищу о необходимости обновления. Рекомендуется хранить их в отдельном файле в приложении. Посмотрим, как это выглядит в коде.

// counter.actions.ts
import { Action } from '@ngrx/store';
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export class Increment implements Action {
  readonly type = INCREMENT;
  constructor() {}
}
export class Decrement implements Action {
  readonly type = DECREMENT;
  constructor() {}
}
export type CounterActions = | Increment | Decrement;

Как видите, каждое действие - это отдельный класс, который реализует интерфейс Action, предоставленный нам ngrx. Все, что нам нужно сделать, это задать ему тип.

В нашем контейнере, AppComponent, наши методы incrementCounter и DecmentCounter будут отправлять эти действия, по сути, посылая в наш магазин сигнал о том, что он должен обновиться определенным образом. Вот наш AppComponent:

import { AppState } from './common/models/appState.model';
import { Component } from '@angular/core';
import * as CounterActions from './common/actions/counter.actions';
import { Store } from '@ngrx/store';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(private store: Store<AppState>) { }
  decrementCounter() {
    this.store.dispatch(new CounterActions.Decrement);
  }
  incrementCounter() {
    this.store.dispatch(new CounterActions.Increment);
  }
}

Шаг 4: Действие процесса в редукторе

Итак, теперь, когда эти действия отправляются, мы можем настроить reducer для их прослушивания. Редуктор - это функция, которая определяет, что содержится в определенной части хранилища, и отвечает за обновление этого раздела при отправке действий. Давайте создадим редуктор специально для хранения текущего номера счетчика и дадим ему конкретные инструкции о том, что нужно изменить при отправке наших действий Increment и Decrement.

import * as CounterActions from './../actions/counter.actions';
export function MainDashboardReducer(state = {
    counter: 0
  },  action) {
  switch (action.type) {
    case CounterActions.INCREMENT:
      return { counter: state.counter + 1 };
    case CounterActions.DECREMENT:
      return { counter: state.counter - 1 };
    default:
      return state;
  }
}

Как видите, reducer в redux и ngrx / store - это просто большой оператор switch с регистром для каждого действия, которое может быть отправлено. Во второй строке, где я установил state = {counter: 0}, я определяю форму моего объекта магазина, указывая, что у него должен быть дочерний объект с именем mainDashboard, равный объекту (со свойством counter, установленным на 0) .

Когда мой редуктор слышит, что действие CounterActions.INCREMENT отправляется, он должен вернуть новое хранилище, где значение счетчика на единицу выше, чем в старом хранилище. Практически тот же процесс для действия декремента. Все, что возвращает редуктор, будет новым объектом store.mainDashboard.

Итак, где я могу сказать ngrx / store, что этот редуктор должен управлять разделом mainDashboard в магазине? Это происходит в app.module.ts:

@NgModule({
declarations: [
  AppComponent,
  MainDashboardComponent,
  CounterComponent
],
imports: [
  BrowserModule,
  StoreModule.forRoot({ mainDashboard: MainDashboardReducer }),
],
providers: [],
bootstrap: [AppComponent]
})

Шаг 5. Отображение состояния в пользовательском интерфейсе

Итак, мы успешно обновили объект центрального хранилища при каждом нажатии кнопки. Как мы отображаем свойство store.mainDashboard.counter в нашем CounterComponent? Просто за счет использования Observables. Ngrx / store предоставляет нам удобный метод для объекта хранилища, который позволяет нам выбрать нужную часть хранилища и подписаться на его изменения. Теперь app.component.ts выглядит так:

export class AppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>) {
store.select(state => state.mainDashboard.counter).subscribe(res =>   {
this.counter = res;
});
}
decrementCounter() {
  this.store.dispatch(new CounterActions.Decrement);
}
incrementCounter() {
  this.store.dispatch(new CounterActions.Increment);
}
}

Теперь при изменении state.mainDashboard.counter этот наблюдаемый объект будет выдавать новое значение. Все, что мне нужно сделать, это назначить это одному из моих свойств компонента, и я могу делать с ним все, что захочу, например отображать его в моем шаблоне с помощью интерполяции строк или передавать его в мой дочерний компонент CounterComponent в качестве входных данных. .

Заключение

Я знаю, что впервые погрузиться в поток данных redux или ngrx / store может быть сложно. Подводя итог, вот шаги, которые мы прошли:

Шаг 1: зафиксируйте взаимодействие с пользователем

Мы настроили наши кнопки в MainDashboardComponent, чтобы просто вызывать функции при нажатии.

Шаг 2: отправьте событие в контейнер

При нажатии кнопок генерируются события из компонента, чтобы их мог слышать контейнер (который будет отвечать за разговор с магазином). В нашем случае этим контейнером был AppComponent.

Шаг 3. Отправьте действие редуктору

Когда события слышны в AppComponent, отправьте действия, чтобы уведомить редуктор, который ему необходимо обновить.

Шаг 4: Действие процесса в редукторе

Настройте редуктор, чтобы он возвращал новое хранилище, если он слышит отправку действия увеличения или уменьшения.

Шаг 5. Отображение состояния в пользовательском интерфейсе

В своем контейнере (AppComponent) подпишитесь на изменения в конкретной части интересующего вас магазина.