<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 控制器实现
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
@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(响应式非阻塞)
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);
});
}
}
自动感知客户端断开,无需手动检查