ReactJS如何实现PUT请求文件上传跨页面/刷新不中断
React SPA大文件上传跨路由/刷新续传实现方案
你的初始判断完全正确:浏览器手动刷新会销毁当前页面所有JS执行上下文、内存中存储的变量与正在进行的网络请求,单连接PUT上传不可能在刷新后直接保持运行,必须通过分片+断点续传实现用户感知上的进度留存;而SPA内部路由跳转本质是组件卸载与挂载,没有页面级的上下文销毁,只要把上传状态和请求实例抬离页面组件即可实现无中断上传。
一、路由跳转场景:留存上传实例,避免组件卸载中断
这个场景不需要重构现有上传逻辑的核心,只需要解决「上传逻辑与/new-files页面组件强绑定,组件卸载时自动终止请求、丢失进度」的问题:
- 不要把上传请求实例、实时进度值存在页面组件的
useState/useRef中,将这部分逻辑抽到全局状态层:可以用React Context、Redux/Zustand等全局状态方案,存储三类核心数据:当前上传文件的元信息(文件名、大小、唯一标识)、上传请求的XHR/fetch实例(绑定AbortController)、实时上传进度百分比。 - 上传进度条UI不要硬编码在
/new-files页面内部,抽为全局挂载的固定组件(比如挂在路由根布局Layout下),只要全局状态中存在进行中的上传任务就展示,用户跳转至其他路由时也能看到实时进度,返回/new-files页面时直接读取全局状态的进度值渲染即可,不需要重新初始化上传流程。 - 调整useEffect清理函数的逻辑:不要在组件卸载(路由跳转)时默认执行
abortController.abort()中断请求,仅当用户主动触发取消上传、上传完成/失败的场景下才执行请求中断操作。
最简全局状态实现参考(以Zustand为例):
// 全局上传状态store import { create } from 'zustand' const useUploadStore = create((set) => ({ uploadInstance: null, // 存储XHR/AbortController实例 progress: 0, fileMeta: null, startUpload: (file) => { const xhr = new XMLHttpRequest() // 绑定进度事件实时更新全局状态 xhr.upload.onprogress = (e) => { set({ progress: Math.round((e.loaded / e.total) * 100) }) } xhr.open('PUT', 'your-upload-endpoint') xhr.send(file) xhr.onload = () => set({ progress: 100 }) set({ uploadInstance: xhr, fileMeta: { name: file.name, size: file.size } }) }, clearUploadTask: () => set({ uploadInstance: null, progress: 0, fileMeta: null }) })) // /new-files页面仅负责触发上传,不持有上传状态 function NewFilesPage() { const { startUpload } = useUploadStore() return <input type="file" onChange={(e) => startUpload(e.target.files[0])} /> } // 根布局全局挂载进度条,不随路由切换卸载 function RootLayout() { const { progress, uploadInstance } = useUploadStore() return ( <div> <Outlet /> {/* react-router-dom路由出口 */} {uploadInstance && ( <div className="global-upload-progress">文件上传中:{progress}%</div> )} </div> ) }
二、页面刷新场景:分片上传+本地持久化断点续传
该场景下原有PUT请求必然会被浏览器终止,核心实现思路是:刷新前持久化已上传进度,刷新后重新初始化上传任务,从上次中断的位置继续传输,实现用户无感知的进度恢复。
实现步骤如下:
- 替换单PUT全量上传逻辑为分片上传:用
Blob.prototype.slice将大文件切为固定大小的分片(单分片大小建议100MB-200MB,平衡请求数与单分片重传成本),先基于文件的大小、最后修改时间、抽样内容生成唯一文件hash,作为续传的匹配标识。 - 对接服务端实现三个核心接口:分片上传接口、已上传分片查询接口、分片合并接口。每次初始化上传任务时,先传文件hash调用查询接口,拿到已经上传成功的分片列表,跳过这部分分片,仅上传未完成的分片,实现断点续传。
- 本地持久化上传进度:每完成一个分片的上传,就将当前文件hash、已完成分片索引、进度值存入
localStorage或IndexedDB。页面刷新后,用户选择同一文件时先校验hash是否与本地存储的记录匹配,匹配则直接读取已上传分片记录,跳过已传部分继续上传。如果需要实现刷新后不用用户重新选文件,可以用File System Access API持久化文件句柄(需用户授权,兼容Chrome/Edge等Chromium内核浏览器)。 - 所有分片传输完成后,调用服务端分片合并接口生成完整文件,同时清除本地存储的对应上传进度记录。
如果你当前使用云厂商对象存储服务(OSS、COS等)存储文件,这类服务一般原生提供分片上传、断点续传的SDK能力,直接复用SDK能力即可,不需要从零实现分片逻辑。
三、常见避坑点
- 不建议用Service Worker尝试实现刷新时请求保活:Service Worker生命周期与浏览器标签页强绑定,页面刷新/关闭时Service Worker会被冻结或终止,且保活逻辑的复杂度远高于分片续传方案,投入产出比极低。
- 单PUT全量上传不适合1GB以上的大文件场景:一旦出现网络波动就会全量重传,哪怕不需要支持刷新续传,大文件场景也建议默认使用分片上传。
- 路由跳转场景不需要把上传状态序列化存入sessionStorage/URL参数,全局状态直接持有请求实例的方案性能最优,没有序列化/反序列化开销,进度更新延迟最低。
内容的提问来源于stack exchange,提问作者Kid_Learning_C
相关产品推荐
相关产品推荐

