基于Redux Thunk实现带取消与进度追踪的文件上传方案咨询
嘿,这个方案的核心思路完全可行,咱们来拆解下它的合理性和可以优化的细节:
可行性分析
- 核心逻辑贴合Redux设计:用Redux-Thunk处理异步上传流程,自定义可序列化的文件状态描述符,配合FSA规范的action更新store,这完全符合Redux的状态管理原则——可序列化的状态能保证store的可追溯性,甚至后续要做状态持久化也毫无障碍。
- 进度追踪与取消的基础已具备:你定义的
pending、sent状态已经能覆盖上传的核心阶段,后续只需要在Thunk里监听XMLHttpRequest的progress事件,触发action更新上传字节数就能实现进度追踪;取消功能则可以通过在Thunk中保存XHR实例,调用abort()方法实现,同时触发对应的状态更新action即可。 - FSA规范的action让reducer更易维护:用switch-case处理不同action类型的写法很标准,逻辑清晰,后续扩展新状态时也能快速迭代。
优化建议
1. 细化状态字段,覆盖更多场景
- 把
sent字段拆成uploadedBytes和totalBytes,这样能精准计算上传进度百分比,UI层展示进度条更方便; - 新增
canceled状态,和errored区分开——取消操作是用户主动行为,和网络错误等被动错误的状态逻辑应该分开,避免混淆; - 给文件描述符加
fileName字段,UI层直接从store取文件名展示,不用额外关联原始File对象,更简洁。
2. 优化Thunk的异步逻辑与取消机制
- 用一个模块作用域的映射表(比如
xhrMap)保存XHR实例,以文件uuid为key,这样外部触发取消操作时能快速找到对应的XHR。示例代码如下:// 模块作用域的XHR映射表,用于存储待取消的上传请求 const xhrMap = new Map(); export const uploadFile = (file) => async (dispatch) => { const uuid = generateUUID(); // 自行实现uuid生成逻辑,比如用uuid库 const xhr = new XMLHttpRequest(); xhrMap.set(uuid, xhr); // 初始化文件状态 dispatch(addFile({ uuid, fileName: file.name, pending: true, uploadedBytes: 0, totalBytes: file.size, canceled: false, errored: false, errorMessage: '' })); // 监听上传进度 xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { dispatch(updateFileProgress(uuid, e.loaded, e.total)); } }); // 上传成功 xhr.addEventListener('load', () => { xhrMap.delete(uuid); // 如果需要保存上传后的结果(比如文件URL),可以在这里传入响应数据 dispatch(setFileCompleted(uuid, JSON.parse(xhr.responseText))); }); // 上传错误 xhr.addEventListener('error', () => { xhrMap.delete(uuid); dispatch(setFileErrored(uuid, '上传失败,请检查网络')); }); // 上传取消 xhr.addEventListener('abort', () => { xhrMap.delete(uuid); dispatch(setFileCanceled(uuid)); }); // 发送上传请求 xhr.open('POST', '/api/upload'); const formData = new FormData(); formData.append('file', file); xhr.send(formData); }; // 新增取消上传的action export const cancelUpload = (uuid) => () => { const xhr = xhrMap.get(uuid); if (xhr) { xhr.abort(); } };
3. 优化Reducer的状态存储结构
- 把文件状态从数组改成对象映射:现在用数组存储的话,每次更新某个文件都要遍历数组查找uuid,效率较低;改成
files: { [uuid]: fileDescriptor }的结构,更新时直接通过uuid定位,性能更好,示例如下:const initialState = { files: {} }; const filesReducer = (state = initialState, action) => { switch (action.type) { case FILES__ADD_FILE: return { ...state, files: { ...state.files, [action.payload.uuid]: action.payload } }; case FILES__UPDATE_PROGRESS: const { uuid, uploadedBytes, totalBytes } = action.payload; return { ...state, files: { ...state.files, [uuid]: { ...state.files[uuid], uploadedBytes, totalBytes } } }; case FILES__SET_CANCELED: return { ...state, files: { ...state.files, [action.payload.uuid]: { ...state.files[action.payload.uuid], pending: false, canceled: true } } }; // 其他action的处理逻辑类似 default: return state; } };
4. 其他细节优化
- 错误处理细化:
setFileErrored可以携带具体的错误信息,比如接口返回的错误提示,方便UI层展示给用户; - UI层配合:组件里通过
useSelector获取对应uuid的文件状态,渲染进度条、取消按钮等,点击取消按钮时直接触发cancelUpload(uuid)即可; - 清理冗余状态:上传完成、取消或错误后,把
pending设为false,让状态更清晰。
内容的提问来源于stack exchange,提问作者Cyril CHAPON
相关产品推荐
相关产品推荐

