[英]Idiomatic way to await background tasks with redux + redux thunk
在我的應用程序中,我們有一個異步完成的有點長時間運行的“供應”任務,我們基本上必須使用輪詢來檢測它是否已完成。 為了在發生這種情況時不會阻止用戶使用應用程序,我們在用戶交互流程的開始就開始了。 但是,在交互流程結束時,如果該配置任務尚未完成,用戶會采取需要等待該配置任務的操作。 這是一種序列圖來說明。
[provisioning start][poll for completion....]
[user interactions] [blocked action....][post provisioning task]
我遇到的問題是找出一種在 Redux 中正確執行此操作的慣用方法。 以下是我考慮做的一些事情:
Promise
,然后當[blocked action]
執行時,它只是等待承諾。 問題在於 Redux 明確要求所有存儲狀態都是可序列化的,因此似乎不歡迎 Promises 之類的東西。[blocked action]
操作創建器中訂閱存儲,並查找表示配置已完成的數據。 不幸的是,我非常喜歡的redux-thunk
不包含對 store 的引用,只包含它的dispatch
和getState
方法。[blocked action]
之前等待配置完成。 我不喜歡這樣,因為它為我的視圖層添加了太多的動作序列邏輯意識。由於這些似乎都不是很好的選擇,我目前正在使用:
Promise
存儲在模塊屬性中,並且基本上執行選項 #1 而不從存儲中獲取Promise
。 我對此並不感冒,因為它會將狀態移到 redux 循環之外,但這似乎是最簡單的選擇。 有沒有更慣用的方法來實現這一點redux-thunk
? 如果有另一個異步中間件可以使這個更清晰,我願意考慮,盡管我已經在redux-thunk
投入了大量資金。
有幾個 redux-promise-middleware 選項可讓您分派承諾操作並讓中間件使用它們並在承諾狀態更改時發出 PENDING,RESOLVED|REJECTED 操作。 如果您正在使用 Promise 和 Redux,您可能需要查看其中之一。 但是,他們不會幫助您解決這個特定問題。
我認為redux-saga是一個非常適合幫助您處理這種情況的中間件。 我還沒有親自使用它,所以我不能提供一個例子。
另一個可能更簡單的選項是redux-tap 。 使用點擊公開您的應用可以訂閱的操作流。 讓一個被阻止的動作創建者 thunk 訂閱該流並等待表示承諾已完成的動作(當然,它應該首先通過getState
檢查存儲的內容,以查看在此被阻止的動作被分派之前承諾是否已完成)。 像這樣的東西:
// ========= configureStore.js
import ee from 'event-emitter';
// ...
export const actionStream = ee();
// ...
const emitActions = tap(({type} => type, (type, action) => actionStream.emit(type, action);
// install emitActions middleware to run *after* thunk (so that it does not see thunk actions but only primitive actions)
// =========== your action module.js
import {actionStream} from './configureStore';
export function blockedAction(arg) {
return (dispatch, getState) => {
if (getState().initDone) {
return dispatch({type: "blockedAction", payload: arg});
}
// wait for init action
actionStream.once("initAction", initAction => dispatch({type: "blockedAction", payload: arg}));
};
}
請記住,它不是二進制的“thunk 或 xxx 中間件”選擇。 您可以加載 thunk 以及許多其他中間件。 在一個應用程序中,我使用:
除了 thunk 沒有任何中間件,這是另一種選擇:
選項 5 - 將選項 2 與選項 4 結合起來:將承諾存儲在全球某個地方。 在分派原始動作之前,讓被阻止的動作創建者 thunk 等待承諾。 為了最小化“商店外的狀態”,您還可以讓承諾動作向商店發出動作以保持商店更新(可能使用承諾中間件)
我認為這更簡單——除非我遺漏了一些東西——如果我們更嚴格地將 UI 視為狀態的函數。 在交互流程的每一點上,都有一些事情是對的或錯的:
這三個事實足以告訴我們用戶在特定時刻應該在他們的屏幕上看到什么,所以我們的狀態應該包括這三個事實:
const initialState = {
provisioningStarted: false,
provisioningDone: false,
laterActionIsPending: false,
// ...
};
使用 redux-thunk 我們可以處理配置操作:
function startProvisioning() {
return dispatch => {
dispatch({ type: START_PROVISIONING });
pollProvisioning().then(postSetupInfo => {
dispatch({ type: DONE_PROVISIONING, postSetupInfo });
});
};
}
...在我們的減速器中:
function appReducer(state, action) {
// ...
switch (action.type) {
case START_PROVISIONING:
return { ...state, provisioningStarted: true };
case DONE_PROVISIONING:
return { ...state, provisioningDone: true, postSetupInfo: action.postSetupInfo };
case LATER_ACTION_PENDING:
return { ...state, laterActionIsPending: true };
// ...
}
}
正如我所說,這應該足以告訴我們用戶應該看到什么:
const Provisioner = ({provisioningStarted, provisioningDone, /* ... */}) => {
if (provisioningStarted) {
if (laterActionIsPending) {
if (provisioningDone) {
return <p>Finished! Here's your info: {postSetupInfo}</p>;
}
return <p>Provisioning...</p>;
}
return <button type="button" onClick={doAction}>Do action</button>;
}
return <button type="button" onClick={startProvisioning}>Start provisioning</button>;
};
export default connect(mapStateToProps, mapDispatchToProps)(Provisioner);
不完全是 redux-thunk 但功能上等效的是使用redux-background 。 我創建的一個包使這個過程更容易。
它是這樣的:
import { startJob } from 'redux-background';
import store from './store';
store.dispatch(startJob('provision', function(job, dispatch, getState) {
const { progress, data } = job;
return new Promise((resolve, reject) => {
// Do some async stuff
// Report progress
progress(10);
// Return value
return 10;
})
}, { data: '...' } );
當它結束時,你的狀態應該是這樣的
{
background: {
provision: {
active: true,
running: false,
value: 10,
error: null,
/* and lot of more metadata */
....
},
}
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.