【问题标题】:Idiomatic way to await background tasks with redux + redux thunk使用 redux + redux thunk 等待后台任务的惯用方式
【发布时间】:2017-02-07 19:39:52
【问题描述】:

在我的应用程序中,我们有一个运行时间较长的“供应”任务,它异步完成,我们基本上必须使用轮询来检测它是否已完成。为了在此过程中不会阻止用户使用应用程序,我们在用户交互流程开始时启动了该应用程序。但是,在交互流结束时,用户会采取需要等待该供应任务(如果尚未完成)的操作。这是一个序列图来说明。

[provisioning start][poll for completion....]
[user interactions]      [blocked action....][post provisioning task]

我遇到的问题是找出一种在 Redux 中正确执行此操作的惯用方法。以下是我考虑过的一些事情:

  1. 有一个“requests”reducer,我会在其中存储Promise 用于那个长时间运行的配置任务,然后当[blocked action] 执行时,它只是等待承诺。这样做的问题是 Redux 明确要求所有 store 状态都是serializable,所以那里似乎不欢迎像 Promises 这样的东西。
  2. 订阅[blocked action] 操作创建器中的存储并查找表明配置已完成的数据。不幸的是,我非常喜欢的 redux-thunk 不包含对商店的引用,只是它的 dispatchgetState 方法。
  3. 让我的 React 组件等待配置完成,然后再执行 [blocked action]。我不喜欢这个,因为它给我的视图层增加了太多的动作顺序逻辑意识。

由于这些似乎都不是很好的选择,我目前正在选择:

  1. 将配置Promise 存储在模块属性中,基本上执行选项#1 而不从存储中获取Promise。我对此并不感兴趣,因为它会将状态移到 redux 循环之外,但这似乎是最简单的选择。

有没有更惯用的方法来使用redux-thunk 实现这一目标?如果有另一个异步中间件可以让这个更干净,我愿意考虑,尽管我已经对redux-thunk 投入了大量资金。

【问题讨论】:

  • 你现在的状态是什么样的?
  • 简化,但有点像{ infoUsedForSetup, postSetupInfo, laterActionIsPending },在执行特定操作之前我需要postSetupInfo。但我想对客户隐瞒这一点;我希望他们执行该操作,将laterActionIsPending 设置为在 UI 中显示“等待”屏幕的信号,然后在完成时将其清除,无论我们是否必须等待 postSetupInfo

标签: javascript reactjs redux react-redux redux-thunk


【解决方案1】:

有几个 redux-promise-middleware 选项可让您分派 Promise 操作并让中间件使用它们并在 Promise 状态更改时发出 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 以及许多其他中间件。在一个应用程序中,我使用:

  • 谢谢
  • promise 中间件
  • 控制台日志记录中间件
  • 用于批量更新的自定义中间件
  • 自定义中间件,用于将操作持久化到 IndexedDB
  • 用于暂停操作流的自定义中间件
  • 防止递归存储通知的中间件

除了 thunk 之外没有任何中间件,这里是另一种选择:

选项 5 - 将选项 2 与选项 4 结合起来:将承诺存储在全局某处。在调度原始操作之前,让被阻止的操作创建者 thunk 等待承诺。为了最大限度地减少“商店外的状态”,您还可以让 promise 操作向商店发出操作以保持商店更新(可能使用承诺中间件)

【讨论】:

  • 您的选项 5 几乎就是我所说的选项 4 的意思,目前看来工作正常。这个redux-tap 看起来不允许动作创建者订阅动作流,尽管对于应用级动作订阅来说它看起来确实很棒。谢谢你的回答。
  • 我添加了一个例子来说明我对tap的意思。
  • 我明白了,谢谢。到目前为止,我似乎需要有一个我订阅的全局商店引用或一个等待的全局 Promise。 Promise 似乎是这里更简单的答案。但我确实看到您可以 inject a custom argument 获取 thunk,所以也许来自 redux-tap 的动作流可以作为 redux-thunk 参数注入。
【解决方案2】:

我认为这会更简单——除非我遗漏了什么——如果我们更严格地将 UI 视为状态的函数。在您的交互流程中的每一点,都有一些事情是真或假:

  1. 配置已开始/未开始
  2. 配置已完成/未完成
  3. 用户操作正在等待/未等待

这三个事实足以告诉我们用户在给定时刻应该在他们的屏幕上看到什么,所以我们的状态应该包括这三个事实:

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 });
    });
  };
}

...在我们的 reducer 中:

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);

【讨论】:

  • 感谢您的回答,但我的问题不是要向用户显示什么,而是如何让操作等到配置操作完成后再运行。也许不清楚的部分是我不希望 user 通过提供可用的操作来阻止,而是让操作透明地等待配置,这样他们就没有例如,在他们点击按钮之前等待。
  • 我明白这一点,但是为什么单击按钮的操作是立即调度而不是在配置完成后调度呢?如果 UI 是状态的函数,那没关系。
【解决方案3】:

不完全是 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 */
     ....
   },
  }
}

【讨论】:

    猜你喜欢
    • 2017-04-03
    • 2020-07-12
    • 2023-03-23
    • 2019-03-02
    • 2019-08-07
    • 2017-06-15
    • 2018-03-19
    • 2016-06-12
    • 1970-01-01
    相关资源
    最近更新 更多