前端-文件的上传与下载专题

1,888 阅读8分钟

概要

这篇文章总结前端实现各类文件(如 pdf,word,excel,img)上传下载的方法,供参考。

相关的概念与API

文件的上传下载,可以通过表单、FormData对象,在了解这些具体的方法之前,先了解一些与之相关的概念

  • XMLHttpRequest2:在XMLHttpRequest1的时代,ajax请求只能传输文本数据,直到XMLHttpRequest2出现后才允许服务器返回二进制数据,它是实现文件上传下载的关键。XMLHttpRequest2相比XMLHttpRequest1多了以下特性或改进
    • 可以设置HTTP请求的时限
    • 支持文件上传
    • 可以使用FormData对象管理表单数据
    • 可以请求不同域名下的数据(跨域请求)
    • 可以获取服务器端的二进制数据(请求时设置content-type为blob、arraybuffer类型)
    • 可以获得数据传输的进度信息
  • 流(Stream)字节流(Byte Steram)的简称,也就是长长的一串字节。在Java中,流就是对象,可以从这个对象读字节序列(输入流)或者写字节序列(输出流)。
  • Blob对象Blob是计算机界通用术语之一,全称写作:BLOB (binary large object),表示二进制大对象。MySql/Oracle数据库中,就有一种Blob类型,专门存放二进制数据。MDN地址
    • 实际的web应用中,Blob可以用作任意文件的二进制传输
    • Blob对象可以直接传入到URL.createObjectURL方法中,并生成一个Blob地址,形式类似于blob:null/5ca9355d-dd32-4c12-87cb-1f7955fcd426
    • Blob地址,它应该是指向浏览器内存中的一块区域,这块区域保存着真实的文件或字符串数据,可以认为Blob地址是指向浏览器特定内存区域的指针。
    • Blobl地址可以直接在浏览器地址栏中打开,如果指向的是图片或文件,还可以直接赋值给imgsrc属性或是a标签href属性并实现预览或下载。
    • Blob对象的属性之type,它是一个字符串,只读。表明该Blob对象所包含数据的MIME类型。例如,一个jpg格式图片的MIME就是image/jpeg, 如果类型未知,则该值为空字符串。
    • Blob对象的属性之size,它表示Blob对象中所包含数据的大小,以字节为单位,只读。(这个属性可以用来判断文件的大小)
  • DOMString:可以认为它就是String,即字符串
  • File对象:专门用于表示文件的对象,MDN地址
    • 含义:本身是二进制对象,并且从属于Blob对象,继承了Blob对象的所有属性和方法
    • 如何生成:File对象的获取,常见如使用类型为fileinput元素在选取文件后返回的FileList对象,即是File对象的集合,或者是使用拖拽操作产生的DataTransfer对象的files属性,也是File对象的集合。
  • URL.createObjectURL():该静态方法会创建一个DOMString,其中包含一个表示参数中给出的对象的URL。这个URL的生命周期和创建它的窗口中的docement绑定。这个新的URL对象指向参数中的File对象或Blob对象。可以简单理解为,在浏览器内存中开辟一块区域存储参数中的File对象或Blob对象内容,然后生成的URL作为指针指向这块内存区域。Note:这里的参数也可以是MediaSource对象,不过实在是不常用,因此就不计入本篇内容了。MDN地址
  • FileReader对象:FileReader对象让Web应用可以异步的读取用户电脑上的文件,它的参数可以是一个File对象或Blob对象。MDN地址,该对象以下几个API较为常用,如FileReader.readAsDataURL(),FileReader.readAsArrayBuffer(),FileReader.readAsText()
    • FileReader.readAsDataURL():该方法读取指定的Blob对象,并以data: URL格式返回指定Blob对象的内容。假如Blob对象的内容是图片(只要是二进制数据),返回的Data:URL格式其实是经过base64编码过的,如果后端要求传递Base64编码后的图片内容,可以截取Base64那一部分并传递给后端。返回结果也可以直接赋值给img的src属性,图片便可以被预览。场景3.1 有使用这个API的详细代码。
  • Data:URL:Data URLs,即前缀为 data: 协议的URL;Data URLs 由四个部分组成:前缀(data:)、指示数据类型的MIME类型、base64标记(非文本时展示)、数据本身(文本不用编码,如果是二进制数据则需要使用base64编码)。MDN地址,格式如下: data:[<mediatype>][;base64],<data>

mediatype 是个 MIME 类型的字符串,例如image/jpeg,表示JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII ,如果数据是文本类型,你可以直接将文本嵌入 (根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行base64编码之后再进行嵌入。(例如使用FileReader.readAsDataURL()会自动将文件内容转为base64编码后的字符串),区分第四部分的是否经过base64编码,最简单的方法就是看第三部分是否携带;base64标记。如:data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEd的内容区域就是经过base64编码过的。

  • FormData对象:FormData 接口提供了一种表示表单数据的键值对的构造方式,经过它的数据可以使用了XMLHttpRequest.send() 方法送出,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。这是MDN上对FormData对象的描述,我认为想要理解这句话,还要从多个方面说起。
    • rfc1867规范出来之前,http协议中不存在文件上传的功能
    • rfc1867规范规定了上传文件时需要使用from表单,且限定 form 的 method 必须为 POST , enctype 为 “multipart/form-data”,至于后面有boundary以及一串字符,这是分界符,后面的一堆字符串是随机生成的,目的是防止上传文件中出现分界符导致服务器无法正确识别文件起始位置。如:multipart/form-data; boundary=----WebKitFormBoundaryYfC6SptVA6JlK45R
    • FormData对象可以完全替代这种Form表单上传文件的形式,它可以直接添加二进制文件(blob对象或File对象)作为其某键的值,如formData.append('file', fileObj),这里的fileObj即是一个File对象。不仅是二进制数据,append方法可以添加任意js类型的数据。把Form对象传递到后端的方法也很简单,直接使用ajax请求即可。Note:使用jquery封装的ajax时,processData要设为false,processData原本是jquery用来序列化参数的,使用FormData对象时不需要序列化的处理,因此需要设为false。
    • 当使用ajax传递FormData数据时,请求头中的content-type字段会被自动设为content-type: multipart/form-data; boundary=----WebKitFormBoundaryYfC6SptVA6JlK45R,注意:boundary后面那段字符串是随机生成的,每次都不一样。

如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。

这句话再回过头来看,就会发现,它其实就是想表达,如果使用FormData作为载体来传递二进制数据或其它类型数据,并且Content-type设为multipart/form-data(实际上会被自动设置),它可以实现Form表单上传文件的能力,结合Ajax,这是一个非常优秀的功能。下面是一张利用FormData结合ajax传递图片以及多个字符串、数字类型数据的参数截图:

dKn58g.md.jpg

需要知道的MIME类型

  • pdf文件:application/pdf
  • png图片:image/png
  • jpg图片:image/jpeg
  • gif图片:image/gif
  • js文件:text/javascript
  • html文件:text/html
  • xlsx表格:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  • pptx格式ppt文件:application/vnd.openxmlformats-officedocument.presentationml.presentation
  • txt格式文件:text/plain
  • xml文件:text/xml
  • doc格式:application/msword
  • docx格式:application/vnd.openxmlformats-officedocument.wordprocessingml.document
  • xls格式:application/vnd.ms-excel
  • ppt格式:application/vnd.ms-powerpoint

1 PDF的预览

  • 原理
    • 第一步:获取Blob对象格式的pdf文件或者是File对象格式的pdf文件。(File接口继承自Blob接口)
    • 第二步:调用URL.createObjectURL()方法,生成一个DOMString
    • 第三步:将该DOMString赋值给一个iframesrc属性,该iframe会作为容器展示pdf的内容。
  • 疑问1 如何获取Blob对象File对象格式的pdf文件
    • 方案1:类型为file的input元素,选择文件后会返回一个FileList对象,它是File对象的集合
  • 场景1.1:选取本地的pdf文件并实现预览
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style>
			iframe{
				margin-top: 50px;
				margin-left: 13px;
			}
			input{
				position: fixed;
				top: 20px;
				left: 20px;
			}
		</style>
	</head>
	<body>
		<input type="file" id="input" multiple>
		<iframe id="inlineFrameExample"
		    title="Inline Frame Example"
		    width="300"
		    height="200"
		    src="">
		</iframe>
	</body>
	<script>
		const inputElement = document.getElementById("input");
		
		inputElement.addEventListener("change", handleFiles, false);
		
		function handleFiles() {
		  const fileList = this.files; /* now you can work with the file list */
		  for (let i = 0, numFiles = fileList.length; i < numFiles; i++) {
			// 这里的file即是File对象,可以直接传入URL.createObjectURL方法中并生成一个DOMString
		    const file = fileList[i];
			// obj_url是一个DOMString
			const obj_url = URL.createObjectURL(file);
			const iframe = document.getElementById('inlineFrameExample');
			iframe.setAttribute('src', obj_url);
			// DOMString的生命周期默认是在网页被关闭时结束,如果明确知道用不到该DOMString了,可以手动释放,减少浏览器的内存使用
			URL.revokeObjectURL(obj_url);
		  }
		}
	</script>
</html>
  • 场景1.1 展示效果如下图:

avlTj1.gif

  • 场景1.2:接受后端返回的pdf文件并实现预览,注意,这里后端返回的是字节流。至于Content-Type经过测试,application/jsonapplication/pdf都可行,注意:这里是通过原生的ajax请求,并且指定responseTypeblob
// 下载pdf文件
async downolad(urlparams){
    // 这里的url是请求地址,另包含一个参数,该参数需要用base64编码后发到后端
	let url = this.$ajaxUrl.content.space_downloadFile;
	let base64url = window.btoa(encodeURIComponent(urlparams));
	
	let xhr = new XMLHttpRequest();
	let path = '';
	xhr.open("get", '/space' + url + '?fileOutPath=' + base64url);
	xhr.responseType = "blob";
	xhr.onload = function() { 
	    path = URL.createObjectURL(xhr.response);
	    const iframe = document.getElementById('inlineFrameExample');
	    iframe.setAttribute('src', path);
	};
	
	xhr.send();
	
}
  • 场景1.2展示效果如下图:

av2RQs.gif

  • 场景1.3:后端返回一个存在于阿里或者其他对象存储服务网站的固定地址,如http://doc.xueqiu.com/1415d8a96191603fecf628c4.pdf,这时如果在浏览器中直接打开这个地址或者是赋值给a标签src属性,效果会是在浏览器中新打开一个窗口并预览(如谷歌浏览器中会加载谷歌的pdf阅读插件),此时如果想要实现在网页中局部预览,可以使用和场景2相同的代码。
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style>
			iframe{
				margin-top: 20px;
				margin-left: 16px;
			}
			input{
				width:98px;
				height:36px;
				background:rgba(51,153,255,1);
				border-radius:18px;
				font-size: 16px;
				color: #fff;
				line-height: 36px;
				text-align: center;
				outline: none;
				border: none;
				position: absolute;
				top: 27px;
				left: 340px;
			}
			input:hover{
				background:rgba(33,129,225,1);
			}
		</style>
	</head>
	<body>
		<iframe id="inlineFrameExample"
		    title="Inline Frame Example"
		    width="300"
		    height="200"
		    src="">
		</iframe>
		<input type="submit">
	</body>
	<script type="text/javascript">
		let url = 'http://doc.xueqiu.com/1415d8a96191603fecf628c4.pdf';
		let input = document.querySelector('input');
		
		let xhr = new XMLHttpRequest();
		let path = '';
		xhr.open("get", url);
		xhr.responseType = "blob";
		xhr.onload = function() { 
			path = URL.createObjectURL(xhr.response);
			const iframe = document.getElementById('inlineFrameExample');
			iframe.setAttribute('src', path);
		};
		
		input.addEventListener('click', () => {
			xhr.send();
		})
	</script>
</html>
  • 场景1.3展示效果如下图:

avT2Sf.gif

2 PDF的下载

  • 原理:和pdf的预览几乎一样,只是在第三步时,不再把URL.createObjectURL()方法生成的DOMString赋值给iframe标签的src属性,而是新创建一个a标签,并把它赋值给a标签src属性,然后模拟点击a标签,实现下载,下载完成后再移除a标签

  • Note:下载的场景和预览的场景完全一致,除了使用a标签来模拟下载的部分代码不一致

  • 场景2.1:从网络上获取pdf文件并下载

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<style>
			input{
				width:98px;
				height:36px;
				background:rgba(51,153,255,1);
				border-radius:18px;
				font-size: 16px;
				color: #fff;
				line-height: 36px;
				text-align: center;
				outline: none;
				border: none;
				position: absolute;
				top: 27px;
				left: 40px;
			}
			input:hover{
				background:rgba(33,129,225,1);
			}
		</style>
	</head>
	<body>
		<input type="submit" value="下载"></input>
	</body>
	<script type="text/javascript">
		let url = 'http://doc.xueqiu.com/1415d8a96191603fecf628c4.pdf';
		let input = document.querySelector('input');
		
		let xhr = new XMLHttpRequest();
		let path = '';
		xhr.open("get", url);
		xhr.responseType = "blob";
		xhr.onload = function() { 
			path = URL.createObjectURL(xhr.response);
			const a = document.createElement('a');
			a.setAttribute('download', '监测报告');
			a.setAttribute('href', path);
			a.click();
		};
		
		input.addEventListener('click', () => {
			xhr.send();
		})
	</script>
</html>
  • 场景2.1 展示效果如下图

dpiKE9.gif

3 图片的预览

  • 原理:图片展示最典型的方式是给img标签设置src属性,src可能指向项目中的一张静态图片,也可能指向公网上的某个图片地址。那么,对于本地系统上传的图片,如何实现预览呢?
    • 第一步:从typefileinput元素上获取File对象
    • 第二步:使用FileReader对象读取该File对象,并在读取完成后把生成的类型为Data:URL格式的URL赋值给img标签。另一种方式则与上面的pdf预览下载的方式类似,使用URL.createObjectURL()方法处理取到的Blob对象,并把结果赋值给img标签的src属性
  • 场景3.1:使用FileReader对象实现本地上传的图片预览
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
input{
	margin-bottom: 10px;
}
.imgCom{
    width: 300px;
    height: 300px;
    border: 1px solid #ccc;
}
.imgCom > img{
    width: 100%;
    height: 100%;
}
</style>
</head>
<body>
    <input type="file" id= "file" />
    <div class="imgCom" id= "imgCom" style="width" > </div>
<script>
    let eleFile = document.querySelector('#file');
    function preview(file) {
		// 创建FileReader对象
        let reader = new FileReader();
        reader.onload = function (e) {
			// e.target.result,这里的e.target.result是一个
			// data:URL格式的字符串(base64编码),它表示所读取文件的内容
			// e.target.result形式类似于:data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAABQAAD/4QMraHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3
			// 英文符号,(逗号)后面的表示的就是经过Base64编码后的图片内容
            document.getElementById('imgCom').innerHTML = '<img src="' + e.target.result + '">'
        }
        reader.readAsDataURL(file)
    }
 
    eleFile.addEventListener('change', function (e) {
        let file = e.target.files[0],
        reg = /\.(png|jpg|gif|bmp)$/;
        if (reg.test(file.name)) {
            preview(file)
        } else {
            alert('选择正确格式的图片');
        }
    });
</script>
</body>
</html>
  • 场景3.1:效果如下图

dpQJJO.gif

  • 场景3.2:使用URL.createObjectURL()方法实现本地上传图片的预览
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
input{
	margin-bottom: 10px;
}
.imgCom{
    width: 300px;
    height: 300px;
    border: 1px solid #ccc;
}
.imgCom > img{
    width: 100%;
    height: 100%;
}
</style>
</head>
<body>
    <input type="file" id= "file" />
    <div class="imgCom" id= "imgCom" style="width" > </div>
<script>
    let eleFile = document.querySelector('#file');
    function preview(file) {
        // obj_url是一个DOMString
        const obj_url = URL.createObjectURL(file);
        document.getElementById('imgCom').innerHTML = '<img src="' + obj_url + '">'
    }
 
    eleFile.addEventListener('change', function (e) {
        let file = e.target.files[0],
        reg = /\.(png|jpg|gif|bmp)$/;
        if (reg.test(file.name)) {
            preview(file)
        } else {
            alert('选择正确格式的图片');
        }
    });
</script>
</body>
</html>
  • 场景3.2:效果如下图

dp1Waq.gif

4 图片的下载

  • 原理:图片下载完全可以采用和上面pdf下载一样的方式,代码甚至都一模一样,这里就不展示代码了。除了这种方式,当然也可以使用其他方式下载图片,只是这种方式兼容性较好并且几乎支持所有图片。
  • 场景4.1:下载网络上的图片(代码和下载pdf的一模一样,只是换了资源链接地址)
  • 场景4.1效果:如下图

dpJDTs.gif

5 接收arraybuffer类型数据并处理

  • 原理:XMLHttpRequest level2提供了上传和下载二进制数据的能力,这也使得接收后端的二进制数据并转换为前端所需要格式成为可能。接收来自后端的二进制数据时,除了blob类型,arraybuffer类型也是可以的。
  • 原理:默认情况下,ajax请求返回的数据类型是文本,如果想要后端返回arraybuffer类型数据,需要通过responseType字段来指定。
  • responseType类型列举
    • "":responseType为空字符串时,采用默认类型DOMString(即字符串),与responseType值为"text"时相同。
    • arraybuffer:一个包含二进制数据的Javascript ArrayBuffer类型数据
    • blob:一个包含二进制数据的Javascript Blob对象
    • text:和空字符串时意义相同,都表示文本数据
    • document:html或xml格式数据
  • 如何使用
// 现在的场景是从后端获取图片数据并展示在页面的img标签,后端返回的是二进制数据

const xhr = new XMLHttpRequest();

xhr.open("GET", '/path/test.php');

xhr.responseType = 'arraybuffer';

xhr.onload = (e) => {
    const bytes = new Unit8Array(xhr.rsponse);
    let binary = '';
    
    for (let i = 0;  i < bytes.length; i++){
        binary += String.fromCharCode(bytes[i]);
    }
    
    const src = window.btoa(binary);
    
    document.getElementById('image').src = `data:image/jpeg;base64,${src}`;
}

xhr.send();