Как использовать другую службу Angular2 внутри редуктора ngrx/Store?

Новое для ngrx/Store и редуктора. В принципе, у меня есть этот редуктор:

import {StoreData, INITIAL_STORE_DATA} from "../store-data";
import {Action} from "@ngrx/store";
import {
  USER_THREADS_LOADED_ACTION, UserThreadsLoadedAction, SEND_NEW_MESSAGE_ACTION,
  SendNewMessageAction
} from "../actions";
import * as _ from "lodash";
import {Message} from "../../shared-vh/model/message";
import {ThreadsService} from "../../shared-vh/services/threads.service";

export function storeData(state: StoreData = INITIAL_STORE_DATA, action: Action): StoreData {


  switch (action.type) {

    case SEND_NEW_MESSAGE_ACTION:
      return handleSendNewMessageAction(state, action);

    default:
      return state
  }
}

function handleSendNewMessageAction(state:StoreData, action:SendNewMessageAction): StoreData {

  const newStoreData = _.cloneDeep(state);

  const currentThread = newStoreData.threads[action.payload.threadId];

  const newMessage: Message = {
    text: action.payload.text,
    threadId: action.payload.threadId,
    timestamp: new Date().getTime(),
    participantId: action.payload.participantId,
    id: [need a function from this service: ThreadsService]
  }

  currentThread.messageIds.push(newMessage.id);

  newStoreData.messages[newMessage.id] = newMessage;

  return newStoreData;
}

Проблема заключается в функции редуктора, я не знаю, как вводить инъекционную услугу, которую я создал в другом файле, и использовать функцию внутри нее. Часть id - мне нужно сгенерировать идентификатор push-кода firebase, используя такую ​​функцию, как this.threadService.generateID()...

Но так как это функция, у меня нет конструктора для использования DI, и я понятия не имею, как получить функции в threadService!

Ответ 1

В редукторах нет механизма для инъекций услуг. Редукторы должны быть чистыми функциями.

Вместо этого вы должны использовать ngrx/effects - это механизм для реализации побочных эффектов действий. Эффекты прослушивают определенные действия, выполняют некоторый побочный эффект, а затем (необязательно) выделяют дальнейшие действия.

Как правило, вы разделили свое действие на три: запрос; ответ успеха; и ответ об ошибке. Например, вы можете использовать:

SEND_NEW_MESSAGE_REQ_ACTION
SEND_NEW_MESSAGE_RES_ACTION
SEND_NEW_MESSAGE_ERR_ACTION

И ваш эффект будет выглядеть примерно так:

import { Injectable } from "@angular/core";
import { Actions, Effect, toPayload } from "@ngrx/effects";
import { Action } from "@ngrx/store";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/map";

@Injectable()
export class ThreadEffects {

  constructor(
    private actions: Actions,
    private service: ThreadsService
  ) {}

  @Effect()
  sendNewMessage(): Observable<Action> {

    return this.actions
      .ofType(SEND_NEW_MESSAGE_REQ_ACTION)
      .map(toPayload)
      .map(payload => {
        try {
          return {
              type: SEND_NEW_MESSAGE_RES_ACTION,
              payload: {
                  id: service.someFunction(),
                  // ...
              }
          };
        } catch (error) {
          return {
              type: SEND_NEW_MESSAGE_ERR_ACTION
              payload: {
                error: error.toString(),
                // ...
              }
          };
        }
      });
  }
}

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

Эффекты основаны на наблюдениях, поэтому включение синхронных, основанных на обещаниях или наблюдаемых сервисов является прямым.

В ngrx/example-app есть effects.

Относительно ваших запросов в комментариях:

.map(toPayload) предназначен только для удобства. toPayload - это функция ngrx, которая существует, поэтому ее можно передать в .map, чтобы извлечь действие payload, что все.

Вызов службы, которая является наблюдаемой, является прямой. Обычно вы делаете что-то вроде этого:

import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/of";
import "rxjs/add/operator/catch";
import "rxjs/add/operator/map";
import "rxjs/add/operator/switchMap";

@Effect()
sendNewMessage(): Observable<Action> {

  return this.actions
    .ofType(SEND_NEW_MESSAGE_REQ_ACTION)
    .map(toPayload)
    .switchMap(payload => service.someFunctionReturningObservable(payload)
      .map(result => {
        type: SEND_NEW_MESSAGE_RES_ACTION,
        payload: {
          id: result.id,
          // ...
        }
      })
      .catch(error => Observable.of({
        type: SEND_NEW_MESSAGE_ERR_ACTION
        payload: {
          error: error.toString(),
          // ...
        }
      }))
    );
}

Кроме того, эффекты могут быть объявлены как функции, возвращающие Observable<Action> или как свойства типа Observable<Action>. Если вы смотрите на другие примеры, вы, вероятно, столкнетесь с обеими формами.

Ответ 2

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

export const fooBarService= {
    mapFooToBar: (foos: Foo[]): Bar[] => {
        let bars: Bar[];
        // Implementation here ...
        return bars;
    } 
}

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

@Injectable()
export class FooBarService{
    public mapFooToBar (foos: Foo[]): Bar[] {
        let bars: Bar[];
        // Implementation here ...
        return bars;
    } 
}

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

// <!> Play nice and use only services containing pure functions
var injector = ReflectiveInjector.resolveAndCreate([FooBarService]);
var fooBarService= injector.get(FooBarService);

// Due to changes in ngrx4 we need to define our own action with payload
export interface PayloadAction extends Action {
    payload: any
}

/**
 * Foo bar reducer
 */
export function fooBarReducer(
    state: FooBarState = initialState.fooBar, 
    action: PayloadAction
) {
    switch (action.type) {

        case fooBarActions.GET_FOOS_SUCCESS:
            return Object.assign({}, state, <FooBarState>{
                foos: action.payload,
                // No effects used, all nicelly done in the reducer in one shot
                bars: fooBarService.mapFooToBar (action.payload) 

            });

        default:
            return state;
    }

}

Используя эту настройку, я могу использовать три типа сервисов FooBarDataService, FooBarMapsService и FooBarLogicService. Служба данных вызывает webapi и предоставляет результаты из хранилища состояния с результатами. Служба карты используется для сопоставления foos с барами, а служба Logic используется для добавления бизнес-логики в отдельный слой. Таким образом, у меня могут быть крошечные контроллеры, которые используются только для склеивания объектов и обслуживания их к шаблонам. Практически нет логики в контроллерах. И в качестве последнего штриха резольверы могут предоставлять данные хранилища данных на маршрутах, тем самым полностью абстрагируя государственный магазин.

Подробнее о ReflexiveInjector здесь.