Web 应用开发之文件下载


在 web 应用开发中,经常会有用户需要下载文件的地方,通常会有这几种方式:

  • 前端超链接下载后端的静态/实体文件
  • ajax 方式下载超链接后端静态/实体文件
  • ajax 方式获取后端返回的响应包中的文件
  • 浏览器接收后端返回的相应包,直接下载其中的文件

除此之外,还有呈现在页面中,而不是直接下载的文件。例如视频流、图片验证码等。

超链接下载文件

使用超链接下载文件是最简单最常见的文件下载方式,文件可以是静态文件,也可以是动态生成的实体文件。

用法主要是在前端,超链接 url 指向文件所在地址,后端做好 url 路由即可。

使用这种方法,如果想下载的文件是浏览器能够直接打开的,例如 jpg 等图片,则会在浏览器中显示而不是打开下载对话框。

JS下载超链接文件(前端)

需要先获取文件所在的超链接 url 地址,然后可以创建个临时的超链接,并触发此临时超链接的 click 事件

// 创建临时超链接
let link = document.createElement('a');
// 给临时超链接添加 download 属性,设置保存文件名。
link.download = 'test.jpg';
// 给临时超链接添加 url
link.href = url;
// 触发 click 事件
link.click();

这种方法其实是使用了 H5 的新特性。如果不设置 download 属性,就会和点击超链接一样,有可能浏览器直接打开支持的文件进行查看。另外这种方法会直接下载文件到浏览器设置的默认保存目录中。

需注意的是,这种方法只适合同源的图片。如果跨域,则需要使用 canvas

获取 response 中的文件(后端)

使用超链接方式下载的文件是实体文件,即 url 能够找到文件本身。如果文件是临时创建的,保存在缓冲区里的二进制文件,使用此种方式。后端以使用 python + django 为例,实际主要是对相应包进行操作,何种语言、工具都可以。

# file 是文件对象,使用 file.getvalue() 能够获取其二进制字节
# 先创建响应对象
resp = HttpResponse()
# 将二进制字节写入响应对象的 content 中,也就是写入响应体(response 的 body)
# 也可以在创建响应包时写入 resp = HttpResponse(content=file.getvalue())
resp.content = file.getvalue()		# 或使用 resp.write(file.getvalue())
# 设置响应头的 content-type 为文件流
# 如果是图片或其他格式,则可以设置 content-type 为相应格式
# 例如 'image/png' 'application/pdf' 'image/jpeg' 'application/x-zip-compressed'
resp['Content-Type'] = 'application/octet-stream'
# 因为文件是从内存中的文件对象获取,没有文件名,所以在响应头中设置文件名
# 如果只是在浏览器缓存中使用,而不是下载,则可以不设置文件名
# 例如是验证码图片类随机生成的,通过 img 标签的 src 属性访问获取的
resp['Content-Disposition'] = 'attachment;filename=' + filename
# 响应包设置好了,将此响应包发给前端即可

前端收到封装好的响应包,就会开始下载文件了。

需要注意的是,文件名如果使用了中文,则需要对其进行 urlencode 编码

from urllib.parse import quote

resp['Content-Disposition'] = "attachment;filename*=UTF-8''" + quote(filename)

注:UTF-8 后跟了两个单引号,如果不加会将 UTF-8 认为是文件名的一部分。另如果写为 "attachment;filename=" + quote(filename) ,不加编码部分浏览器会出现编码错误。

后端传送二进制文件对象

后端传送的文件对象,即上例中的 file 对象,可以是打开的文件对象,也可以是缓存的文件对象。打开实体文件使用 open() 方法,缓存文件对象这样使用:

# 例如创建了 Excel 的 Workbook 对象 wb ,并已经完成写入数据
# 先创建缓存对象
buf = io.BytesIO()
# 将 wb 保存至缓存对象
wb.save(buf)
# 至此 buf 就可以当实体文件对象使用了,例如使用 file.getvalue() 能够获取其二进制字节

这样也可以使用:

buf = io.BytesIO(wb)

主要看 wb 继承的基础父类是否是 io.BytesIO() 方法支持的参数。如果不支持还是需要使用其自身的保存方法保存至缓存文件对象中。

另注意:使用完成文件缓存对象后,记得释放资源

buf.close()

原生ajax 获取 response 中的文件(前端)

response 中的文件是二进制形式,ajax 需要获取其内容创建 Blob 对象。另 jQuery 的 ajax 貌似无法正确处理二进制文件,会报错:No conversion from text to arraybuffer 所以这里使用原生 ajax

let xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
// 如果需要附加数据,则需要增加数据类型
// xhr.setRequestHeader('Content-Type', 'application/json');
// 对象序列化作为数据发送
// let data = JSON.stringify({...});
xhr.responseType = 'arraybuffer';
// 回调函数
function success(resp, disposition){
    
    
	let blob = new Blob([resp], {
    
    type: 'application/octet-stream'});	// 获取到 blob 数据对象
	// 如果需要直接下载,则可以创建 a 标签
	let link = document.createElement('a');
	// 使用 blob 对象创建 url
	link.href = window.URL.createObjectURL(blob);
	// 根据响应头设置下载的文件名
	// disponsition 是发送时请求头中的 'Content-Disposition',如果进行了 urlencode 编码则需要注意解码
	// decodeURI(disposition.split("UTF-8''")[1]);
	link.download = disposition.split('=')[1];
	// 触发 click 事件,下载文件
	link.click();
	// 释放临时创建的 url
	window.URL.revokeObjectURL(link.href);
}
// xhr 的监听函数,当请求发送完成且接收到 200 的响应后,使用回调函数 success 来进行处理
xhr.onreadystatechange = function(){
    
    
	if (xhr.readyState === 4 && xhr.status === 200){
    
    
		return success(xhr.response, xhr.getResponseHeader('Content-Disposition));
	} else {
    
    
		return fail(xhr.status);	// 在失败回调函数中需判断 xhr.status,表示接受到响应。否则请求发送阶段也会执行回调函数
	}
}
xhr.send();
// 如附带数据,则使用 xhr.send(data);

此种方式是将响应文件交给 ajax 处理,使用到了 blob 对象。这样做的坏处有:

  • 不同浏览器对 blob 的支持不同,限制不同。
  • 必须等到下载完文件之后才能构建 blob 对象,再转化成文件。如果下载文件比较大,用户会发现点击了下载按钮之后看不到下载提示。

而使用 ajax 进行下载可以进行一些预处理:

  • 权限校验,可以根据用户权限限制是否下载,可否高速下载等。
// 不用 blob 来构建 URL,而是通过后端服务器来计算出用户的下载链接,然后再动态创建 <a> 标签的方式来实现下载
fetch('http://somehost/check-permission', options).then(res => {
    
    
    if (res.code === 0) {
    
    
        var a = document.createElement('a');
        var url = res.data.url;
        var filename = 'myfile.zip';
        a.href = url;
        a.download = filename;
        a.click();
    } else {
    
    
        alert('You have no permission to download the file!');
    }
});
  • 动态文件,例如导出数据到 Excel,此时因为对应的 URL 并不存在,所以需要发送请求到服务器来动态生成数据文件。

另,使用 js 获取响应头

    function get_response(){
    
    
        let req = new XMLHttpRequest();
        req.open('GET',document.location,false);
        req.send(null);
        let header = req.getAllResponseHeaders().toLowerCase();
        console.log(header);
        console.log(req.getResponseHeader('content-type'));
    }

再另,前端使用 fetch 方法获取文件

try {
    
    
  const imgURL = 'https://dummyimage.com/300.png';
  const data = await fetch(imgURL);
  const blob = await data.blob();
  // 获取到 blob 对象,可以保存到文件,这里复制到剪切板
  await navigator.clipboard.write([		
    new ClipboardItem({
    
    
      [blob.type]: blob
    })
  ]);
  console.log('Image copied.');
} catch (err) {
    
    
  console.error(err.name, err.message);
}

猜你喜欢

转载自blog.csdn.net/runsong911/article/details/127656442