You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:19:33