Angular路由切换时上传进程中断,如何保留上传进程?
解决路由切换时上传进程中断的问题
嘿,这个问题我之前做项目时也踩过坑!路由切换导致组件作用域销毁,进而中断上传确实挺闹心的,咱们来一步步解决它,顺便说说你深克隆文件对象报错的原因。
为什么深克隆File对象会触发RangeError?
File对象是浏览器提供的特殊对象,属于Blob的子类,内部包含很多循环引用的属性(比如指向自身的内部指针、关联的DOM元素引用等)。不管你用JSON.parse(JSON.stringify())还是lodash的cloneDeep这类深克隆方法,都会因为递归遍历这些循环引用而超出调用栈大小,直接报错。所以完全没必要克隆File对象,这路子走不通,换个思路就行。
正确的解决方案:把上传逻辑放在全局单例服务中
核心思路是:让上传进程脱离组件的作用域,交给全局存在的单例服务来管理。组件只负责触发上传、展示进度,而实际的上传请求(XHR/axios实例)由服务持有,这样即使组件销毁(路由切换),服务里的上传进程依然会在后台运行。
下面给你几个框架的示例:
示例1:Vue + Pinia
Pinia的store是全局单例,非常适合用来管理这类跨组件的长期任务:
// stores/upload.js import { defineStore } from 'pinia' export const useUploadStore = defineStore('upload', { state: () => ({ // 用Map存储任务ID和对应的XHR实例,方便后续取消/跟踪进度 uploadTasks: new Map() }), actions: { startUpload(file, taskId) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() const formData = new FormData() formData.append('file', file) xhr.open('POST', '/api/upload') xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)) this.uploadTasks.delete(taskId) } else { reject(new Error('上传失败')) } } xhr.onerror = () => { reject(new Error('网络错误')) this.uploadTasks.delete(taskId) } // 可选:跟踪上传进度 xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const progress = (e.loaded / e.total) * 100 console.log(`任务${taskId}进度:${progress}%`) } } this.uploadTasks.set(taskId, xhr) xhr.send(formData) }) }, cancelUpload(taskId) { const xhr = this.uploadTasks.get(taskId) if (xhr) { xhr.abort() this.uploadTasks.delete(taskId) } } } })
组件里调用时,只需要传递File对象给store的方法,路由切换后上传依然会继续:
<script setup> import { useUploadStore } from '@/stores/upload' const uploadStore = useUploadStore() const handleUpload = (file) => { const taskId = Date.now().toString() // 生成唯一任务ID uploadStore.startUpload(file, taskId) .then(res => console.log('上传成功', res)) .catch(err => console.error('上传失败', err)) } </script>
示例2:React + 自定义全局服务
React里可以写一个独立的单例服务文件,或者用Context来管理:
// services/uploadService.js const uploadTasks = new Map() export const startUpload = (file, taskId) => { return new Promise((resolve, reject) => { const formData = new FormData() formData.append('file', file) const xhr = new XMLHttpRequest() xhr.open('POST', '/api/upload') xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)) uploadTasks.delete(taskId) } else { reject(new Error('上传失败')) } } xhr.onerror = () => { reject(new Error('网络错误')) uploadTasks.delete(taskId) } uploadTasks.set(taskId, xhr) xhr.send(formData) }) } export const cancelUpload = (taskId) => { const xhr = uploadTasks.get(taskId) if (xhr) { xhr.abort() uploadTasks.delete(taskId) } }
在组件里直接调用这个服务的方法即可,组件卸载不影响后台的上传进程:
import { startUpload } from '../services/uploadService' function UploadComponent() { const handleFileChange = (e) => { const file = e.target.files[0] const taskId = Date.now().toString() startUpload(file, taskId) .then(res => console.log('上传成功', res)) .catch(err => console.error('上传失败', err)) } return <input type="file" onChange={handleFileChange} /> }
示例3:Angular + 全局Service
Angular中只要把Service注册为根注入,就是全局单例:
// upload.service.ts import { Injectable } from '@angular/core' @Injectable({ providedIn: 'root' }) export class UploadService { private uploadTasks = new Map<string, XMLHttpRequest>() startUpload(file: File, taskId: string): Promise<any> { return new Promise((resolve, reject) => { const formData = new FormData() formData.append('file', file) const xhr = new XMLHttpRequest() xhr.open('POST', '/api/upload') xhr.onload = () => { if (xhr.status === 200) { resolve(JSON.parse(xhr.response)) this.uploadTasks.delete(taskId) } else { reject(new Error('上传失败')) } } xhr.onerror = () => { reject(new Error('网络错误')) this.uploadTasks.delete(taskId) } this.uploadTasks.set(taskId, xhr) xhr.send(formData) }) } cancelUpload(taskId: string): void { const xhr = this.uploadTasks.get(taskId) if (xhr) { xhr.abort() this.uploadTasks.delete(taskId) } } }
组件中注入这个Service,调用方法即可:
import { Component } from '@angular/core' import { UploadService } from './upload.service' @Component({ selector: 'app-upload', template: '<input type="file" (change)="handleFileChange($event)">' }) export class UploadComponent { constructor(private uploadService: UploadService) {} handleFileChange(event: Event): void { const file = (event.target as HTMLInputElement).files?.[0] if (file) { const taskId = Date.now().toString() this.uploadService.startUpload(file, taskId) .then(res => console.log('上传成功', res)) .catch(err => console.error('上传失败', err)) } } }
关键总结
- 不要尝试克隆File对象,它的内部结构导致深克隆必然失败;
- 上传逻辑必须放在全局单例服务中,让上传请求实例脱离组件作用域;
- 用Map或对象存储上传任务实例,方便后续取消、跟踪进度等操作。
内容的提问来源于stack exchange,提问作者user3679514
相关产品推荐
相关产品推荐

