一、背景
在前端开发中,文件下载是一个非常常见的功能需求,比如导出 Excel、下载 PDF 等。通常我们会使用 axios 或 fetch 设置 responseType: 'blob' 来处理文件流。但实际开发中,我们常常会遇到一个棘手的问题:
当后端返回错误信息时(如参数校验失败),前端无法正确解析和展示错误提示。
典型场景
以最近开发的「导出诊疗项目明细」功能为例:
javascript
// API 定义
export const exportTreatmentList = (search) =>
request({
url: "/report/costItem/export",
method: "post",
data: search,
responseType: "blob", // 设置为 blob
});
调用时:
javascript
exportTreatmentList(params).then((res) => {
console.log(res); // Blob {size: 56, type: 'application/json'}
// 无法通过 res.code 判断错误,因为 res 不是预期的 JSON 对象
});
问题 :后端返回了 { code: 12000, msg: "请选择起始日期" },但前端收到的却是 Blob 对象,导致错误信息无法被正确处理。
二、问题分析
2.1 为什么会出现这个问题?
当设置 responseType: 'blob' 后,无论后端返回的是什么内容,浏览器都会将其包装为 Blob 对象:
| 后端返回 | 前端收到的 Blob |
|---|---|
| Excel 文件流 | Blob { type: 'application/octet-stream' } |
| JSON 错误信息 | Blob { type: 'application/json' } |
| 纯文本错误信息 | Blob { type: 'text/plain' } |
2.2 解决方案
通过 判断 Blob 的 type 属性 来区分成功还是失败:
type === 'application/json'→ 后端返回了错误信息- 其他类型(如
application/octet-stream)→ 正常的文件流
三、解决方案实现
3.1 基础实现
javascript
exportExcel() {
this.loadingExcel = true;
exportTreatmentList(params)
.then((res) => {
this.loadingExcel = false;
// 通过 Blob.type 判断返回内容类型
if (res.type === "application/json") {
// 错误信息:用 FileReader 读取文本内容
const reader = new FileReader();
reader.onload = (e) => {
try {
const result = JSON.parse(e.target.result);
this.$message.error(result.msg || "导出失败");
} catch (err) {
this.$message.error("导出失败");
}
};
reader.readAsText(res);
} else {
// 正常文件流:触发下载
const blob = new Blob([res], {
type: "application/octet-stream",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "诊疗项目明细.xlsx");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
})
.catch((error) => {
this.loadingExcel = false;
this.$message.error("导出失败");
});
}
3.2 核心流程
接口返回 → 判断 Blob.type
├── 'application/json' → FileReader 读取 → JSON.parse → 显示错误
└── 其他类型 → 触发文件下载
四、拓展延伸
4.1 通用文件下载工具函数
在多个页面都有导出需求时,可以封装一个通用工具函数:
javascript
// utils/download.js
/**
* 通用文件下载方法
* @param {Promise} requestPromise - 返回 Blob 的请求 Promise
* @param {string} filename - 下载文件名
* @param {function} onError - 错误回调
*/
export function downloadFile(requestPromise, filename = "download.xlsx", onError) {
return requestPromise.then((res) => {
// 判断是否为 JSON 错误信息
if (res.type === "application/json") {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const error = JSON.parse(e.target.result);
if (onError) {
onError(error);
} else {
ElMessage.error(error.msg || "下载失败");
}
reject(error);
} catch (err) {
ElMessage.error("下载失败");
reject(err);
}
};
reader.readAsText(res);
});
}
// 正常下载
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
});
}
使用示例:
javascript
import { downloadFile } from "@/utils/download";
import { exportTreatmentList } from "@/api/toll/treatmentList";
exportExcel() {
const params = { /* ... */ };
downloadFile(
exportTreatmentList(params),
"诊疗项目明细.xlsx"
).catch(() => {
// 错误已在 downloadFile 内部处理
});
}
4.2 支持自定义错误处理
javascript
// 在 downloadFile 基础上支持自定义错误处理
export function downloadFile(config) {
const { request, filename, onSuccess, onError } = config;
return request.then((res) => {
if (res.type === "application/json") {
const reader = new FileReader();
reader.onload = (e) => {
try {
const error = JSON.parse(e.target.result);
if (onError) {
onError(error);
}
} catch {
ElMessage.error("下载失败");
}
};
reader.readAsText(res);
return;
}
// 成功下载
if (onSuccess) {
onSuccess(res);
}
// 触发下载
const url = window.URL.createObjectURL(new Blob([res]));
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
window.URL.revokeObjectURL(url);
});
}
4.3 进度条支持
对于大文件下载,可以显示进度条:
javascript
import axios from "axios";
export function downloadWithProgress(url, filename, onProgress) {
return axios({
url,
method: "get",
responseType: "blob",
onDownloadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percentCompleted);
}
},
}).then((res) => {
// 处理下载...
});
}
4.4 大文件分片下载
对于超大文件,可以考虑分片下载:
javascript
async function downloadInChunks(url, filename, chunkSize = 5 * 1024 * 1024) {
// 1. 获取文件大小
const headResponse = await fetch(url, { method: "HEAD" });
const fileSize = parseInt(headResponse.headers.get("Content-Length"));
// 2. 分片下载
const chunks = [];
let offset = 0;
while (offset < fileSize) {
const end = Math.min(offset + chunkSize, fileSize - 1);
const response = await fetch(url, {
headers: { Range: `bytes=${offset}-${end}` }
});
const blob = await response.blob();
chunks.push(blob);
offset = end + 1;
}
// 3. 合并分片
const finalBlob = new Blob(chunks, { type: "application/octet-stream" });
// 4. 触发下载
const link = document.createElement("a");
link.href = URL.createObjectURL(finalBlob);
link.download = filename;
link.click();
}
五、相关技术点
5.1 Blob 对象
Blob(Binary Large Object)是表示二进制数据的对象:
javascript
const blob = new Blob([data], { type: "application/octet-stream" });
// Blob 属性
blob.size // 大小(字节)
blob.type // MIME 类型
常见 MIME 类型:
| 类型 | 说明 |
|---|---|
application/octet-stream |
二进制文件流 |
application/json |
JSON 数据 |
text/plain |
纯文本 |
text/html |
HTML 文档 |
image/png |
PNG 图片 |
application/pdf |
PDF 文件 |
5.2 FileReader 对象
FileReader 用于读取 Blob 或 File 对象的内容:
javascript
const reader = new FileReader();
reader.onload = (e) => {
console.log(e.target.result); // 读取结果
};
// 读取方式
reader.readAsText(blob); // 读取为文本
reader.readAsDataURL(blob); // 读取为 DataURL (base64)
reader.readAsArrayBuffer(blob); // 读取为 ArrayBuffer
5.3 URL.createObjectURL
创建临时的 Blob URL:
javascript
const url = URL.createObjectURL(blob);
// 使用 url...
URL.revokeObjectURL(url); // 释放内存
注意 :使用后需要调用 revokeObjectURL 释放内存。
六、最佳实践
6.1 统一错误处理
javascript
// 在 axios 拦截器中统一处理
axios.interceptors.response.use(
(response) => {
if (response.config.responseType === "blob") {
return response.data; // 返回 Blob
}
return response.data;
},
(error) => {
// 处理网络错误
return Promise.reject(error);
}
);
6.2 类型判断工具函数
javascript
/**
* 判断 Blob 是否为 JSON 错误
*/
export function isJsonError(blob) {
return blob.type === "application/json";
}
/**
* 解析 JSON 错误信息
*/
export function parseBlobError(blob) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
resolve(JSON.parse(e.target.result));
} catch {
resolve({ msg: "未知错误" });
}
};
reader.readAsText(blob);
});
}
6.3 完整示例
javascript
async function handleExport(requestPromise, filename) {
try {
const res = await requestPromise;
if (isJsonError(res)) {
const error = await parseBlobError(res);
ElMessage.error(error.msg || "导出失败");
return;
}
// 触发下载
const url = URL.createObjectURL(res);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
ElMessage.success("导出成功");
} catch (err) {
ElMessage.error("网络错误,请重试");
}
}
七、总结
核心要点
- 问题根源 :
responseType: 'blob'会将所有响应包装为 Blob - 判断方法 :通过
Blob.type区分成功/失败 - 错误解析 :使用
FileReader读取 Blob 内容 - 通用封装:抽离工具函数,统一处理逻辑
注意事项
- ✅ 使用后及时调用
URL.revokeObjectURL()释放内存 - ✅ 处理
FileReader的异步读取 - ✅ 兼容不同 Babel 版本(
catch (err)而非catch {}) - ✅ 考虑大文件下载进度显示
- ✅ 支持多种文件格式(Excel、PDF、图片等)
希望这篇文章能帮助你更好地处理前端文件下载和错误处理的问题!