Работали ли вы с 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) подпишитесь на изменения в конкретной части интересующего вас магазина.