从业务需求到可下载文件 (Blob + a 标签方式下载)
以下以excel文件为例
一、业务场景
假设页面上有一张列表,每条数据后面都有一个“导出”按钮。用户点击后,希望导出当前数据对应的文件。
这个需求本质上是一个“文件下载”场景,但它与普通的 JSON 接口不同:
- 返回值是文件流,而不是业务数据对象。
- 需要处理响应头中的文件名。
- 需要兼容不同浏览器对下载行为的差异。
- 需要在失败时给用户一个明确反馈,而不是静默失败。
二、核心实现思路
前端导出文件的标准做法,可以概括成一句话:
发起
GET请求拿到二进制流,读取响应头中的文件名,构造Blob下载链接并触发浏览器下载。
下面是一个适合测试环境的 mock 写法:
async excelExport(id) {
// 先锁定页面,避免用户在导出过程中重复点击
const loading = this.$loading({
lock: true,
text: "正在导出中...",
spinner: "el-icon-loading",
});
try {
// 这里使用虚假地址(需要自行替换成真实接口)
const url = `https://test.hi.com/mock-api/document/export?id=${id}`;
// 直接请求文件流,注意这里返回的不是普通 JSON
const response = await fetch(url, {
method: "GET",
credentials: "include",
});
// 先判断响应状态,避免把错误页面也当成文件下载
if (!response.ok) {
throw new Error("导出失败,接口返回异常");
}
// 优先从响应头中解析文件名,保持服务端命名一致
const disposition = response.headers.get("content-disposition") || "";
const fileName = (() => {
const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
const fileMatch = disposition.match(/filename="?([^"]+)"?/i);
if (fileMatch?.[1]) return fileMatch[1];
return `导出文件_${Date.now()}.xlsx`;
})();
// 将二进制内容转成 Blob,再生成临时下载链接
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.style.display = "none";
link.download = fileName;
document.body.appendChild(link);
// 模拟用户点击,触发浏览器下载行为
link.click();
document.body.removeChild(link);
// 下载完成后及时释放临时 URL,避免内存浪费
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error("导出失败:", error);
} finally {
// 无论成功还是失败,都要恢复页面状态
loading.close();
}
}
如果没有获取到文件名,需要后端同学配合修改接口相应头
Access-Control-Expose-Headers: Content-Disposition
Content-Disposition: attachment; filename=“report.xlsx”; filename*=UTF-8’'导出报表名称.xlsx
三、为什么要这样写
Blob + a 标签 是最通用的下载方案
这套方案兼容性比较好,适合大多数现代浏览器。核心逻辑是:
- 把响应内容转成
Blob - 用
URL.createObjectURL生成临时地址 - 创建一个隐藏的
<a>标签 - 通过
click()触发下载 - 下载完成后及时释放临时 URL
&spm=1001.2101.3001.5002&articleId=163826606&d=1&t=3&u=add17922bbb4434f96f99e6d7388bd93)
509

被折叠的 条评论
为什么被折叠?



