私は現在、学習者のReact/Reduxアプリケーションを構築しています。サービスの依存性注入を行う方法に頭を悩ますことはできません。
より具体的に言うと、スキャンしてBluetooth経由で他のデバイスに接続するBluetoothService
(サードパーティライブラリを抽象化)があります。このサービスは、アクションの作成者によって次のように利用されます。
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 };
_
(サンクミドルウェアを使用しています)
しかし、私の問題は、サービス自体をアクション作成者に注入する方法ですか?
ハードコーディングされたrequire
(またはES6のimport
in)は、テストを非常に難しくする以外に、適切なパターンではないと思うので、使用したくありません。また、ワークステーション(bluetoothがない)でアプリをテストしながらモックサービスを使用できるようにしたいので、環境によっては、アクションクリエーター内に同じインターフェイスが挿入された別のサービスが必要です。これは、静的インポートの使用では不可能です。
BluetoothServiceをメソッド自体のパラメーター(startDeviceScan(bluetoothService){}
)にしてみました-効果的にメソッド自体を純粋にしましたが、アクションを使用して問題をコンテナーに移動しています。その場合、すべてのコンテナはサービスを認識し、その実装を(たとえば、propsを介して)挿入する必要があります。さらに、別のアクション内からアクションを使用したい場合、同じ問題が再び発生します。
The Goal:アプリで使用する実装のブートストラップ時間を決定したい。これを行うための良い方法またはベストプラクティスはありますか?
非同期アクションに応答する reduxミドルウェア を使用できます。このようにして、必要なサービスやモックを1か所に注入でき、アプリには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が存在する場合、ミドルウェアはアクションタイプを抽出し、開始アクション(ロードスピナーなど)を作成してディスパッチし、非同期リクエストを作成してから、成功または失敗アクションを作成してディスパッチします。
実際のreduxの中間の例 も見てください。
React-thunkは withExtraArgument
を使用して任意のオブジェクトをthunkに渡すことをサポートしています。これを使用して、サービスオブジェクトを依存関係注入することができます。例:
const bluetoothService = require('./blueToothService');
const services = {
bluetoothService: bluetoothService
};
let store = createStore(reducers, {},
applyMiddleware(thunk.withExtraArgument(services))
);
次に、サービスはサンクが3番目の引数として使用できます。
function startDeviceScan() {
return function (dispatch, getstate, services) {
// ...
services.bluetoothService.startDeviceSearch((device) => {
dispatch(addDevice(device));
});
}
}
これは、Angular2での依存性注入デコレーターの使用や、サンクにサービスを渡すための個別のReduxミドルウェアレイヤーの作成ほど正式なものではありません。実装はかなり簡単です。
アクションクリエーターを独自のサービスにラップできますか?
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;
まさにその目的のために、依存関係を注入するミドルウェア redux-bubble-di を作成しました。アクションの作成者に任意の数の依存関係を注入するために使用できます。
npm install --save redux-bubble-di
または download でインストールできます。
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);