Инъекция зависимостей в создателе действия сокращения

В настоящее время я создаю приложение React/Redux для учащегося, и я не могу оборачивать голову тем, как делать инъекции зависимостей для служб.

Более конкретно: у меня есть BluetoothService (который абстрагирует стороннюю библиотеку) для сканирования и подключения к другим устройствам через bluetooth. Эта услуга используется создателями действия, что-то вроде этого:

deviceActionCreators.js

const bluetoothService = require('./blueToothService')
function addDevice(device) {
   return { type: 'ADD_DEVICE', device }
}

function startDeviceScan() {
   return function (dispatch) {
      // The Service invokes the given callback for each found device
      bluetoothService.startDeviceSearch((device) => {
          dispatch(addDevice(device));
      });
   }
}
module.exports = { addDevice, startDeviceScan };

(я использую thunk-middleware)

Моя проблема, однако, заключается в следующем: как внедрить саму службу в action-creator?

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

Я уже пробовал сделать bluetoothService параметром для самого метода (startDeviceScan(bluetoothService){}) - эффективно сделать этот метод чистым - но это просто перемещает проблему в контейнеры с помощью действия. Каждый контейнер должен был бы узнать о службе и затем реализовать ее инъекцию (например, через реквизит). Плюс, когда я хочу использовать действие из другого действия, я снова получаю ту же проблему.

Цель: Я хочу принять решение о времени загрузки, которое можно использовать в моем приложении. Есть ли хороший способ или передовая практика для этого?

Ответы

Ответ 1

Вы можете использовать промежуточное программное обеспечение redux, которое будет реагировать на асинхронное действие. Таким образом, вы можете внедрить любую услугу или макет, который вам нужен, в одном месте, и приложение не будет иметь никаких деталей реализации api:

// bluetoothAPI Middleware
import bluetoothService from 'bluetoothService';

export const DEVICE_SCAN = Symbol('DEVICE_SCAN'); // the symbol marks an action as belonging to this api

// actions creation helper for the middleware
const createAction = (type, payload) => ({ 
    type,
    payload
});

// This is the export that will be used in the applyMiddleware method
export default store => next => action => {
    const blueToothAPI = action[DEVICE_SCAN];

    if(blueToothAPI === undefined) {
        return next(action);
    }

    const [ scanDeviceRequest, scanDeviceSuccess, scanDeviceFailure ] = blueToothAPI.actionTypes;

    next(createAction(scanDeviceRequest)); // optional - use for waiting indication, such as spinner

    return new Promise((resolve, reject) => // instead of promise you can do next(createAction(scanDeviceSuccess, device) in the success callback of the original method
        bluetoothService.startDeviceSearch((device) => resolve(device), (error) = reject(error)) // I assume that you have a fail callback as well
        .then((device) => next(createAction(scanDeviceSuccess, device))) // on success action dispatch
        .catch((error) => next(createAction(scanDeviceFailure, error ))); // on error action dispatch
};

// Async Action Creator
export const startDeviceScan = (actionTypes) => ({
    [DEVICE_SCAN]: {
        actionTypes
    }
});

// ACTION_TYPES
export const SCAN_DEVICE_REQUEST = 'SCAN_DEVICE_REQUEST'; 
export const SCAN_DEVICE_SUCCESS = 'SCAN_DEVICE_SUCCESS'; 
export const SCAN_DEVICE_FAILURE = 'SCAN_DEVICE_FAILURE';

// Action Creators - the actions will be created by the middleware, so no need for regular action creators

// Applying the bluetoothAPI middleware to the store
import { createStore, combineReducers, applyMiddleware } from 'redux'
import bluetoothAPI from './bluetoothAPI';

const store = createStore(
  reducers,
  applyMiddleware(bluetoothAPI);
);

// Usage
import { SCAN_DEVICE_REQUEST, SCAN_DEVICE_SUCCESS, SCAN_DEVICE_FAILURE } from 'ACTION_TYPES';

dispatch(startDeviceScan([SCAN_DEVICE_REQUEST, SCAN_DEVICE_SUCCESS, SCAN_DEVICE_FAILURE]));

Вы отправляете асинхронное действие startDeviceScan с типом действий, которые будут использоваться при создании соответствующих действий. Средство связывания идентифицирует действие с помощью символа DEVICE_SCAN. Если действие не содержит символ, оно отправляет его обратно в хранилище (следующее промежуточное ПО/редукторы).

Если существует символ DEVICE_SCAN, промежуточное ПО извлекает типы действий, создает и отправляет действие запуска (например, для счетчика загрузки), выполняет запрос async, а затем создает и отправляет успешное или неудачное действие.

Также посмотрите на средний пример редукции реального мира.

Ответ 2

React-thunk поддерживает передачу произвольного объекта в thunk с помощью withExtraArgument. Вы можете использовать это для зависимости - вводить объект службы, например:

const bluetoothService = require('./blueToothService');

const services = {
    bluetoothService: bluetoothService
};

let store = createStore(reducers, {},
    applyMiddleware(thunk.withExtraArgument(services))
);

Затем сервисы доступны вашему thunk в качестве третьего аргумента:

function startDeviceScan() {
    return function (dispatch, getstate, services) {
        // ...
        services.bluetoothService.startDeviceSearch((device) => {
            dispatch(addDevice(device));
        });
    }
}

Это не так формально, как использование декоратора встраивания зависимостей в Angular2 или создание отдельного промежуточного слоя Redux для передачи сервисов thunks --- это просто "что-нибудь", что является своего рода уродливым, но с другой стороны, это довольно просто реализовать.

Ответ 3

Можете ли вы превратить создателей своих действий в свою собственную службу?

export function actionCreatorsService(bluetoothService) {
   function addDevice(device) {
      return { type: 'ADD_DEVICE', device }
   }

   function startDeviceScan() {
      return function (dispatch) {
         // The Service invokes the given callback for each found device
         bluetoothService.startDeviceSearch((device) => {
            dispatch(addDevice(device));
         });
      }
   }

   return {
      addDevice,
      startDeviceScan
   };
}

Теперь всем клиентам этой службы необходимо будет предоставить экземпляр bluetoothService. В вашем действительном коде src:

const bluetoothService = require('./actual/bluetooth/service');
const actionCreators = require('./actionCreators')(bluetoothService);

И в ваших тестах:

const mockBluetoothService = require('./mock/bluetooth/service');
const actionCreators = require('./actionCreators')(mockBluetoothService);

Если вы не хотите указывать услугу bluetooth каждый раз, когда вам нужно импортировать создателей действия, в модуле создателей действия вы можете иметь обычный экспорт (который использует фактический сервис bluetooth) и макет экспорта (который использует макет службы). Затем вызывающий код может выглядеть так:

const actionCreators = require('./actionCreators').actionCreators;

И ваш тестовый код может выглядеть так:

const actionCreators = require('./actionCreators').mockActionCreators;

Ответ 4

Я создал промежуточное ПО для зависимостей, называемое redux-bubble-di именно для этой цели. Его можно использовать для ввода произвольного количества зависимостей в создателей действий.

Вы можете установить его на npm install --save redux-bubble-di или загрузить его.

Ваш пример с использованием redux-bubble-di будет выглядеть так:

//import { DiContainer } from "bubble-di";
const { DiContainer } = require("bubble-di");
//import { createStore, applyMiddleware } from "redux";
const { createStore, applyMiddleware } = require("redux");
//import reduxBubbleDi from "redux-bubble-di";
const reduxBubbleDi = require("redux-bubble-di").default;

const bluetoothService = require('./blueToothService');

DiContainer.setContainer(new DiContainer());
DiContainer.getContainer().registerInstance("bluetoothService", bluetoothService);

const store = createStore(
    state => state,
    undefined,
    applyMiddleware(reduxBubbleDi(DiContainer.getContainer())),
);

const startDeviceScan = {
    bubble: (dispatch, bluetoothService) => {
        bluetoothService.startDeviceSearch((device) => {
            dispatch(addDevice(device));
        });
    },
    dependencies: ["bluetoothService"],
};

// ...

store.dispatch(startDeviceScan);