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

时间:2018-03-29 09:10:42

标签: file-upload redux functional-programming axios redux-thunk

想要在react-redux中上传一些文件,我得到了以下想法:

  • 设置一个redux-thunk uploadFile操作,启动上传,将File描述符作为参数
  • 在商店中定义我自己的“文件描述符”(可uuidpendingsenterroredpending特性
  • 设置其他FSA,例如addFileremoveFilesetFileErroredsetFileSentsetFileSent

喜欢这样

减速器

  switch (action.type) {
    case FILES__ADD_FILE:
      return {
        ...state,
        files: [
          ...state.files,
          action.payload
        ]
      }
    case FILES__REMOVE_FILE:
      return {
        ...state,
        files: state.files.filter(
          file => file.id !== action.payload
        )
      }
    case FILES__SET_FILE_ERRORED:
      return {
        ...state,
        files: state.files.map(file => {
          if(file.id !== action.payload.fileId) {
            return file
          }

          return {
            ...file,
            sending: false,
            errored: true,
            sent: false
          }
        })
      }
    case FILES__SET_FILE_SENDING:
      return {
        ...state,
        files: state.files.map(file => {
          if(file.id !== action.payload) {
            return file
          }

          return {
            ...file,
            sending: true,
            errored: false,
            sent: false
          }
        })
      }
    case FILES__SET_FILE_SENT:
      return {
        ...state,
        files: state.files.map(file => {
          if(file.id !== action.payload) {
            return file
          }

          return {
            ...file,
            sending: false,
            errored: false,
            sent: true
          }
        })
      }
    case FILES__SET_FILE_PROGRESS:
      return {
        ...state,
        files: state.files.map(file => {
          if(file.id !== action.payload.fileId) {
            return file
          }

          return {
            ...file,
            progress: action.payload.progress
          }
        })
      }
    default:
      return state
  }

动作

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

注意uploadFile是我的帮手,包含了一个axios承诺。 第一个参数是File描述符,第二个参数是axios选项对象。

我认为应该有效..

但现在我正在努力解决一些设计问题:

  • 这是正确的方法吗?我的意思是:
    • 这里充满了杂质,但ajax查询本质上是Impure ..
    • 我完全丢失了File描述符引用,因此不允许以后访问它的任何机会(例如preview)。我会在哪里存放?我觉得将它存储在商店中是非常糟糕的,主要是因为我们不能用ES6的东西纯粹更新File描述符,因此我们需要改变它
  • Axios提供了一个整洁的CancelToken内容,我可以将其传递给我的选项。我之前在React中使用过它,但转换到redux,同样的问题出现在我面前:如果我在cancelToken中定义uploadFile(),我在哪里存储它,所以我可以在里面访问它进一步说,cancelFileUpload(fileId) thunk action?

1 个答案:

答案 0 :(得分:1)

在减速机方面,我认为你的行动很好(传播/休息+1)。但是通过使用像simple-update-in这样的“更新”库,代码结构看起来会更好一些。它可以帮助您消除一些filter()次呼叫。

在动作设计上,我猜您正在构建上传队列,因此您需要QUEUE / SEND_PENDING / SEND_PROGRESS / SEND_REJECTED / {{1} (为了清楚起见,我用redux-promise-middleware命名方法对它们进行了改写。)没有什么可以逃脱的。

在动作工厂,因为Promise没有进度事件,所以它现在看起来有点笨拙。您可以使用SEND_FULFILLED尝试实施。代码看起来只是一点点清洁。但处理多个事件变得轻而易举。但那里有一个学习曲线。代码应该类似于下面。专注于下面的while循环,并查看取消处理(通过redux-saga操作)。

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 }); } }); 操作。要取消上传文件,只需发送QUEUE操作。

由于CANCEL只接受Promise或action,要将事件流转换为Promise,我在这里引入了event-as-promise

使用redux-saga,流控制(进度/完成/错误/取消)变得非常清晰,不易出错。在您的情况下,我使用redux-saga的方式就像文件上传的生命周期管理器一样。设置和拆解总是成对出现。如果未正确触发拆除调用(完成/错误/取消),您无需担心。

您需要弄清楚有关文件描述符的故事。尽量不要将它放在商店中,因为它会阻止商店被保留,您可能希望在页面导航或应用程序重新启动时保持商店。取决于您的场景,如果它用于重试上传,您可以将其临时存储在传奇内的闭包中。

在此示例中,您可以轻松地将其扩展到逐个上传文件的上传队列中。如果您精通redux-saga,那么这不是一项艰巨的任务,应该少于10行代码更改。没有redux-saga,实现它​​将是一项相当大的任务。