File upload 文件上传的Redux thunk设计,包括取消和进度

File upload 文件上传的Redux thunk设计,包括取消和进度,file-upload,redux,functional-programming,axios,redux-thunk,File Upload,Redux,Functional Programming,Axios,Redux Thunk,想要在react redux中上传一些文件,我有以下想法: 设置redux thunkuploadFile操作,以文件描述符作为参数启动上载 定义我自己的可在存储中序列化的“文件描述符”(具有uuid、pending、sent、error和pending属性 设置一些其他FSA,如addFile,removeFile,setfileerred,setFileSent,setFileSent 像这样 减速器 行动 注意:上传文件是我的助手,包装了axios承诺。 第一个参数是文件描述符,第二个

想要在react redux中上传一些文件,我有以下想法:

  • 设置redux thunk
    uploadFile
    操作,以
    文件
    描述符作为参数启动上载
  • 定义我自己的可在存储中序列化的“文件描述符”(具有
    uuid
    pending
    sent
    error
    pending
    属性
  • 设置一些其他FSA,如
    addFile
    removeFile
    setfileerred
    setFileSent
    setFileSent
像这样

减速器

行动

注意
上传文件
是我的助手,包装了axios承诺。 第一个参数是
文件
描述符,第二个参数是axios选项对象

我认为这应该管用

但现在我正为一些设计问题而挣扎:

  • 这是正确的方法吗?我的意思是:
    • 这充满了杂质,但ajax查询本质上是不纯净的
    • 我完全失去了
      文件
      描述符引用,因此不允许以后访问它(例如用于
      预览
      )。我将它存储在哪里?我发现将它存储在存储中非常糟糕,主要是因为我们不能用ES6的东西纯粹更新
      文件
      描述符,因此我们需要对它进行变异
  • Axios提供了一个简洁的
    CancelToken
    功能,我可以将其传递给我的选项。我以前在React中使用过它,但切换到redux时,同样的问题出现了:如果我在
    uploadFile()
    中定义了
    CancelToken
    ,我应该将其存储在哪里,这样我就可以在另一个目录中访问它,比如说,
    cancelFileUpload(fileId)
    thunk动作

在reducer方面,我认为您的操作是好的(+1在spread/rest上)。但是通过使用一些“update in”库(如)可以使代码结构看起来更好。它可以帮助您消除一些
filter()
调用

在动作设计上,我猜您正在构建一个上传队列,因此您需要
队列
/
发送挂起
/
发送进度
/
发送被拒绝
/
发送已完成
(为了清晰起见,我用命名方法对它们进行了重新表述)。在这里您无法逃脱任何东西

在action factory上,因为Promise没有进度事件,所以它现在看起来有点笨重。您可以尝试使用
redux saga
实现。代码看起来稍微干净一点。但是处理多个事件变得很容易。但是那里有一个学习曲线。代码应该类似于下面的内容。关注while循环现在,还要查看取消处理(通过
cancel
操作)

要上载文件,请分派
队列
操作。要取消上载文件,只需分派
取消
操作

import { call, put, race, takeEvery } from 'redux-saga/effects';
import EventAsPromise from 'event-as-promise';

yield takeEvery('QUEUE', function* (action) {
  yield put({ type: 'SEND_PENDING' });

  const { file } = action.payload;
  const progress = new EventAsPromise();
  const donePromise = uploadFile(file, progress.eventListener);

  try {
    while (true) {
      // Wait until one of the Promise is resolved/rejected
      const [progress] = yield race([
        // New progress event came in
        call(progress.upcoming),

        // File upload completed promise resolved or rejected
        call(() => donePromise),

        // Someone dispatched 'CANCEL'
        take(action => action.type === 'CANCEL' && action.payload.file === file)
      ]);

      if (progress) {
        // Progress event come first, so process it here
        yield put({ 
          type: 'SEND_PROGRESS', 
          payload: { 
            loaded: progress.loaded,
            total: progress.total
          }
        });
      } else {
        // Either done or cancelled (use your cancel token here)
        // Breaking a while-true loop isn't really looks good, but there aren't any better options here
        break;
      }
    }

    // Here, we assume CANCEL is a kind of fulfillment and do not require retry logic
    yield put({ type: 'SEND_FULFILLED' });
  } catch (err) {
    yield put({ type: 'SEND_REJECTED', error: true, payload: err });
  }
});
由于
redux saga
只接受承诺或行动,为了将事件流转换为承诺,我在这里介绍

使用
redux saga
,流程控制(进度/完成/错误/取消)变得非常清晰,不太容易出现错误。在您的情况下,我使用
redux saga
的方式就像文件上传的生命周期管理器。安装和拆卸总是成对进行的。如果拆卸调用(完成/错误/取消),您无需担心没有正确地开火

您需要了解有关文件描述符的情况。尽量不要将其放在存储中,因为这会阻止存储被持久化,您可能希望跨页面导航或应用程序重新启动持久化存储。根据您的场景,如果用于重试上载,您可以将其临时存储在传奇中的闭包中


在本例中,您可以轻松地将其扩展到一个上传队列,一个一个地上传文件。如果您精通
redux saga
,这不是一项困难的任务,应该少于10行代码更改。如果没有
redux saga
,实现这一点将是一项相当大的任务。

疯狂的回答,但正如您所提到的,确实存在一个学习曲线:P我需要时间来检查这个伟大的东西。同时,你会回答:代码生成器是绑定的,还是这个承诺是可翻译的?我对生成器非常在行,而且更像是一个承诺者,也许这可以让学习曲线变得更清晰一点:X你能解释一下关于“生成器绑定”的更多信息吗“承诺可翻译”?Redux传奇使用生成器来维护“工作流”。通过使用
redux saga/effects/call
函数,从生成器到Promise有一个转义图案。但是没有事件转义图案,因此我们需要将事件转换为Promise。我的意思是您提供的代码完全基于生成器。我对redux saga进行了一些挖掘,并试图理解这些生成器的内容。我很惊讶ng如果必须使用生成器,或者只有承诺才能实现这一点,那么就必须使用生成器。当我开始讲述redux saga时,我也考虑过将一切都变成承诺,我认为这是遗留问题。但事实上,redux saga使用生成器使用闭包来“保存工作流”。因此redux saga始终可以“继续它离开的地方”。非常聪明的技巧。但缺点是:你不能持久化工作流或恢复工作流。好的,谢谢。我接受这一点,即使redux saga不是我过去的方式。语法对我来说太晦涩了,导致可维护性降低。我现在使用redux logic,并考虑使用重新匹配或更简单的方法来解决副作用,逻辑a一切
// skipping static actions

export const uploadFile = (actualFile) => {
  const file = {
    id: uuidv4(),
    sending: false,
    sent: false,
    errored: false
  }

  return (dispatch) => {
    dispatch(addFile({
      ...file,
      sending: true
    }))

    return uploadFile(actualFile, {
      onUploadProgress: (evt) => {
        if(evt.loaded && evt.total) {
          const progress = (evt.loaded / evt.total) * 100

          dispatch(setFileProgress(file.id, progress))
        }
      }
    })
    .then((fileUrl) => {
      dispatch(setFileSent(file.id))
      dispatch(setFileUrl(file.id, url))
    })
    .catch((err) => {
      console.log(err)
      dispatch(setFileErrored(file.id))
    })
  }
}
import { call, put, race, takeEvery } from 'redux-saga/effects';
import EventAsPromise from 'event-as-promise';

yield takeEvery('QUEUE', function* (action) {
  yield put({ type: 'SEND_PENDING' });

  const { file } = action.payload;
  const progress = new EventAsPromise();
  const donePromise = uploadFile(file, progress.eventListener);

  try {
    while (true) {
      // Wait until one of the Promise is resolved/rejected
      const [progress] = yield race([
        // New progress event came in
        call(progress.upcoming),

        // File upload completed promise resolved or rejected
        call(() => donePromise),

        // Someone dispatched 'CANCEL'
        take(action => action.type === 'CANCEL' && action.payload.file === file)
      ]);

      if (progress) {
        // Progress event come first, so process it here
        yield put({ 
          type: 'SEND_PROGRESS', 
          payload: { 
            loaded: progress.loaded,
            total: progress.total
          }
        });
      } else {
        // Either done or cancelled (use your cancel token here)
        // Breaking a while-true loop isn't really looks good, but there aren't any better options here
        break;
      }
    }

    // Here, we assume CANCEL is a kind of fulfillment and do not require retry logic
    yield put({ type: 'SEND_FULFILLED' });
  } catch (err) {
    yield put({ type: 'SEND_REJECTED', error: true, payload: err });
  }
});