【问题标题】:How to queue requests using react/redux?如何使用 react/redux 对请求进行排队?
【发布时间】:2021-08-24 13:27:08
【问题描述】:

我必须处理非常奇怪的案件。

我们有几个盒子,我们可以在每个盒子上调用一些动作。当我们单击框内的按钮时,我们会调用服务器上的某个端点(使用axios)。来自服务器的响应返回新的更新信息(关于所有框,而不是我们调用操作的唯一框)。

问题: 如果用户非常快地单击许多框上的提交按钮,则请求会一一调用端点。它有时会导致错误,因为它在服务器上以错误的顺序计算(盒子组的状态取决于单个盒子的状态)。我知道这可能是更多的后端问题,但我必须尝试在前端解决这个问题。

提案修正: 在我看来,在这种情况下,最简单的解决方法是禁用每个 submit 按钮(如果有任何请求正在进行)。不幸的是这个解决方案很慢,项目负责人拒绝了这个提议。

我们想要达到的目标: 在某种程度上,我们希望在不禁用每个按钮的情况下对请求进行排队。目前对我来说完美的解决方案:

  • 单击第一个按钮 - 调用端点,请求在服务器上挂起。
  • 单击第二个按钮 - 按钮显示微调器/加载信息而不调用端点。
  • 服务器让我们响应第一次点击,然后我们才真正调用第二次请求。

我认为这样的事情是巨大的反模式,但我没有设定规则。 ;)

我正在阅读例如redux-observable,但如果我不需要,我不想为redux 使用其他中间件(现在我们使用redux-thunk)。 Redux-saga 会好的,可惜我不知道这个工具。我准备了简单的codesandbox example(我在redux 操作中添加了超时以便于测试)。

我只有一个愚蠢的提案解决方案。创建数据数组需要发送正确的请求,并在 useEffect 内部检查数组长度是否等于 1。像这样:

const App = ({ boxActions, inProgress, ended }) => {
  const [queue, setQueue] = useState([]);

  const handleSubmit = async () => {  // this code do not work correctly, only show my what I was thinking about 

    if (queue.length === 1) {
      const [data] = queue;
      await boxActions.submit(data.id, data.timeout);
      setQueue(queue.filter((item) => item.id !== data.id));
  };
  useEffect(() => {
    handleSubmit();
  }, [queue])


  return (
    <>
      <div>
        {config.map((item) => (
          <Box
            key={item.id}
            id={item.id}
            timeout={item.timeout}
            handleSubmit={(id, timeout) => setQueue([...queue, {id, timeout}])}
            inProgress={inProgress.includes(item.id)}
            ended={ended.includes(item.id)}
          />
        ))}
      </div>
    </>
  );
};

有什么想法吗?

【问题讨论】:

    标签: reactjs axios redux-saga redux-thunk


    【解决方案1】:

    如果你想使用 redux-saga,你可以使用 actionChannel 效果和阻塞 call 效果来实现你的目标:

    工作叉: https://codesandbox.io/s/hoh8n

    这里是 boxSagas.js 的代码:

    import {actionChannel, call, delay, put, take} from 'redux-saga/effects';
    // import axios from 'axios';
    import {submitSuccess, submitFailure} from '../actions/boxActions';
    import {SUBMIT_REQUEST} from '../types/boxTypes';
    
    function* requestSaga(action) {
      try {
        // const result = yield axios.get(`https://jsonplaceholder.typicode.com/todos`);
        yield delay(action.payload.timeout);
        yield put(submitSuccess(action.payload.id));
      } catch (error) {
        yield put(submitFailure());
      }
    }
    
    export default function* boxSaga() {
      const requestChannel = yield actionChannel(SUBMIT_REQUEST); // buffers incoming requests
      while (true) {
        const action = yield take(requestChannel); // takes a request from queue or waits for one to be added
        yield call(requestSaga, action); // starts request saga and _waits_ until it is done
      }
    }
    

    我使用 box reducer 立即处理 SUBMIT_REQUEST 动作(并将给定 id 设置为待处理)这一事实,而 actionChannel+call 顺序处理它们,因此动作一次只触发一个 http 请求。

    更多关于行动渠道的信息: https://redux-saga.js.org/docs/advanced/Channels/#using-the-actionchannel-effect

    【讨论】:

    • 非常感谢。在您的示例中,我应该在哪里调用端点?在这个测试用例中,我使用虚拟超时来进行最简单的测试,但在我的真实示例中,我等待的不是动作超时,而是服务器的响应,但是在你的分叉中,我们根本不调用端点(正如我提到的,我不知道传奇)。您能否更新您的示例而不使用虚拟超时?
    • 您只需要删除延迟并取消注释请求(也是 axios 导入错误 O:)) - 请参阅 codesandbox.io/s/hw4zb 以了解使用端点而不是超时的工作版本
    • 好的,如果你能这么好,我有最后一个问题。如果我有另一个按钮,调用另一个端点,但想将每个端点调用放在一个队列中,我该怎么办?像这样的东西:codesandbox.io/s/…
    • 如果你真的需要复制所有的 redux 动作(例如,因为你想在 reducer 中做不同的操作),那么你可以将一个数组传递给actionChannel effect,然后运行不同的基于 sagas动作类型:codesandbox.io/s/iodsl .... 如果您只需要更改端点,也许一个简单的type 参数就足够了codesandbox.io/s/1klz7 ...您甚至可以结合这两种解决方案(例如,有两个动作不同的端点而不是类型参数,但保持其余相同)。
    【解决方案2】:

    我同意您的评估,即我们最终需要在后端进行更改。无论您如何组织,任何用户都可以乱用前端并按照他们想要的任何顺序提交请求。

    不过我明白了,您希望在前端设计愉快的路径,使其与当前的后端一起工作。

    如果不确切了解用例,很难说,但我们通常可以从用户体验的角度进行一些改进,无论我们是否在后端进行修复,这些改进都将适用。

    是否有一个端点可以发送多个更新?如果是这样,我们可以仅在用户活动出现延迟时才对网络调用进行去抖动处理。

    用户是否需要了解选择顺序及其影响?如果是这样,听起来我们需要更新前端来传达这些信息,这可能会为这种情况提供一个自然的解决方案。

    创建一个请求队列并按顺序执行它们相当简单,但它似乎充满了新的挑战。

    例如如果用户单击 5 个复选框,并且订单很重要,那么第二次更新执行失败意味着我们需要停止对框 3 到 5 的任何进一步执行,直到更新 2 可以完成。我们还需要弄清楚我们将如何处理超时、重试和退避。我们希望如何将所有这些传达给最终用户存在一些复杂性。

    但是,假设我们已经完全准备好走这条路了。在这种情况下,使用 Redux 进行状态管理并不是很重要,用于发送请求的库也不是很重要。

    正如您所建议的,我们将创建一个内存中的更新队列,并按顺序进行出队。每次用户对盒子进行更新时,我们都会推送到该队列并尝试发送更新。我们的processEvents 函数将保留关于请求是否处于运动中的状态,它将用于决定是否采取行动。

    每次用户单击一个框时,该事件都会添加到队列中,我们会尝试处理。如果处理已经在进行中或者我们没有要处理的事件,我们不会采取任何行动。每次处理轮结束时,我们都会检查要处理的进一步事件。您可能希望与 Redux 挂钩并触发新操作以指示事件成功并更新每个处理的事件的状态和 UI 等等。您使用的库之一可能也提供类似这样的功能。

    // Get a better Queue implementation if queue size may get high.
    class Queue {
      _store = [];
      enqueue = (task) => this._store.push(task);
      dequeue = () => this._store.shift();
      length = () => this._store.length;
    }
    
    export const createSerialProcessor = (asyncProcessingCallback) => {
      const updateQueue = new Queue();
    
      const addEvent = (params, callback) => {
        updateQueue.enqueue([params, callback]);
      };
    
      const processEvents = (() => {
        let isReady = true;
    
        return async () => {
          if (isReady && updateQueue.length() > 0) {
            const [params, callback] = updateQueue.dequeue();
            isReady = false;
    
            await asyncProcessingCallback(params, callback); // retries and all that include
    
            isReady = true;
            processEvents();
          }
        };
      })();
    
      return {
        process: (params, callback) => {
          addEvent(params, callback);
          processEvents();
        }
      };
    };
    

    希望这会有所帮助。

    编辑:我刚刚注意到您包含了一个代码框,这非常有用。我已经创建了您的沙盒副本,其中包含为实现您的目的而进行的更新并将其与您的 Redux 设置集成。还有一些明显的捷径仍在使用,例如 Queue 类,但它应该与您正在寻找的内容有关:https://codesandbox.io/s/dank-feather-hqtf7?file=/src/lib/createSerialProcessor.js

    【讨论】:

      【解决方案3】:

      只需存储上一个请求的承诺并等待它解决,然后再发起下一个请求。为简单起见,下面的示例使用全局变量 - 但您可以使用 smth else 来跨请求保留状态(例如,来自 thunk 中间件的 extraArgument)。

      // boxActions.ts
      
      let submitCall = Promise.resolve();
      
      export const submit = (id, timeout) => async (dispatch) => {
        dispatch(submitRequest(id));
      
        submitCall = submitCall.then(() => axios.get(`https://jsonplaceholder.typicode.com/todos`))
      
        try {
          await submitCall;
      
          setTimeout(() => {
            return dispatch(submitSuccess(id));
          }, timeout);
        } catch (error) {
          return dispatch(submitFailure());
        }
      };
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-10-13
      • 2011-06-14
      • 2021-11-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-10
      • 1970-01-01
      相关资源
      最近更新 更多