零、前言
在真实项目中,文件下载是常见的需求,但在前后端分离开发作为主流的今天,有的前端同学可能并不清楚如何选择合适的实现方法。
本文将分别讨论基于服务端或客户端的方式去实现需求。
后续代码展示,将使用原生nodeJS启动服务端,及使用原生前端技术开发前端页面,讨论需求实现的方法。
一、服务端方式
1.1 客户端配置
使用服务器配置的方式,前端只需要一个最简朴的a标签,其href属性值就是资源下载的路径。
<a href="http://127.0.0.1:3000/pdf">点击下载</a>
1.2 服务端配置
服务端需要设置的是Content-Disposition
响应头,设置其值为附件,并且可以使用filename指定下载的默认文件名。
const http = require('http')
const fs = require('fs')
const app = http.createServer((request, response) => {
const urlInfo = new URL(request.url, 'http://127.0.0.1')
// 获取请求路径
const { pathname } = urlInfo
// 获取请求类型
const { method } = request
// 请求路径为'/pdf',且请求类型为GET请求时才进行处理
if (pathname === '/pdf' && method === 'GET') {
// 读取需要发送的文件数据
const file = fs.readFileSync('./haha.pdf')
// 关键步骤,设置响应头以表明此资源应进行下载,文件名不必与真实文件名相同,但后缀需要正确
response.setHeader('Content-Disposition', 'attachment; filename=xixi.pdf')
// 表明响应资源的类型为pdf。实际上不设置也不会影响下载效果。
response.setHeader('Content-Type', 'application/pdf')
// 发送数据给客户端
response.end(file)
}
})
app.listen('3000', () => {
console.log('listen 3000...');
})
1.3 总结
通过如上设置后,用户在页面点击a标签后,就会自动触发下载行为,且不会受到跨域的影响
,因为点击a标签发送请求的行为并不是ajax技术,浏览器不会对此进行限制。
1.4 补充
之前我认为既然服务端已经正确设置了Content-Disposition响应头,点击a标签是发送get请求,那么我使用ajax技术来发送get请求,是不是也能做到相同效果,能直接触发下载?
不知道有没有小伙伴有过与我相同的疑惑,针对这个疑惑,我实践了一下,发现是不行的。使用ajax发送请求后(注意跨域问题),不会触发下载行为,只会接收到来自服务端的文件数据,并且可以赋值给变量,对数据进行进一步的处理(下文将会讨论使用此方式进行下载)。
二、客户端方式
2.1 服务端配置
服务端只需正确处理请求类型和请求路径,直接将文件数据返回给客户端
const http = require('http')
const fs = require('fs')
const app = http.createServer((request, response) => {
const urlInfo = new URL(request.url, 'http://127.0.0.1')
// 获取请求路径
const { pathname } = urlInfo
// 获取请求类型
const { method } = request
// 请求路径为'/pdf',且请求类型为GET请求时才进行处理
if (pathname === '/pdf' && method === 'GET') {
// 读取需要发送的文件数据
const file = fs.readFileSync('./haha.pdf')
// 发送数据给客户端
response.end(file)
}
})
app.listen('3000', () => {
console.log('listen 3000...');
})
2.2 客户端配置
客户端仍然是需要使用a标签,并且需要添加一个重要的属性download
。
download属性告诉浏览器:用户点击a标签之后需要触发下载行为,其值为文件的默认文件名(不需要与服务器中的文件名字一致)
<a href="http://127.0.0.1:3000/pdf" download="enen.pdf">点击下载</a>
值得注意的是,设置download属性之后,href的值需要是同源url,才能正确触发下载行为。
如不是同源url,点击a标签之后,将不会下载资源,而是在网页中展示出来(但遇到浏览器不能识别的文件时,是会触发下载行为的。这种情况在2.4中会展开讨论)。
2.3 总结
使用客户端的方式,虽然不需与后端同学battle就可实现,但href的值需要是同源url
,这在某些项目中可能并不适用,但如果项目部署时使用了nginx代理,那么这不失为一个可行的方案。
2.4 补充
有一个细节:当使用a标签请求回来的资源,浏览器无法识别时(如word文档,excel文档等),是会自动触发下载行为的(即使服务端没有设置Content-Disposition或a标签没有设置download属性)。
即,当客户端不设置download属性,且服务端不设置Content-Disposition
(而返回了文件数据)时,用户点击a标签时:
- 浏览器能认识服务器返回的资源数据(如pdf文件,PNG图片等),则会直接展示在网页中。
- 浏览器不认识服务器返回的资源数据(如word文档),则会自动触发下载行为。
三、总结
如上两种方式都可以实现文件下载,它们都有各自需要注意的配置点。
但个人认为服务端配置Content-Disposition响应头的方式似乎是更加方便,通用的方式。
四、拓展:当下载资源时需要验证用户身份
这里拓展一个场景:在某个网页中,前后端使用token进行会话管理,客户端向服务端发送请求时,需要在请求头中携带token,以便服务端鉴别其身份,如果用户有权限,才会返回正确的数据。
其实在用户点击a标签,发送请求时,浏览器是会自动在请求报文中携带同源cookie的。但上述场景中,此网站使用的是token而不是cookie,这就无法被自动携带,且也不能以编码的方式,使点击a标签时携带token。
4.1 解决方法
遇到上述情况,我们可以使用ajax技术,并携带token,获取文件数据后,在前端生成一个临时的url和临时的a标签,并使用JS模拟点击a标签,以实现下载。
// 获取“下载”按钮
const oBtn = document.getElementById('download-btn')
oBtn.addEventListener('click', handleDownload)
async function handleDownload() {
// 使用ajax技术,获取文件数据。(使用XHR同理)
const res = await fetch('http://127.0.0.1:3000/pdf', {
header: {
// 手动携带手动携带token
authorization: `Bearer ${localStorage.getItem('token')}`
}
})
// 将数据处理为Blob形式。
const blob = await res.blob()
// 在前端生成临时url,代表了文件数据
const url = URL.createObjectURL(blob)
// 生成一个临时a标签
const a = document.createElement('a')
// 设置其download属性,以便触发其下载行为
a.download = 'haha.pdf'
a.href = url
// 触发下载行为
a.click();
}
上述方法使用ajax技术,发送GET请求并携带token,获取了文件数据,并使用res变量保存数据,再作后续操作。这在处理一般需求时,的确是可行且简单的方式。只需要服务端设置CORS允许跨域请求即可。
4.2 优化:大文件下载
在这里补充一点:当直接用户直接点击a标签,触发下载行为后,服务器会源源不断地发送数据给客户端,但是这些数据将不再被浏览器所接收,而是直接被写入到计算机的硬盘中(即直接保存到选中的文件夹中)。
我们接下来再思考一种情况,当需要下载的文件较大时,使用上述方法实现下载,是否就存在以下瑕疵了:
- 文件数据需要先完全下载到浏览器后,才能模拟点击a标签触发下载行为,可能增加用户等待时间,影响用户体验。
- 文件数据需要下载并使用变量接收,换而言之,这些数据需要先保存在计算机内存中,可能会增加浏览器或计算机负担。
那么要对这种情况进行优化,现在我们将拥有两个问题:
- 需要下载的文件较大,不能直接保存在计算机内存中。
- 发送请求时需要携带token(用户身份证明)。
其实对于上述这两个问题,棘手的地方在于:当前网站使用token进行会话管理。如果使用的是cookie,那么用户直接点击真实a标签就可以自动携带,进行下载了。
那么我们是否可以考虑使用ajax技术发送请求,并手动携带token,服务端校验了用户的身份之后,为其生成一个cookie(过期时间较短),并返回下载文件的url给客户端。
当客户端收到响应报文之后,cookie就会自动设置,然后再生成a标签,设置其href属性和download属性,然后模拟点击,触发下载(此时cookie就会被自动携带,服务端也就能鉴别用户信息了。)
const oBtn = document.getElementById('download-btn')
oBtn.addEventListener('click', handleDownload)
async function handleDownload() {
// 使用ajax技术,携带token,收到响应报文后,cookie将被自动设置
const res = await fetch('http://127.0.0.1:3000/pdf', {
header: {
authorization: `Bearer ${localStorage.getItem('token')}`
}
})
// 服务端返回的数据是真实下载文件的url,而不是文件数据
const data = await res.json()
const a = document.createElement('a')
a.download = 'haha.pdf'
// 设置服务端返回的文件下载地址
a.href = data.url
// 点击a标签,cookie将会被自动携带
a.click();
}
注意:使用这种方法,文件下载的url需要是同源url(或部署项目时使用nginx进行代理)。