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

Redux thunk design for file upload, including cancel and progress

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

像这样

减速机

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

note : uploadFile 是我的帮手,包装了一个 axios promise。 第一个参数是一个 File 描述符,第二个参数是一个 axios 选项对象。

我认为应该可行..

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

在 reducer 方面,我认为你的行为很好(spread/rest+1)。但是通过使用一些像 simple-update-in 这样的 "update-in" 库,代码结构看起来会更好一些。它可以帮助您消除一些 filter() 电话。

在动作设计上,我猜你正在建立一个上传队列,所以你需要有QUEUE/SEND_PENDING/SEND_PROGRESS/SEND_REJECTED/SEND_FULFILLED(为了清楚起见,我用 redux-promise-middleware 命名方法重新表述了它们。)这里没有什么是你无法逃避的。

关于动作工厂,因为 Promise 没有进度事件,所以现在看起来有点笨拙。您可以尝试使用 redux-saga 来实现。代码看起来稍微干净了一点。但是处理多个事件变得轻而易举。但是那里有一个学习曲线。代码应该与下面类似。关注下面的 while-loop,并查看取消处理(通过 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 操作。

由于redux-saga只接受Promise或action,为了将事件流转化为Promise,所以这里介绍event-as-promise

有了redux-saga,流程控制(progress/done/error/cancel)变得非常清晰并且不容易出现错误。在您的情况下,我使用 redux-saga 的方式类似于文件上传的生命周期管理器。设置和拆卸总是成对进行的。如果拆解调用 (done/error/cancel) 未正确触发,您无需担心。

你需要弄清楚关于文件描述符的故事。尽量不要将它放在商店中,因为它会阻止商店被持久化,您可能希望在页面导航或应用重启时持久化商店。看你的场景,如果用于重试上传,可以暂时存放在saga里面的闭包中。

在此示例中,您可以轻松地将其扩展为上传文件 one-by-one 的上传队列。精通redux-saga不难,改代码10行以内。如果没有 redux-saga,要实现这一目标将是一项艰巨的任务。