java+vue实现文件下载进度条
- 方案
- [1. Java 后端(Spring Boot)](#1. Java 后端(Spring Boot))
- [2. Vue组件](#2. Vue组件)
- [3. 效果](#3. 效果)
- [4. 可继续扩展](#4. 可继续扩展)
方案
先让后端"流式写入 "并把已写入字节数放到内存里,前端发两次请求:
①
/download/{fileKey}真正下文件;②
/progress/{fileKey}每 500 ms 轮询一次进度。
1. Java 后端(Spring Boot)
java
@RestController
@RequestMapping("/api")
public class FileDownloadController {
// key=文件唯一标识,value=已写入字节
private final Map<String, AtomicLong> progressMap = new ConcurrentHashMap<>();
@GetMapping("/download/{fileKey}")
public void download(@PathVariable String fileKey,
HttpServletResponse resp) throws IOException {
File file = new File("D:/tmp/bigfile.zip"); // 任意大文件
long total = file.length();
AtomicLong counter = new AtomicLong(0);
progressMap.put(fileKey, counter);
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
resp.setContentLengthLong(total);
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
ServletOutputStream out = resp.getOutputStream()) {
byte[] buf = new byte[8192];
int len;
while ((len = bis.read(buf)) != -1) {
out.write(buf, 0, len);
counter.addAndGet(len); // 实时累加
}
out.flush();
} finally {
progressMap.remove(fileKey);
}
}
@GetMapping("/progress/{fileKey}")
public Map<String, Long> progress(@PathVariable String fileKey) {
AtomicLong c = progressMap.get(fileKey);
long done = c == null ? 0 : c.get();
return Map.of("done", done);
}
}
- 通过
bis.read(buf)读取文件字节数- 返回值 len 表示本次实际读到的字节数,可能小于或等于 buf.length(这里是 8192 字节)。
- 当文件读完时,read 返回 -1。
2. Vue组件
vue
<template>
<div>
<el-button type="primary" @click="handleDownload">下载文件</el-button>
<el-progress
v-if="percent > 0"
:percentage="percent"
:stroke-width="12"
style="width: 360px"
/>
</div>
</template>
<script>
export default {
data() {
return {
percent: 0,
fileKey: '' // 本次下载的唯一标识
};
},
methods: {
handleDownload() {
this.fileKey = Date.now() + ''; // 简单生成 key
this.percent = 0;
// 1. 先启动轮询
const timer = setInterval(async () => {
const { data } = await this.$axios.get(`/api/progress/${this.fileKey}`);
const done = data.done;
// 假设总大小 200 MB(可按需让后端再接口返回 total)
const total = 200 * 1024 * 1024;
this.percent = Math.round((done / total) * 100);
if (this.percent >= 100) clearInterval(timer);
}, 500);
// 2. 真正下载文件(不阻塞 UI)
this.$axios({
url: `/api/download/${this.fileKey}`,
method: 'get',
responseType: 'blob'
}).then(res => {
// 浏览器触发保存
const blob = new Blob([res.data]);
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'bigfile.zip';
link.click();
URL.revokeObjectURL(link.href);
}).catch(() => {
clearInterval(timer);
this.percent = 0;
});
}
}
};
</script>
3. 效果
- 点击"下载文件"按钮 → 进度条从 0% 开始实时增长;
- 下载完成自动触发浏览器保存窗口;
- 支持任意大文件,不占内存(后端流式输出,前端 Blob 接收)。
4. 可继续扩展
- 让
/progress接口同时返回total,前端即可精确百分比; - 下载失败/取消时清除轮询;
- 多文件同时下载时给每个文件分配独立
fileKey即可。