我当初面试一家公司的时候,面试官问我:“前端下载一个文件,你会怎么做?”我嘴里说着a标签加download属性,心里其实已经闪过七八个念头——这玩意儿真这么简单?下载这个动作,放在浏览器里,表面的“点击就下”背后藏着太多分支:文件是后端下发的流还是静态资源?要带token吗?文件名是中文会不会乱码?下载到一半断了怎么办?这些问题不搞清楚,实际做项目的时候就等着一个个踩坑吧。
这篇就把前端下载这件事掰开揉碎讲。从浏览器最底层的下载触发机制,到Blob二进制下载、文件名解析、大文件断点和鉴权下载,每个环节我都会给你可以直接抄走的方案和代码。内容面向从初级到中高级的前端开发者,尤其适合准备面试或者正在做文件模块的兄弟们。
1. 浏览器下载的本质:一条链接引发的血案
很多前端干了两年还在用window.location.href = url下载文件,偶尔能用,但断断续续出些怪问题,还找不着原因。要搞清楚前端下载,第一步就是理解浏览器的下载行为是怎么被触发的。
1.1 下载的本质不是“代码在下载”,而是“浏览器在导航”
浏览器天生就有一项能力:识别服务器返回的响应头。只要HTTP响应头里的Content-Disposition带了attachment,浏览器收到内容后就不会去渲染页面,而是把内容当成一个文件丢进下载管理器。这个行为跟你怎么发请求没关系——你直接地址栏输入URL也好,a标签href指向也好,window.open也好,只要响应头是attachment,它就下载。
这就是最最初的下载原理:下载并不是前端“主动拉”一个文件,而是前端引导浏览器去访问一个URL,浏览器根据服务器的响应头决定是渲染还是下载。
所以一个最直接的前端下载方法就是:
html复制<a href="https://example.com/files/report.pdf" download>下载报告</a>
你可能会问,这里我加了download属性,是不是就靠它实现的?其实并不是。download属性只是告诉浏览器“这个链接我希望是下载而不是跳转”,但它有两个前提:第一,必须是同源URL,因为download属性对跨域资源是无效的(Chrome会忽略它,直接打开预览);第二,服务器响应头也得配合,如果服务器没有返回attachment,即便加了download有的场景也会变成预览。
1.2 三种方式的本质区别
前端触发下载的方式汇总起来其实就三大类,很多文章讲得云里雾里,我这里列个表,一下就清楚了:
| 触发方式 | 是否受跨域限制 | 能否携带请求头 | 文件名控制权 | 适用场景 |
|---|---|---|---|---|
| a标签 + download | 跨域时download属性失效 | 不能 | 前端可指定(同源才生效) | 静态资源、同源文件下载 |
| window.open / location.href | 只要URL可达即可 | 不能 | 依赖Content-Disposition | 后端直接返回文件流,且无需鉴权 |
| fetch/xhr 拿流再Blob下载 | 受CORS限制,但能携带token | 能 | 完全由前端控制 | 需鉴权、动态生成文件、需要处理大文件 |
这张表是理解前端下载整个体系的骨架。第一二种本质就是让浏览器做一次导航请求,第三种是先用代码拿到文件数据,再在前端拼出一个文件让浏览器下载。别看写法差几个字母,背后完全是两种思路。
1.3 为什么有时候点击下载没反应
这个问题高频出现在工作中。结合上面的原理,基本可以按这几条去排查:
浏览器拦截了window.open——因为window.open如果不在用户点击的同步事件栈里触发,会被弹窗拦截。有人代码里先await了一个接口,再调window.open,就被拦了。解决方法是用a标签临时创建或者先开空窗再赋值location。
服务器返回的是JSON错误信息而不是文件流——比如token过期了,后端返回401加一段JSON。前端还在傻傻接收,接收完发现不是文件,自然下不了。
后端响应头没设置Content-Type导致浏览器直接去渲染文件内容了——比如返回一个JSON格式的数据却标了text/html,浏览器把内容当页面展示。这类问题看Network面板里响应体内容比看任何报错都直观。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二进制流下载:为什么你下载的文件总是一堆乱码
前端的进阶下载方式——先请求拿到数据流,然后在浏览器端“造”一个文件出来,就是常说的Blob下载。
2.1 后端下发文件流的两种形态
后端接口返回的文件数据,因为网关或历史原因,常见有两种形态。
第一种,接口直接返回二进制流。你fetch拿到的是一个Response对象,要调res.blob()拿到Blob实体。
第二种,接口被包了一层JSON结构,文件内容被转成了Base64编码字符串或二进制数组。出现这种情况,多半是后端没用流式输出,而是把文件读进内存再编码塞进JSON里。前端拿到这种数据,需要先转成Uint8Array或直接处理Base64,再new Blob。
如果后端返回的是这种带外层结构的JSON,前端就得二次处理。以常见的Base64字符串为例:
javascript复制// 后端JSON里长这样:{ code: 0, data: "JVBERi0xLjQK..." }
async function downl
