• Web 应用开发之文件下载



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

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

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

    超链接下载文件

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

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

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

    如果将超链接 a 元素添加 download 属性,则可以保存下载文件。

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

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

    // 创建临时超链接
    let link = document.createElement('a');
    // 给临时超链接添加 download 属性,设置保存文件名。需要注意的是有些浏览器不支持 download 设置文件名的方式
    link.download = 'test.jpg';
    // 给临时超链接添加 url
    link.href = url;
    // 触发 click 事件
    link.click();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    需注意的是,这种方法只适合同源的图片。如果跨域,则需要使用 canvas。另外使用完临时创建的超链接对象后需要考虑到资源回收。如果将此对象添加到了 dom 中,则需要使用 link.remove() 方法(如果不支持 remove() 方法,则使用 parentNode.removeChild(link)),如果只是变量引用,而没有添加到 dom 中,则可以将变量设置为 null 让垃圾回收器将其回收。

    获取 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
    # 响应包设置好了,将此响应包发给前端即可
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

    from urllib.parse import quote
    
    resp['Content-Disposition'] = "attachment;filename*=UTF-8''" + quote(filename)
    
    • 1
    • 2
    • 3

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

    后端传送二进制文件对象

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

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

    这样也可以使用:

    buf = io.BytesIO(wb)
    
    • 1

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

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

    buf.close()
    
    • 1

    原生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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

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

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

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

    • 权限校验,可以根据用户权限限制是否下载,可否高速下载等。
    // 不用 blob 来构建 URL,而是通过后端服务器来计算出用户的下载链接,然后再动态创建  标签的方式来实现下载
    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'));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    再另,前端使用 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    需要注意的是,由于 fetch 是异步的,其返回的是一个 Promise 对象,所以上一个例子中需使用 await 等待其执行完成有了结果后才能返回正确的数据。但是 await 必须处在一个异步函数中,即 async 函数。如果需要在同步函数中处理,使用下面的方式:

    try {
    	const imgURL = 'https://dummyimage.com/300.png';
    	fetch(imgURL).then(response => response.blob())	// 获取到 response 对象,进行处理,并返回 blob 对象
    		.then(blob => {								// 根据需要处理获取到的 blob 对象,例如保存
    	        let link = document.createElement('a');
    	        link.href = window.URL.createObjectURL(blob);
    	        link.download = fileName;			// fileName 需要定义
    	        link.click();
    	        window.URL.revokeObjectURL(link.href);
    		});
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    day14网络编程
    Java自动化测试框架有哪几类、区别是什么?
    Docker
    Java【算法 05】通过时间获取8位验证码(每两个小时生成一个)源码分享
    [sqoop]hive3.1.2 hadoop3.1.1安装sqoop1.4.7
    包含日志文件
    X-CSV-Reader:一个使用Rust实现CSV命令行读取器
    C#基础学习笔记
    【FPGA教程案例66】硬件开发板调试6——基于FPGA的UDP网口通信和数据传输
    【温故而知新】构建高可用Linux服务器(二)
  • 原文地址:https://blog.csdn.net/runsong911/article/details/127656442