前端调用后端 API 下载 PDF、ZIP、图片、证书等文件时,应该如何正确接收文件数据并让浏览器触发下载?
完整流程可以理解为:
Vue / JavaScript
│
│ Axios 请求
▼
后端 API
│
│ 读取服务器文件
▼
HTTP Response
│
│ 文件数据
▼
Axios
│
│ responseType: "blob"
▼
Blob
│
│ URL.createObjectURL()
▼
临时 blob URL
│
▼
<a download>
│
▼
浏览器下载文件
最核心的一句话:
Blob 是浏览器中表示原始文件数据的一种对象。前端通过 Axios 下载文件时,可以使用
responseType: "blob"将 HTTP 响应作为 Blob 接收,再通过URL.createObjectURL()创建临时地址,最终触发浏览器下载。
一、什么是 Blob?
Blob 全称:
Binary Large Object
即:
二进制大型对象
在浏览器 JavaScript 中,Blob 是一种用于表示不可变原始数据的对象。
它可以表示:
PDF
ZIP
图片
音频
视频
Excel
Word
证书
普通文本文件
其他文件数据
例如:
const blob = new Blob(["Hello World"], {
type: "text/plain"
})
此时:
"Hello World"
↓
new Blob()
↓
Blob 对象
Blob 不等于“磁盘上的文件”。
它更准确地说是:
浏览器内存中保存的一块文件/原始数据。
二、为什么下载文件经常使用 responseType: "blob"?
使用 Axios 请求普通 API 时:
const res = await axios.get("/api/user")
后端可能返回:
{
"id": 1,
"name": "Tom"
}
Axios 会按照普通 API 数据进行处理。
但是,如果接口返回的是:
PDF
ZIP
PNG
JPG
XLSX
CRT
我们的目标不是读取:
res.data.name
而是:
把整个 HTTP Response Body 当作文件数据处理。
所以可以指定:
responseType: "blob"
例如:
const response = await axios.get("/api/download", {
responseType: "blob"
})
此时:
response.data
就是一个:
Blob
三、一个需要修正的理解
原来的理解是:
JavaScript 默认会把服务器返回的数据解析成字符串,如果不设置
responseType: "blob",二进制文件就一定会损坏。
这个说法过于绝对。
更准确的理解是:
Axios 会根据配置、响应头和默认转换逻辑处理响应。对于文件下载接口,显式设置
responseType: "blob"可以明确告诉 Axios:我要把响应数据作为浏览器 Blob 接收,而不是按照普通 JSON 等业务数据处理。
另外:
.crt
.key
也不一定都是二进制文件。
例如很多 PEM 格式证书其实是文本:
-----BEGIN CERTIFICATE-----
MIID...
...
-----END CERTIFICATE-----
但即使文件本身是文本格式,如果我们的目标是:
把它作为一个完整文件下载下来
仍然可以使用:
responseType: "blob"
所以不要把 Blob 简单理解成:
Blob = 只能处理二进制文件
更好的理解是:
Blob = 浏览器用于表示原始文件数据的对象
四、前端下载文件的完整过程
假设用户点击:
下载证书
前端调用:
GET /api/certificate/download
后端返回:
certificate.crt
整个过程:
用户点击下载
↓
Vue
↓
Axios
↓
GET /api/certificate/download
↓
Gin Controller
↓
读取 certificate.crt
↓
HTTP Response
↓
responseType: "blob"
↓
response.data
↓
Blob
↓
URL.createObjectURL(blob)
↓
blob:http://localhost/xxxx
↓
创建 <a>
↓
a.click()
↓
浏览器下载
五、Vue + Axios 下载文件示例
假设后端接口:
GET /api/certificate/download
前端:
import axios from "axios"
async function downloadCertificate() {
try {
const response = await axios.get(
"/api/certificate/download",
{
responseType: "blob"
}
)
const blob = response.data
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = "certificate.crt"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
} catch (error) {
console.error("文件下载失败:", error)
}
}
六、每一步到底在干什么?
第一步:发送请求
const response = await axios.get(
"/api/certificate/download",
{
responseType: "blob"
}
)
告诉 Axios:
我要请求这个文件,并把 Response Body 作为 Blob 返回。
于是:
response.data
得到的是:
Blob
七、为什么还要 URL.createObjectURL()?
现在:
const blob = response.data
Blob 只是 JavaScript 内存中的一个对象。
浏览器 <a> 标签通常需要:
<a href="某个URL">
所以需要:
const url = URL.createObjectURL(blob)
浏览器会为这个 Blob 创建一个临时 URL。
例如概念上类似:
blob:http://localhost:5173/550e8400-e29b-41d4-a716
于是:
Blob
↓
URL.createObjectURL()
↓
blob URL
现在浏览器就可以通过这个 URL 访问 Blob 数据。
八、创建隐藏的 a 标签
接下来:
const link = document.createElement("a")
相当于动态创建:
<a></a>
然后:
link.href = url
相当于:
<a href="blob:http://localhost/..."></a>
再指定:
link.download = "certificate.crt"
相当于:
<a
href="blob:http://localhost/..."
download="certificate.crt"
>
</a>
download 属性告诉浏览器:
不要正常打开这个 URL,而是把它作为文件下载。
九、为什么调用 link.click()?
代码:
link.click()
相当于程序模拟用户点击:
<a download="certificate.crt">
下载
</a>
于是浏览器开始下载:
certificate.crt
因此:
link.click()
本质就是:
通过 JavaScript 主动触发浏览器的文件下载行为。
十、为什么最后要 revokeObjectURL?
前面执行:
const url = URL.createObjectURL(blob)
浏览器为 Blob 创建了一个临时 URL。
使用完成后应该:
URL.revokeObjectURL(url)
告诉浏览器:
这个临时 URL 已经不用了,可以释放相关资源。
所以完整生命周期:
Blob
↓
createObjectURL()
↓
临时 URL
↓
浏览器下载
↓
revokeObjectURL()
↓
释放临时 URL
对于偶尔下载一个小文件,不释放通常不会立即产生明显问题。
但如果页面长期运行并不断创建 Blob URL:
下载
下载
下载
下载
...
一直不释放,就可能造成不必要的内存占用。
因此推荐:
URL.revokeObjectURL(url)
十一、完整 Vue 3 示例
例如 Vue 3:
<script setup>
import axios from "axios"
const downloadCertificate = async () => {
try {
const response = await axios.get(
"/api/certificate/download",
{
responseType: "blob"
}
)
// 获取 Blob
const blob = response.data
// 创建临时 URL
const url = URL.createObjectURL(blob)
// 创建下载标签
const link = document.createElement("a")
link.href = url
link.download = "certificate.crt"
document.body.appendChild(link)
// 触发下载
link.click()
// 删除临时 DOM
document.body.removeChild(link)
// 释放 Blob URL
URL.revokeObjectURL(url)
} catch (error) {
console.error("下载证书失败:", error)
}
}
</script>
<template>
<button @click="downloadCertificate">
下载证书
</button>
</template>
十二、后端 Gin 如何返回文件?
假设服务器存在:
/data/certificate.crt
Gin Controller 可以:
func DownloadCertificate(c *gin.Context) {
filePath := "/data/certificate.crt"
c.Header(
"Content-Disposition",
`attachment; filename="certificate.crt"`,
)
c.Header(
"Content-Type",
"application/octet-stream",
)
c.File(filePath)
}
前端:
const response = await axios.get(
"/api/certificate/download",
{
responseType: "blob"
}
)
于是完整流程就是:
Vue
│
│ GET
▼
Gin Router
│
▼
Controller
│
│ c.File()
▼
certificate.crt
│
▼
HTTP Response Body
│
▼
Axios
│
│ responseType: blob
▼
Blob
│
▼
createObjectURL()
│
▼
<a download>
│
▼
浏览器保存文件
十三、Content-Type 是什么?
后端返回文件时通常会设置:
Content-Type
它告诉浏览器:
Response Body 中的数据是什么类型。
例如:
PDF
application/pdf
ZIP
application/zip
PNG
image/png
JPEG
image/jpeg
JSON
application/json
普通二进制文件
application/octet-stream
例如 PDF:
c.Header(
"Content-Type",
"application/pdf",
)
ZIP:
c.Header(
"Content-Type",
"application/zip",
)
不知道具体类型或者希望作为通用文件处理时,经常使用:
application/octet-stream
十四、Content-Disposition 是什么?
文件下载接口中还有一个非常重要的 Header:
Content-Disposition
例如:
Content-Disposition: attachment; filename="certificate.crt"
其中:
attachment
表示:
希望浏览器把响应作为附件下载。
而:
filename="certificate.crt"
表示建议下载文件名:
certificate.crt
所以:
Content-Type
解决:
这是什么数据?
而:
Content-Disposition
解决:
浏览器应该如何处理它,以及建议使用什么文件名?
十五、让后端决定文件名
实际项目中,文件名通常不是写死在前端:
link.download = "certificate.crt"
而是后端返回:
Content-Disposition: attachment; filename="certificate.crt"
前端读取:
const disposition =
response.headers["content-disposition"]
然后解析文件名。
简单示例:
const response = await axios.get(
"/api/certificate/download",
{
responseType: "blob"
}
)
let filename = "download"
const disposition =
response.headers["content-disposition"]
if (disposition) {
const match = disposition.match(/filename="?([^"]+)"?/)
if (match) {
filename = match[1]
}
}
const url = URL.createObjectURL(response.data)
const link = document.createElement("a")
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
于是:
后端
│
│ Content-Disposition
│ filename="certificate.crt"
▼
前端
│
│ 读取 filename
▼
link.download
│
▼
certificate.crt
实际生产项目如果需要支持中文文件名,还需要正确处理 filename*、UTF-8 编码以及跨域时的响应头暴露问题。
十六、Blob 不只是用于下载
Blob 还有一个很常见的用途:
预览文件。
例如后端返回一张图片:
const response = await axios.get(
"/api/image",
{
responseType: "blob"
}
)
const imageUrl =
URL.createObjectURL(response.data)
Vue:
<img :src="imageUrl">
流程:
服务器图片
↓
Axios
↓
Blob
↓
createObjectURL()
↓
blob URL
↓
<img src="">
↓
浏览器显示图片
PDF 也可以采用类似思路进行预览。
十七、Blob 和 File 的区别
JavaScript 中还有一个:
File
实际上可以简单理解为:
File 是带有文件信息的 Blob
关系大致是:
Blob
↑
File
Blob 主要描述:
一块原始数据
而 File 在 Blob 基础上还包含:
文件名
修改时间
文件类型
例如用户通过:
<input type="file">
选择文件之后,JavaScript 获得的通常是:
File
而从 Axios 文件下载接口获得:
response.data
在设置:
responseType: "blob"
以后通常得到的是:
Blob
十八、Blob 和 FormData 不要混淆
这两个概念经常同时出现在文件功能中,但用途不同。
下载
通常:
服务器
↓
HTTP Response
↓
Blob
↓
浏览器
上传
通常:
File
↓
FormData
↓
HTTP Request
↓
服务器
例如上传:
const formData = new FormData()
formData.append("file", file)
await axios.post(
"/api/upload",
formData
)
所以可以先简单记:
下载文件
↓
Blob
上传文件
↓
File + FormData
这不是绝对规则,但对于常见 Web 开发场景非常实用。
十九、为什么普通文件链接有时根本不需要 Blob?
Blob 并不是所有下载都必须使用。
例如:
<a href="/api/download/file">
下载文件
</a>
如果后端已经正确返回:
Content-Disposition: attachment
浏览器本身就可以直接下载。
甚至可以:
window.location.href = "/api/download/file"
因此:
文件下载 ≠ 必须 Blob
Blob 更适合以下情况:
需要 Axios / Fetch 请求文件
需要携带 Token
需要先检查接口结果
需要动态处理文件
需要生成预览
需要前端决定文件名
需要对下载数据进一步处理
二十、什么时候应该使用 Blob?
场景一:下载 PDF
responseType: "blob"
例如:
合同
发票
报告
证书
场景二:下载 ZIP
例如后端动态打包:
certificate.crt
private.key
config.json
生成:
certificate.zip
前端:
responseType: "blob"
然后下载。
场景三:下载 Excel
例如:
用户点击“导出 Excel”
↓
后端生成 users.xlsx
↓
HTTP Response
↓
Blob
↓
浏览器下载
场景四:图片/PDF预览
例如:
API
↓
Blob
↓
createObjectURL()
↓
<img>
或者:
<iframe>
进行预览。
二十一、最终完整心智模型
以后遇到“前端下载文件”,可以直接想到:
① 用户点击下载
↓
② Axios 请求后端
↓
③ 后端读取/生成文件
↓
④ HTTP Response 返回文件数据
↓
⑤ Axios 使用
responseType: "blob"
↓
⑥ response.data 得到 Blob
↓
⑦ URL.createObjectURL(blob)
↓
⑧ 创建临时 blob URL
↓
⑨ 创建 <a>
↓
⑩ 设置 download 文件名
↓
⑪ link.click()
↓
⑫ 浏览器下载
↓
⑬ URL.revokeObjectURL()
↓
⑭ 释放临时 URL
对应代码:
const response = await axios.get(
"/api/download",
{
responseType: "blob"
}
)
const blob = response.data
const url = URL.createObjectURL(blob)
const link = document.createElement("a")
link.href = url
link.download = "example.zip"
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
后端 Gin:
func Download(c *gin.Context) {
filePath := "/data/example.zip"
c.Header(
"Content-Disposition",
`attachment; filename="example.zip"`,
)
c.Header(
"Content-Type",
"application/zip",
)
c.File(filePath)
}
二十二、总结
Blob 是浏览器 JavaScript 中表示原始文件数据的重要对象。
在 Axios 中:
responseType: "blob"
表示:
把 HTTP Response 的数据作为 Blob 文件数据交给前端。
前端获得 Blob 后:
response.data
再通过:
URL.createObjectURL(blob)
创建临时 URL。
然后:
link.href = url
link.download = "文件名"
link.click()
触发浏览器下载。
最终:
URL.revokeObjectURL(url)
释放临时 URL。
所以整个文件下载过程可以浓缩成:
后端文件
↓
HTTP Response
↓
Axios
↓
responseType: "blob"
↓
Blob
↓
createObjectURL()
↓
临时 URL
↓
<a download>
↓
浏览器下载
最需要记住的是:
Blob 解决的是“浏览器如何表示和操作文件/原始数据”的问题;
responseType: "blob"解决的是“Axios 应该以什么形式把 HTTP 响应交给前端代码”的问题;URL.createObjectURL()解决的是“如何让浏览器中的其他 API 通过 URL 使用这块 Blob 数据”的问题。
浙公网安备 33010602011771号