
如何通过JS代码下载接口返回的文件并保留文件名
调用下载接口时,文件内容通常以二进制响应返回,文件名则由响应头提供。浏览器不会自动把接口响应变成用户下载的文件:前端需要读取响应、创建可下载资源,并把文件名交给浏览器。若文件名含中文、空格或扩展名,还要正确处理服务端的 Content-Disposition 响应头。
本文从 Fetch API 的常见实现开始,逐步说明响应头解析、跨域限制、文件名安全处理和易错场景,并补充 Axios 的写法。示例面向浏览器端 JavaScript,不适用于 Node.js 服务端下载场景。
一、适用场景与处理流程
这种实现适用于用户点击按钮后,请求需要携带登录态、查询条件或其他请求头的下载接口。例如导出报表、下载合同、获取图片或压缩包。相比直接把接口 URL 放进链接,使用 JavaScript 请求可以控制请求方法、参数和身份凭据,也能检查 HTTP 状态码并读取响应头。
典型流程如下:
- 使用 Fetch 或 Axios 请求下载接口。
- 检查 HTTP 状态码,避免把错误提示当作文件保存。
- 将响应体读取为
Blob。 - 从
Content-Disposition中取得文件名,并对其进行校验。 - 创建临时对象 URL,通过临时
<a>元素触发下载。 - 下载触发后释放对象 URL。
关键点是:文件名和文件内容是两回事。Blob 保存响应体,响应头可能提供文件名;如果接口没有返回文件名,前端只能使用约定的默认名称或自行生成名称。
二、最小可运行示例
下面的函数使用 Fetch 发起请求,读取服务端文件名;响应头缺少文件名时使用调用方提供的默认值。把示例中的 URL 和请求参数替换为实际接口即可。
async function downloadFile(url, options = {}) {
const {
method = "GET",
headers = {},
body,
credentials = "same-origin",
fallbackName = "download"
} = options;
const response = await fetch(url, {
method,
headers,
body,
credentials
});
// fetch 遇到 404、500 等 HTTP 错误时通常仍会正常返回 Response,
// 因此需要显式检查状态码。
if (!response.ok) {
throw new Error(`下载失败:HTTP ${response.status}`);
}
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition");
const filename = getSafeFilename(
getFilenameFromDisposition(disposition) || fallbackName
);
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = filename;
link.style.display = "none";
document.body.appendChild(link);
link.click();
link.remove();
// 先触发下载,再延迟释放 URL,避免部分浏览器过早失效。
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}
downloadFile("/api/report/export", {
method: "GET",
fallbackName: "report.xlsx"
}).catch(error => {
console.error(error);
alert(error.message || "下载失败");
});
如果接口使用 POST,可以传入请求体:
downloadFile("/api/report/export", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
from: "2025-01-01",
to: "2025-01-31"
}),
fallbackName: "report.xlsx"
}).catch(console.error);
注意,GET 请求通常不应发送请求体;是否使用 GET 或 POST,应由接口定义决定。若使用 JSON 请求体,服务端也必须支持对应的请求方法和内容类型。
三、从 Content-Disposition 读取文件名
常见响应头大致如下:
Content-Disposition: attachment; filename="report.xlsx"
Content-Disposition: attachment; filename*=UTF-8''%E6%8A%A5%E8%A1%A8.xlsx
attachment 表示响应内容适合作为附件处理。filename 是较早的文件名参数;filename* 使用扩展参数格式,能够表达 UTF-8 编码的非 ASCII 文件名,常用于中文名称。服务端可能同时发送两者,以兼容不同客户端;前端一般优先选择有效的 filename*,再退回到 filename。
下面的解析函数支持常见的 filename* 和带引号的 filename 格式。它不是完整的通用 HTTP 头语法解析器;对于非 UTF-8 编码、复杂转义或格式不规范的响应,应由服务端统一输出规范格式。
function getFilenameFromDisposition(header) {
if (!header) return null;
// RFC 5987 / 6266 常见形式:
// filename*=UTF-8''%E6%8A%A5%E8%A1%A8.xlsx
// filename*=UTF-8'en'%E6%8A%A5%E8%A1%A8.xlsx
const extendedMatch = header.match(
/filename\*\s*=\s*([^;]+)/i
);
if (extendedMatch) {
let value = extendedMatch[1].trim();
// 去掉可能存在的引号。
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
const parts = value.match(/^([^']*)'[^']*'(.*)$/);
if (parts) {
const charset = parts[1].toLowerCase();
const encodedName = parts[2];
if (charset === "utf-8") {
try {
return decodeURIComponent(encodedName);
} catch {
// 编码格式不合法时继续尝试普通 filename。
}
}
}
}
// 普通 filename,兼容带引号及引号内的反斜杠转义。
const filenameMatch = header.match(
/filename\s*=\s*("((?:\\.|[^"])*)"|[^;]*)/i
);
if (!filenameMatch) return null;
let filename = filenameMatch[2] !== undefined
? filenameMatch[2].replace(/\\(.)/g, "$1")
: filenameMatch[1].trim();
// 对普通 filename 不应一概调用 decodeURIComponent:
// 它可能包含字面上的百分号,未必是百分号编码。
return filename || null;
}
上面的实现对 filename* 只接受 UTF-8。HTTP 扩展参数可以标注其他字符集,但现代 Web 服务通常应统一使用 UTF-8。若服务端返回其他字符集,不能把它当成 UTF-8 强行解码;应优先调整服务端响应头。
四、文件名必须按不可信输入处理
响应头来自服务端,并不意味着其中的文件名可以直接信任。文件名可能含有路径分隔符、控制字符或操作系统不适用的字符。浏览器会对 download 属性做一定处理,但不应把浏览器的处理当成完整的安全校验,也不应把服务器提供的文件名用于服务端文件路径。
在前端下载场景中,可以先把路径分隔符替换为下划线,再移除控制字符和常见不适用字符,并处理空名称、特殊名称及尾部空格或句点:
function getSafeFilename(value) {
let name = String(value || "download");
// 去掉可能伪装成路径的目录部分。
name = name.replace(/\\/g, "/").split("/").pop();
// 移除控制字符,替换常见文件系统不适用字符。
name = name
.replace(/[\u0000-\u001F\u007F]/g, "")
.replace(/[<>:"|?*]/g, "_")
.trim()
.replace(/[. ]+$/g, "");
if (!name || name === "." || name === "..") {
name = "download";
}
return name;
}
这段处理主要用于浏览器端展示和下载命名,不是防止恶意文件内容的安全机制,也不是所有操作系统文件名规则的完整实现。不要因为扩展名看起来安全,就信任文件内容;如果应用需要判断文件类型或限制上传、存储行为,应在服务端按实际数据和业务规则校验。
五、跨域接口需要服务端配合暴露响应头
同源接口中,脚本通常可以读取 Content-Disposition。跨域请求则受浏览器 CORS 规则约束:即使响应中实际包含该头,前端 JavaScript 也可能读不到它。服务端需要把它加入 Access-Control-Expose-Headers:
Access-Control-Expose-Headers: Content-Disposition
若同时需要带跨域 Cookie,服务端还必须按凭据请求配置 CORS,明确允许请求来源,并允许凭据;不能在允许凭据时把允许来源设为通配符 *。前端对应设置为:
fetch("https://files.example.com/api/export", {
credentials: "include"
});
credentials 常见取值是 same-origin(默认值)和 include。前者在同源请求中发送凭据,后者允许跨域请求携带凭据,但能否成功仍取决于服务端 CORS 配置及浏览器的 Cookie 策略。带凭据跨站请求还可能受到 Cookie 的 SameSite 属性和浏览器隐私限制影响。
跨域环境下,若读取到的 Content-Disposition 为 null,先检查浏览器开发者工具中的响应头和 CORS 配置。仅在前端增加代码,无法绕过浏览器的 CORS 限制。
六、什么时候不必使用 Blob 方式
如果下载接口不需要前端自定义请求头或请求体,也不需要在 JavaScript 中检查响应,可以直接使用链接,让浏览器处理导航和下载:
<a href="/api/files/123">下载文件</a>
也可以由服务器在响应头中设置 Content-Disposition。这种方式通常不需要把整个文件读入 JavaScript 内存,更适合大文件。但它不方便添加自定义授权请求头,也不便于在前端读取响应内容。链接的 download 属性对跨源 URL 的行为有限制,且浏览器可能根据响应头、文件类型和安全策略决定实际行为;需要可靠的文件名时,优先正确设置服务端响应头。
使用 Fetch 后调用 response.blob(),浏览器需要把响应体作为 Blob 供脚本使用。对于体积很大的文件,这可能增加内存占用,具体表现与浏览器实现有关。若文件很大,或需要可靠的断点续传、进度控制和流式写入,应考虑由浏览器直接导航到下载地址,或采用支持流式保存的专用方案,而不是默认把完整响应读成 Blob。
七、Axios 写法
使用 Axios 时,浏览器端请求需要指定 responseType: "blob",否则响应可能按文本或 JSON 处理。文件名仍从响应头读取;Axios 通常会把响应头键名转换为小写:
async function downloadWithAxios(axios, url) {
const response = await axios.get(url, {
responseType: "blob",
withCredentials: false
});
const disposition =
response.headers["content-disposition"] || null;
const filename = getSafeFilename(
getFilenameFromDisposition(disposition) || "download"
);
const objectUrl = URL.createObjectURL(response.data);
const link = document.createElement("a");
link.href = objectUrl;
link.download = filename;
link.style.display = "none";
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}
如果跨域请求需要 Cookie,可以将 withCredentials 设为 true,同时配置服务端 CORS。示例中的 false 仅表示不主动要求跨域凭据;同源认证、拦截器或其他 Axios 配置仍可能影响实际请求。
Axios 的 HTTP 错误通常会进入拒绝分支,但如果服务端用成功状态码返回了 JSON 错误内容,且响应类型指定为 Blob,错误信息也可能成为一个 Blob。遇到这种接口约定时,需要检查响应类型或读取 Blob 文本后再判断,不能仅凭“拿到了 Blob”就认定下载成功。
八、常见错误与排查方法
1. 下载下来的是 JSON 或错误页面
检查响应状态码、响应体和服务端日志。Fetch 不会因为 HTTP 404 或 500 自动抛出异常,因此必须检查 response.ok。还要确认接口在成功时返回的是文件内容,而不是一个包含文件地址的 JSON;如果返回 JSON,应先按接口约定解析并请求实际文件。
2. 文件名始终是默认值
检查响应头是否确实包含 Content-Disposition。跨域时还要确认响应中有 Access-Control-Expose-Headers: Content-Disposition。此外,服务端可能返回的是不同大小写的头名称;使用 Fetch 的 headers.get("Content-Disposition") 不需要区分大小写,Axios 则通常从小写键名读取。
3. 中文文件名乱码
检查服务端是否提供了格式正确的 UTF-8 filename* 参数,以及其中的百分号编码是否合法。不要对普通 filename 无条件调用 decodeURIComponent,否则合法的百分号字符可能引发解码异常或被错误解释。
4. 点击后没有弹出下载
确认代码运行在用户操作触发的流程中。浏览器可能限制非用户触发的下载;若先等待很久再创建并点击链接,也可能不符合浏览器对用户激活的要求。检查 JavaScript 异常、对象 URL 是否有效,以及页面是否在点击后立刻跳转或卸载。
5. 文件内容为空或无法打开
确认服务端实际返回了文件字节,而不是空响应、登录跳转页面或错误内容。还要检查请求方法、参数、Cookie 和授权头是否符合接口要求。对于返回的压缩包、表格等文件,扩展名与实际内容不匹配也可能导致系统或应用无法识别。
6. 文件名带有奇怪字符或被截断
检查服务端的响应头格式和前端解析逻辑。分号、引号、反斜杠等字符会影响简单的正则解析;如果服务端生成的头字段格式复杂,优先修正服务端输出,保持 UTF-8 filename* 与兼容用的普通 filename 规范一致。不要试图用一个未经验证的短正则覆盖所有可能的 HTTP 头格式。
九、推荐实践
- 优先让服务端使用
Content-Disposition: attachment,并提供规范的 UTF-8filename*;必要时再提供兼容用的filename。 - Fetch 请求后检查
response.ok,不要把错误响应直接保存成文件。 - 仅在确实需要前端控制请求时使用 Blob 下载;大文件应评估内存开销。
- 把响应头文件名视为不可信输入,使用安全处理后的名称,并提供合理默认值。
- 跨域时分别检查 CORS 是否允许请求,以及脚本是否被允许读取
Content-Disposition;它们是两个独立问题。 - 下载完成后调用
URL.revokeObjectURL,及时释放对象 URL 关联的资源。
归纳起来,保留服务端文件名的核心并不复杂:读取 Content-Disposition,优先解析 filename*,将响应体转换为 Blob,再通过临时链接触发下载。真正容易出问题的地方是跨域响应头暴露、错误响应判断、中文编码和不可信文件名处理。把这些边界纳入实现,下载逻辑才更可靠。




