如何使用 react/redux 对请求进行排队?

How to queue requests using react/redux?

我不得不处理一个非常奇怪的案例。

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

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

建议修复: 在我看来,在这种情况下,最简单的解决方法是在有任何请求进行时禁用每个 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>
    </>
  );
};

有什么想法吗?

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

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

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

是否有端点可以发送多个更新?如果是这样,我们可以去抖动我们的网络调用以仅在用户 activity.

出现延迟时提交

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

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

例如如果用户单击 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();
    }
  };
};

希望这对您有所帮助。

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

如果您想使用 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

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

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