Часть 2 из серии Angular 2 Ngrx

Если вы еще не прочитали мой первый пост о настройке вашего проекта angular 2 с помощью Ngrx / store, вам следует сначала это проверить. В этом посте мы построим эту архитектуру управления состоянием, узнав, как думать об асинхронных действиях в мире ngrx. Мы начнем с нескольких простых примеров и в конечном итоге доработаем до @Effects, которые извлекают данные из базы данных Firebase. Давайте начнем!

Исходный код

Вы можете просмотреть файлы кода для полного примера проекта ngrx, который использует как ngrx / store, так и ngrx / effects здесь:
https://github.com/JimTheMan/Jims-Ngrx-Example

Почему @Effects?

В простом проекте ngrx / store без ngrx / effects действительно нет хорошего места для размещения ваших асинхронных вызовов. Предположим, пользователь нажимает кнопку или вводит текст в поле ввода, а затем нам нужно выполнить асинхронный вызов. Тупой компонент первым узнает об этом действии от пользователя, а его обработчик будет вызван при фактическом нажатии кнопки. Однако мы не хотим помещать логику для выполнения нашего асинхронного вызова прямо в немой компонент, поскольку мы хотим, чтобы он оставался немым! Единственное, что есть в обработчике немого компонента - это эмиттер @Output, отправляющий событие умному компоненту, сообщающее ему, что была нажата кнопка. Затем интеллектуальный компонент получает событие, и его функция-обработчик запускается, но мы не хотим помещать асинхронный вход прямо в него, потому что мы хотим, чтобы он был экономным и только дипатчинг действий для нашего магазина, чтобы магазин мог изменять состояние ! Хорошо ... но хранилище обрабатывает только действия в редукторе, а редуктор должен быть чистыми функциями, поэтому где мы должны логически размещать наши асинхронные вызовы, чтобы мы могли помещать их данные ответа в хранилище? Ответ, друзья, @Effects! Вы можете почти думать о своих эффектах как о специальных видах функций-редукторов, которые предназначены для того, чтобы вы могли размещать свои асинхронные вызовы таким образом, чтобы возвращенные данные затем можно было легко вставить во внутреннее состояние хранилища для приложения.

Отдельная служба для асинхронного режима

Вы можете подумать: «А что, если у вас есть интеллектуальный компонент, который просто обменивается данными с другой службой, которая вызывает асинхронные данные, а затем, когда этот вызов возвращается, служба отправляет событие в хранилище с возвращенными данными в качестве полезной нагрузки?» И в каком-то смысле вы были бы правы! В Angular 2 сервис - это просто обычный старый класс TypeScript с метаданными @Injectable, а при работе с @Effects вы создаете один «Класс эффекта» или «Сервис эффектов», который затем содержит различные @Effect функции, каждая из которых соответствует действию, отправляемому вашим магазином ngrx.

Установка Ngrx / Effects

Первое, что вам нужно сделать, это установить @ ngrx / effects через npm:

npm install @ngrx/effects --save

Добавьте эффекты RunEffects в ваш NgModule

Затем вам нужно сообщить своему приложению, что вы хотите использовать эффекты. В массиве import вашего NgModule добавьте строку, в которой вы вызываете EffectsModule.run. Передайте класс (или классы), который вы используете как «Класс эффектов» (или классы). Конечно, ваш файл NgModule, вероятно, будет содержать намного больше кода в файле thie, но я сократил его до всего ngrx:

import { StoreModule } from '@ngrx/store';
import { MainStoreReducer } from './state-management/reducers/main-store-reducer';
import { EffectsModule } from '@ngrx/effects';
import { MainEffects } from "./state-management/effects/main-store-effects";
@NgModule({
  imports: [
    StoreModule.provideStore({mainStoreReducer}),
    EffectsModule.run(MainEffects),
  ]
  ]
})
export class AppModule { }

Создать класс эффектов

Имя этого класса должно совпадать с именем, указанным в шаге NgModule выше. По сути, класс эффектов - это просто сервис Angular 2:

import {Effect, Actions, toPayload} from "@ngrx/effects";
import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
@Injectable()
export class MainEffects {
  
  constructor(private action$: Actions) { }
}

Привет, мир @Effect

Теперь, когда у вас есть вся настройка, давайте приступим к написанию некоторых эффектов!

@Effect() update$ = this.action$
    .ofType('SUPER_SIMPLE_EFFECT')
    .switchMap( () =>
      Observable.of({type: "SUPER_SIMPLE_EFFECT_HAS_FINISHED"})
    );

Ух ты! Не пугайтесь! Я знаю, что сначала это выглядит очень странно, но давайте рассмотрим это. Мы использовали метаданные TypeScript для обозначения нашей переменной update $ ($ обычно используется в качестве суффикса для переменных, значение которых является наблюдаемым) как «эффект ngrx», который будет запускаться, когда мы отправляем действия с хранилищем (то же самое было и с нами. всегда отправлять действия редуктору или редукторам). Затем мы видим «this .action $ .ofType (‘ SUPER_SIMPLE_EFFECT ’)». Помните, мы переводим отправленное событие в наблюдаемое, поэтому .ofType означает, что вы принимаете наблюдаемое, а затем возвращаете наблюдаемое, только если оно относится к этому типу. Затем мы выполняем switchMap, потому что хотим «переключиться» с исходной наблюдаемой на совершенно новую наблюдаемую. То, что вы хотите вернуть из ngrx / effect, является наблюдаемым для действия, и когда все это воспроизводится на экране, начальное действие будет отправлено из компонента (или какой-либо службы). Затем он обходит редуктор и обрабатывается эффектом. Затем этот эффект вернет наблюдаемый некоторому действию, а новое действие будет обработано в редукторе.

Пример полезной нагрузки

В следующем примере мы станем немного интереснее, работая с полезными нагрузками. Мы можем как принять полезную нагрузку из начального действия, так и вернуть полезную нагрузку. В приведенном ниже коде сразу после того, как мы получаем действие типа «SEND_PAYLOAD_TO_EFFECT», вызывающее «map (toPayload)». Итак, мы берем наблюдаемый объект с действием и полезной нагрузкой, а также кучу других вещей и возвращаем Observable только с полезной нагрузкой. Затем мы выполняем switchMap, потому что хотим переключиться на наблюдаемый ответ, но у нас все еще есть эта полезная нагрузка в качестве аргумента в нашей функции switchMap. Затем вы можете увидеть, что, следуя шаблону, очень похожему на Redux, у нас есть объект с типом и полезной нагрузкой. Полезная нагрузка может быть объектом, содержащим в основном все, что вы хотите. Затем мы возвращаем это как наблюдаемое, и все готово!

@Effect() effectWithPayloadExample$ = this.action$
    .ofType('SEND_PAYLOAD_TO_EFFECT')
    .map(toPayload)
    .switchMap(payload => {
      console.log('the payload was: ' + payload.message);
      return Observable.of({type: "PAYLOAD_EFFECT_RESPONDS", payload: {message: "The effect says hi!"}})
    });

Асинхронный эффект с таймером

В следующей игре есть таймер. Это похоже на «setTimeout», как вы, возможно, видели раньше в JavaScript. Однако мы, конечно, хотим, чтобы этот таймер возвращался и был наблюдаемым, поэтому мы будем использовать Observable.timer (). Обратите внимание, что мы получаем полезную нагрузку как количество секунд, которое нужно установить на таймере. Ключевой момент, который нужно понять, это то, что там, где у нас обычно была бы функция обратного вызова после асинхронного события, мы теперь просто switchMap отключили от нее. Когда таймер завершает работу, мы возвращаем наблюдаемое действию «TIMER_FINISHED», которое затем обрабатывается редуктором.

@Effect() timeEffect = this.action$
    .ofType('SET_TIMER')
    .map(toPayload)
    .switchMap(payload =>
      Observable.timer(payload.seconds * 1000)
        .switchMap(() =>
          Observable.of({type: "TIMER_FINISHED"})
        )
    )

Получение данных из Firebase с помощью AngularFire2

Лично мне очень нравится использовать Firebase. Это база данных NoSQL от Google, которая отличается сверхвысокой производительностью и простотой использования. Библиотека AngularFire2 здесь особенно хорошо подходит, потому что она позволяет вам запрашивать вашу базу данных таким образом, чтобы результат был наблюдаемым. Сначала убедитесь, что он у вас установлен:

npm i angularfire2 --save

Асинхронный эффект извлечения массива из Firebase

Хорошо, давайте перейдем к @Effect, которое извлекает данные из firebase! Итак, мы получаем действие, мы видим, что оно имеет тип PULL_ARRAY_FROM_FIREBASE, а затем переключаем карту, чтобы запустить новый Observable. Вот тут и пригодится наш асинхронный вызов! В данном случае мы используем базу данных Firebase Realtime, а изящная библиотека AngularFire2 дает нам очень хороший API. Ключевым моментом здесь является то, что в библиотеке AngularFire2 af.database.list возвращает Observable! Строка, которую вы передаете, позволяет вам «углубиться» в хранилище данных объекта JSON в NoSQL, чтобы извлечь какой-либо заданный узел в виде массива. Затем мы switchMap переходим к новому Observable, который мы хотим вернуть. Затем мы вернем Observable действию типа «GOT_FIREBASE_ARRAY» с полезной нагрузкой, содержащей массив, полученный нами от Firebase.

@Effect() pullArrayFromFirebase$ = this.action$
    .ofType('PULL_ARRAY_FROM_FIREBASE')
    .switchMap( () => 
        this.af.database.list('/cypherapp/rooms/')
        .switchMap(result =>
          Observable.of({type: "GOT_FIREBASE_ARRAY", payload: {pulledArray:  result}})
          )
    )

Асинхронный эффект извлечения объекта из Firebase

Итак, теперь мы знаем, как извлечь узел из Firebase в виде массива, но что, если мы хотим извлечь его как обычный объект JavaScript? Что ж, все, что нам нужно сделать, это изменить af.database.object вместо af.database.list, и остальной код останется точно таким же!

@Effect() pullObjectFromFirebase$ = this.action$
    .ofType('PULL_OBJECT_FROM_FIREBASE')
    .switchMap( () =>
      this.af.database.object('/cypherapp/rooms/')
        .switchMap(result =>
          Observable.of({type: "GOT_FIREBASE_OBJECT", payload: {pulledObject: result}})
        )
    )

SwitchMap просто берет на себя функцию

Здесь мы используем много switchMaps, поэтому важно понять, что за этим стоит. Вот скриншот со страницы switchMap docs reactive.io:

Посмотрите описаниеg здесь. Первый (и часто единственный) параметр switchMap - это функция, которая применяется к элементу, испускаемому источником Observable, и возвращает Observable. Также стоит отметить, что Толстые стрелки, которые у нас здесь есть, на самом деле просто сокращение для обозначения того, что мы создаем функцию. Поскольку один наблюдаемый объект - это единственное, что есть в теле функции, и это всего лишь одна строка, мы можем опустить фигурные скобки и ключевое слово return. Например, за исключением некоторой дополнительной регистрации, приведенный ниже код идентичен предыдущему фрагменту выше:

@Effect() pullObjectFromFirebase$ = this.action$
    .ofType('PULL_OBJECT_FROM_FIREBASE')
    .switchMap(() => {
      console.log('in the first switchMap!');
      return this.af.database.object('/cypherapp/rooms/')
        .switchMap(result => {
          console.log('oh yeah, we got the result!');
          return Observable.of({type: "GOT_FIREBASE_OBJECT", payload: {pulledObject: result}})
        })
    });

Хотя первый пример более краток, и вы, вероятно, в какой-то момент перейдете к нему, посмотрите на этот пример, приведенный непосредственно выше, и вы сможете понять, что на самом деле происходит в коде.