在开发时,一个绕不开的功能就是:
「用户一键下载自己生成的项目代码」
表面上是一个"下载按钮",但背后涉及到:
- 权限校验
- 后端流式输出 ZIP
- HTTP 响应头设计
- 前端 fetch + Blob 下载
这篇文章我结合真实项目代码,完整拆解一套前后端协作的文件下载方案。
一、整体方案概览
我们先看整体流程(非常关键):
前端:
-
点击「下载代码」
-
使用 fetch 发起 GET 请求(携带 Cookie)
-
后端返回 二进制流(zip)
-
前端将响应转成 Blob
-
使用 URL.createObjectURL 触发浏览器下载
后端:
-
校验应用是否存在
-
校验当前用户是否是创建者
-
定位生成好的代码目录
-
将目录打包为 ZIP
-
通过 HttpServletResponse 以流的形式返回
二、前端实现:fetch + Blob 的标准下载姿势
1️⃣ 下载状态与按钮控制
ts
const downloading = ref(false)
2️⃣ 使用 fetch 发起下载请求
ts
const downloadCode = async () => {
if (!appId.value) {
message.error('应用ID不存在')
return
}
downloading.value = true
try {
const API_BASE_URL = request.defaults.baseURL || ''
const url = `${API_BASE_URL}/app/download/${appId.value}`
const response = await fetch(url, {
method: 'GET',
credentials: 'include', // ⭐ 非常关键
})
if (!response.ok) {
throw new Error(`下载失败: ${response.status}`)
}
- credentials: 'include'
用于携带 Cookie(登录态)
否则后端 getLoginUser 会直接失败
- 不用 axios?
axios 对二进制下载反而更麻烦
fetch + blob 是浏览器原生、最稳定方案
3️⃣ 从响应头中解析文件名
ts
const contentDisposition = response.headers.get('Content-Disposition')
const fileName =
contentDisposition?.match(/filename="(.+)"/)?.[1]
|| `app-${appId.value}.zip`
为什么要从响应头拿文件名?
-
后端可以动态控制下载名称
-
避免前端硬编码
-
符合 HTTP 标准
4️⃣ Blob 是什么?为什么一定要用它?
Blob(Binary Large Object)本质上是:浏览器中的"二进制文件对象"
-
ZIP / PDF / 图片 / Excel
-
在 JS 世界里都必须先变成 Blob 才能下载
5️⃣ 触发浏览器下载(核心技巧)
ts
const downloadUrl = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = downloadUrl
link.download = fileName
link.click()
URL.revokeObjectURL(downloadUrl)

这是目前浏览器最标准、兼容性最好的下载方式
6️⃣ 下载按钮示例
html
<a-button
type="primary"
ghost
@click="downloadCode"
:loading="downloading"
:disabled="!isOwner"
>
<template #icon>
<DownloadOutlined />
</template>
下载代码
</a-button>
三、后端实现:权限校验 + ZIP 流式下载
1️⃣ 接口定义
java
@GetMapping("/download/{id}")
public void downloadAppCode(@PathVariable Long id,
HttpServletRequest request,
HttpServletResponse response)
2️⃣ 权限与合法性校验(必须)
3️⃣ 获取目录
4️⃣ 核心:如何设置响应头?
在 downloadProjectAsZip 内部,一定要做这几件事:
java
response.setContentType("application/octet-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader(
"Content-Disposition",
"attachment; filename=\"" + fileName + ".zip\""
);

如果不设置 Content-Disposition:
浏览器可能直接尝试"打开"
前端拿不到文件名
5️⃣ ZIP 流式输出(关键思想)
核心原则:
- 不要先生成 zip 文件再读, 边压缩,边写入 response 输出流
优点:
-
节省磁盘
-
支持大项目
-
更快响应