Инъекция зависимостей в создателе действия сокращения
В настоящее время я создаю приложение 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);