简单说,H5 想直接下载服务器上的文件,最省事的做法是让后端给下载接口配上 Content-Disposition: attachment 响应头,然后前端用 window.location.href 或一个隐藏的 <a> 标签把请求发出去,遇到需要鉴权或跨域的场景,才需要改用 fetch + Blob 这条路径。
先分清:浏览器到底把文件“打开”了还是“存下来”了
很多人第一次做下载功能,会卡在一个很基础的地方:点了按钮,PDF 在新标签页里显示出来了,但硬盘上什么都没有,这不是代码错了,而是浏览器在看响应头。
决定行为的是 Content-Type 和 Content-Disposition
服务器返回一个文件时,浏览器会先看这两个头:
Content-Type: application/pdf或image/png这类能被浏览器渲染的类型,默认行为是“打开”。- 带上
Content-Disposition: attachment; filename="report.pdf"之后,浏览器才会走下载通道,把文件存到本地。 - 如果只有
Content-Type: application/octet-stream,多数浏览器也会当作二进制流处理,触发下载。
据 MDN 文档的说明,Content-Disposition 的 attachment 值就是用来声明“这是一份该被保存的附件”,所以问题的本质不是前端怎么写,而是服务端有没有说清楚“这东西是给你存的”。
前端能做的三件事
前端在这个链路里只负责三件事:发起请求、带上身份凭证、接住返回的数据,至于文件去哪儿、叫什么名字,决定权基本在服务端手里。
h5前端实现文件下载的几种方式对比
同样一个下载需求,选哪条路取决于三个条件:文件是不是同源、要不要带 Token、文件有多大。
a 标签的 download 属性
最简单的写法,适合静态资源或者同源接口:
<a href="/api/file/download?id=123" download="报告.pdf">下载</a>
download 属性有两个作用:告诉浏览器走下载流程,以及指定保存时的默认文件名,但它的生效范围有硬限制,后面的小节会细说。
window.open 或 location.href
window.location.href = '/api/file/download?id=123';
适合服务端已经配好 Content-Disposition 的接口,优点是不用管跨域,Cookie 会自动带上;缺点是拿不到下载进度,也没法在失败时给用户提示,如果后端返回的是 JSON 错误信息,用户会看到一个莫名其妙的新页面。
fetch + Blob + createObjectURL
const res = await fetch('/api/file/download?id=123', {
headers: { Authorization: `Bearer ${token}` }
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '报告.pdf';
a.click();
URL.revokeObjectURL(url);
这种方式能自定义请求头、能中途取消、能拿到 loading 状态,是目前业务系统里用得最多的一种,代价是整个文件会先进内存,几百兆以上的文件要慎用。
| 方式 | 能否带自定义请求头 | 大文件友好 | 跨域限制 | 进度反馈 |
|---|---|---|---|---|
| a 标签 download | 否 | 是 | 受同源策略约束 | 无 |
| location.href | 否 | 是 | 无(Cookie 自动携带) | 无 |
| fetch + Blob | 是 | 否(占内存) | 需服务端开 CORS | 可实现 |
| 后端直链 + 跳转 | 视场景 | 是 | 无 | 需轮询或听事件 |
一个比较通用的判断逻辑:能拿到稳定直链就直接跳转,需要鉴权就走 fetch + Blob,文件特别大就让后端生成一次性签名链接。
h5 a标签download属性下载不了怎么办
这是搜索量很高的一个问题,原因基本集中在两类。
跨域资源,download 被忽略
按 HTML 规范,download 属性只对同源 URL 和 blob:、data: 这类本地 URL 生效,一旦 href 指向另一个域名,浏览器会直接忽略这个属性,退回到默认行为能显示就显示,不能显示就下载。
解决办法是把文件放到同域下,或者让后端加一个中转接口,前端请求自己的域名,由服务端去拉取远端文件再吐回来。
文件名被服务端覆盖或忽略
当服务端返回了 Content-Disposition: attachment; filename="xxx",浏览器会优先用服务端给的名字,download 属性里写的值就不起作用了,如果文件名有中文,还要确认服务端做了 filename=UTF-8'' 编码,否则可能出现乱码或者下载失败。
还有一个容易忽略的点:部分浏览器对 download 属性的支持早于对 blob 下载的支持,在低版本 iOS Safari 上,a.click() 触发下载可能没反应,需要用户手动长按,这种情况通常的做法是提示用户在系统浏览器中打开页面。
h5页面下载服务器文件跨域怎么解决
跨域是下载功能里最消耗时间的一块,因为它往往不是前端单方面能解决的。
服务端配置 CORS
需要三个头配合:
Access-Control-Allow-Origin: https://your-site.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Content-Disposition
第三个头特别关键。Content-Disposition 默认不在 fetch 能读取的白名单里,不暴露的话,前端就拿不到服务端给的文件名,只能自己硬编码。
Nginx 反向代理
如果后端不方便改,可以让前端请求同域的 /proxy/file/xxx,Nginx 转发到真实文件服务器:
location /proxy/file/ {
proxy_pass https://file.origin.com/;
add_header Content-Disposition 'attachment';
}
这样对浏览器来说就是同源请求,跨域问题自然消失,Cookie 和鉴权也能顺带处理掉。
后端出一个下载中转接口
最稳但最耗服务器资源,客户端请求自己的接口,接口内部去对象存储拉文件,边拉边写入响应流,适合文件体积不大、但对权限控制要求高的内部系统。
业内专家指出,跨域下载的复杂度八成来自“鉴权方式”和“文件来源”这两个变量没提前对齐,前端埋头写代码之前,先和后端确认接口形态能省掉大量返工。
h5下载文件安卓和iOS有什么区别
这个差异主要来自浏览器内核和系统沙盒,跟你的代码关系不大。
安卓端
- 主流浏览器和厂商内置浏览器对
a.download支持比较完整。 - 微信内置浏览器用的是 X5/XWeb 内核,可能不会直接下载,而是跳到一个“用其他应用打开”的提示页。
- 下载后的文件通常落在
Download目录,用户可以在文件管理器里找到。
iOS 端
- iOS 13 之后 Safari 才正式支持
download属性,更早的版本需要长按或借助分享。 - iOS 的文件系统是沙盒化的,下载完成后一般进入“文件”App 的下载目录,或者弹出一个分享面板让你选择保存位置。
- 微信内打开 H5 下载,常见表现是顶部出现“在浏览器中打开”,用户点了之后才会走真正的下载流程。
行业共识认为,移动端下载体验的差异更多取决于“用户从哪个 App 点进来”,而不是前端代码写得好不好,所以做移动端下载时,判断 navigator.userAgent 里的 MicroMessenger 标识、给出引导提示,是一个必要的兜底动作。
大文件和断点续传的实际做法
文件超过一百兆,fetch + Blob 就不太合适了,内存会顶不住,这时有两个方向。
后端给一次性签名直链
前端调接口拿到一个带过期时间的 URL,然后直接 location.href 跳过去,下载过程完全交给浏览器,不占 JS 内存,还能享受浏览器的断点续传能力。
范围请求
服务端支持 Range 头之后,客户端可以分段请求:
fetch(url, { headers: { Range: 'bytes=0-1048575' } });
服务端返回 206 Partial Content 和 Content-Range,前端把分片拼起来再写入文件系统,这条路径适合必须自己做进度条和暂停续传的场景,但实现成本明显更高,需要 Service Worker 或者流式写入 API 配合。
近年来,越来越多的团队选择把下载交给 CDN 和对象存储处理,前端只负责换取临时凭证,这是成本最低的做法。
落地前值得过一遍的检查清单
- 服务端是否返回了
Content-Disposition: attachment,中文文件名是否做了 UTF-8 编码。 - 接口是否同源;跨域时是否暴露了
Content-Disposition响应头。 - 下载请求是否带上了鉴权信息,Token 放在 header 还是 Cookie。
- 文件大小预估是多少,是否超过 200MB,是否需要走直链方案。
- 微信内置浏览器是否有降级提示。
- 下载失败时是否有可读的错误反馈,而不是只留一个白屏。
h5下载文件常见问题解答
h5怎么直接下载服务器文件而不用后端额外开发?
如果文件是公开可访问的静态资源,直接把 URL 放进 <a> 标签的 href 就行,前提是服务端返回的文件类型不会触发浏览器预览,不过要真正控制文件名和下载行为,还是绕不开服务端的响应头配置,纯前端解决不了。
为什么 fetch 请求下载能成功,但文件名是乱的?
大概率是 Content-Disposition 没有通过 Access-Control-Expose-Headers 暴露出来,前端读不到,只能用默认名,另一种情况是服务端只写了 filename= 而没写 filename=,中文名在部分浏览器上会编码错误。
微信里点下载没反应,一定要跳浏览器吗?
不是一定要跳,但跳转是最省事的方案,微信内置浏览器对 blob 下载和 download 属性的支持不一致,直接触发往往静默失败,更稳妥的做法是检测到微信环境后,展示一个带箭头引导的提示层,让用户手动选择“在浏览器中打开”。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/783359.html


