axios结合AbortController取消文件上传

javascript 复制代码
<template>
  <div>
    <input type="file" multiple @change="handleFileUpload" />
    <button @click="cancelUpload" :disabled="!isUploading">取消上传</button>
    <div>总进度:{{ totalProgress }}%</div>
    <ul>
      <li v-for="(file, index) in fileList" :key="index">
        {{ file.name }} - {{ file.progress }}%
        <span v-if="file.error" style="color:red">(失败: {{ file.error }})</span>
      </li>
    </ul>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      fileList: [],       // 文件列表及各自进度
      totalProgress: 0,   // 总进度
      isUploading: false, // 上传状态
      controllers: new Map(), // 存储每个文件的 AbortController
      uploadedSize: 0,    // 已上传总字节数
      totalSize: 0,       // 总字节数
      prevLoadedMap: {}   // 记录上次回调的 loaded 值
    };
  },
  methods: {
    // 处理文件选择
    async handleFileUpload(event) {
      const files = event.target.files;
      if (!files.length) return;

      this.resetState();
      this.initFiles(files);
      this.isUploading = true;

      try {
        await this.uploadFiles(files);
      } catch (error) {
        if (error.message !== 'canceled') {
          console.error('上传出错:', error);
        }
      } finally {
        this.isUploading = false;
      }
    },

    // 初始化文件列表
    initFiles(files) {
      this.fileList = Array.from(files).map(file => ({
        name: file.name,
        progress: 0,
        error: null
      }));
      this.totalSize = files.reduce((sum, file) => sum + file.size, 0);
    },

    // 并行上传文件
    async uploadFiles(files) {
      const uploadPromises = Array.from(files).map((file, index) => {
        const controller = new AbortController();
        this.controllers.set(file.name, controller);

        const formData = new FormData();
        formData.append('file', file);

        return axios.post('/api/upload', formData, {
          headers: { 'Content-Type': 'multipart/form-data' },
          signal: controller.signal,
          onUploadProgress: (progressEvent) => {
            this.updateProgress(file, index, progressEvent);
          }
        }).catch(error => {
          if (!axios.isCancel(error)) {
            this.fileList[index].error = error.message;
          }
          throw error;
        });
      });

      await Promise.all(uploadPromises);
    },

    // 更新上传进度
    updateProgress(file, index, progressEvent) {
      // 计算本次新增的字节数(避免重复累加)
      const prevLoaded = this.prevLoadedMap[file.name] || 0;
      const newChunk = progressEvent.loaded - prevLoaded;
      this.uploadedSize += newChunk;
      this.prevLoadedMap[file.name] = progressEvent.loaded;

      // 更新单个文件进度
      const fileProgress = Math.round((progressEvent.loaded / file.size) * 100);
      this.fileList[index].progress = fileProgress;

      // 计算总进度
      this.totalProgress = Math.round((this.uploadedSize / this.totalSize) * 100);
    },

    // 取消上传
    cancelUpload() {
      this.controllers.forEach(controller => {
        controller.abort('用户取消上传');
      });
      this.isUploading = false;
    },

    // 重置状态
    resetState() {
      this.uploadedSize = 0;
      this.totalProgress = 0;
      this.prevLoadedMap = {};
      this.controllers.clear();
      this.fileList = [];
    }
  }
};
</script>

2. java后端实现

2.1 控制器实现

java 复制代码
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;

@RestController
public class FileUploadController {

    @PostMapping("/api/upload")
    public String uploadFile(
        @RequestParam("file") MultipartFile file,
        HttpServletRequest request // 用于监听中断
    ) throws Exception {
        
        try (InputStream inputStream = file.getInputStream()) {
            //文件上传业务
            return "上传成功";
        } catch (InterruptedException | IOException e) {
        	//前端取消请求时会触发中断异常,实现取消请求业务
            // 清理已上传的部分文件
            cleanPartialFile(file.getOriginalFilename());
            throw e;
        }
    }

    private void processChunk(byte[] chunk, int length) {
        // 实际业务处理(如写入文件)
    }

    private void cleanPartialFile(String filename) {
        // 删除未完成的上传文件
    }
}

Tomcat通过 Thread.interrupted() 检测中断(Tomcat 会在客户端断开时中断线程)。

2.2 原生 Servlet

java 复制代码
@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        try {
           	//文件上传业务实现
            response.getWriter().write("上传成功");
        } catch (Exception e) {
            cleanPartialFile();
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "上传失败");
        }
    }

    private boolean isClientConnected(HttpServletRequest request) {
        try {
            // 尝试读取1字节(如果连接关闭会抛出异常)
            request.getInputStream().read();
            return true;
        } catch (IOException e) {
            return false;
        }
    }
}

Servlet通过 request.getInputStream().isReady() 判断连接是否关闭

2.3 Spring WebFlux(响应式非阻塞)

java 复制代码
import org.springframework.http.codec.multipart.FilePart;
import reactor.core.publisher.Mono;

@RestController
public class ReactiveUploadController {

    @PostMapping("/upload")
    public Mono<String> uploadFile(@RequestPart("file") FilePart filePart) {
        return filePart.content()
            .map(dataBuffer -> {
                // 处理数据块
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                processChunk(bytes);
                return bytes;
            })
            .then(Mono.just("上传成功"))
            .onErrorResume(e -> {
                cleanPartialFile();
                return Mono.error(e);
            });
    }
}

自动感知客户端断开,无需手动检查

相关推荐
颜酱6 小时前
图结构完全解析:从基础概念到遍历实现
javascript·后端·算法
m0_736919106 小时前
C++代码风格检查工具
开发语言·c++·算法
2501_944934736 小时前
高职大数据技术专业,CDA和Python认证优先考哪个?
大数据·开发语言·python
失忆爆表症7 小时前
05_UI 组件库集成指南:Shadcn/ui + Tailwind CSS v4
前端·css·ui
小迷糊的学习记录7 小时前
Vuex 与 pinia
前端·javascript·vue.js
发现一只大呆瓜7 小时前
前端性能优化:图片懒加载的三种手写方案
前端·javascript·面试
黎雁·泠崖7 小时前
【魔法森林冒险】5/14 Allen类(三):任务进度与状态管理
java·开发语言
不爱吃糖的程序媛7 小时前
Flutter 与 OpenHarmony 通信:Flutter Channel 使用指南
前端·javascript·flutter
利刃大大7 小时前
【Vue】Element-Plus快速入门 && Form && Card && Table && Tree && Dialog && Menu
前端·javascript·vue.js·element-plus
NEXT067 小时前
AI 应用工程化实战:使用 LangChain.js 编排 DeepSeek 复杂工作流
前端·javascript·langchain