前端本地文件操作及上传
时间:2023-03-16 23:41:37
科技观察
前端不能像原生APP那样直接操作本地文件,否则打开一个网页会窃取用户电脑上的所有文件,所以需要用户触发。用户可以使用以下三种方式操作触发:通过inputtype=”file”选择本地文件通过拖拽的方式将文件拖过来,复制粘贴到编辑框***是最常用的方法,还有通常自定义一个按钮,然后覆盖在上面,因为type="file"的input不容易改变样式。用下面的代码写一个选择控件,放到表单中:
然后就可以用FormData获取内容了整个形式的:$("#file-input").on("change",function(){console.log(`filenameis${this.value}`);letformData=newFormData(this.form);formData.append("fileName",this.value);console.log(formData);});像这样打印出inputvalue和formData:可以看到文件的路径是假路径,也就是说在浏览器中无法获取到文件的实际存放位置。同时打印出来的FormData是一个空对象,但并不代表它的内容是空的,而是对前端开发者透明的,不能查看、修改、删除里面的内容,只能追加到添加字段。FormData获取不到文件的内容,但是使用FileReader可以读取整个文件的内容。用户选择文件后,input.files可以获取用户选择的文件,如下:$("#file-input").on("change",function(){letfileReader=newFileReader(),fileType=this.files[0].type;fileReader.onload=function(){if(/^image/.test(fileType)){//读取结果在fileReader.result$(`
`).appendTo("body");}}//打印原始File对象console.log(this.files[0]);//Base64方式读取fileReader.readAsDataURL(this.files[0]);});打印出原来的File对象是这样的:它是window.File的一个实例,包括文件的修改时间、文件名、文件大小、文件的mime类型等。如果需要限制上传文件的大小,可以判断是否超过size属性,单位是字节。判断是否是图片文件,可以判断type类型是否以image开头。判断文件名的后缀可能不准确,但这种判断会更准确。上面的代码使用了正则判断。如果是图片,则赋值给img的src,添加到dom中。但其实这段代码有个问题,就是不是所有网页上的图片都可以通过img标签显示出来,通常有jpg/png/gif三种,所以要判断图片再次格式化,例如可以将判断改成:/^image\/[jpeg|png|gif]/.test(this.type)然后实例化一个FileReader,调用它的readAsDataURL,把File对象传给它,监听到它的onload事件,加载后的读取结果在它的result属性中。它是base64格式,可以直接赋值给一个img的src。使用FileReader不仅可以读取为base64,还可以读取为如下格式://以base64的方式读取,结果为base64,任何文件都可以转换为base64格式fileReader.readAsDataURL(this.files[0]);//以二进制字符串方式读取,结果为二进制内容的utf-8格式,已被丢弃fileReader.readAsBinaryString(this.files[0]);//以原始二进制方式读取,读取结果可以直接转成整型数组fileReader.readAsArrayBuffer(this.files[0]);其他的主要可以读作ArrayBuffer,这是一个rawbinaryFormat结果。打印ArrayBuffer是这样的:可以看到它对前端开发者也是透明的,不能直接读取里面的内容,但是可以通过ArrayBuffer.length获取长度,并转换成整型数组,这样就可以知道了文件原始二进制内容:letbuffer=this.result;//依次读取每字节8位,放入整型数组letview=newUint8Array(buffer);console.log(view);如果是通过第二次拖拽,我应该如何通过拖拽来读取文件呢?下面的html(省略样式):dropyourimagehere 这将在页面上显示一个框:然后监听它的拖动事件:$(".img-container").on("dragover",function(event){event.preventDefault();}).on("drop",function(event){event.preventDefault();//数据在事件的dataTransfer对象中letfile=event.originalEvent.dataTransfer.files[0];//然后就可以使用FileReader来操作fileReader.readAsDataURL(file);//或者添加到一个FormDataletformData=newFormData();formData.append("fileContent",file);})数据在drop事件的event.dataTransfer.files中,得到File对象后,可以进行和输入框一样的操作,即使用FileReader读取,或者创建一个空的formData,然后追加到表单数据。第三种粘贴方式通常是在编辑框中操作,比如将div的contenteditable设置为true:$("#editor").on("粘贴",函数(事件){letfile=event.originalEvent.clipboardData.files[0];});但是Safari的粘贴并没有通过事件传递,它是直接在输入框中添加图片,如下图所示:它新建了一个img标签,并将img的src指向了一个blob的本地数据。什么是blob,如何读取blob的内容?Blob是一种类文件的存储格式,几乎可以存储任何格式的内容,比如json:letdata={hello:"world"};letblob=newBlob([JSON.stringify(data)],{type:'application/JSON'});为了获取本地的blob数据,我们可以使用ajax发送本地请求:$("#editor").on("paste",function(event){//需要setTimeout0,等待图片出来之前ProcessingsetTimeout(()=>{letimg=$(this).find("img[src^='blob']")[0];console.log(img.src);//用xhr获取blob数据letxhr=newXMLHttpRequest();xhr.open("GET",img.src);//改变mime类型xhr.responseType="blob";xhr.onload=function(){//response是一个Blob对象console.log(this.response);};xhr.send();},0);});上面的代码打印blob是这样的:可以获取到它的大小和类型,但是看不到具体的内容,它有一个slice方法,可以用来对大文件进行切片。和File一样,你可以使用FileReader来读取它的内容:console.log(err);}fileReader.readAsDataURL(blobImg);}readBlob(this.response);另外可以使用window.URL来读取,这是一个新的API,经常和ServiceWorker一起使用,因为在SW中经常会解析url。代码如下:关于src使用blob链接,除了上面提到的img,还有一个很常见的就是video标签,比如youtobevideo就是使用的blob:这种数据不是直接在本地,而是通过不断请求视频数据,然后通过blob这个容器medium添加到视频中,也是通过URLAPI创建的:letmediaSource=newMediaSource();video.src=URL.createObjectURL(mediaSource);letsourceBuffer=mediaSource.addSourceBuffer('video/mp4;codecs="avc1.42E01E,mp4a.40.2"');sourceBuffer.appendBuffer(buf);我还没有实践过,就不讨论了。上面我们使用了三种方法来获取文件内容,最终得到:FormData格式FileReader读取的base64或者ArrayBuffer二进制格式直接就是一个FormData,那么可以不做任何处理直接用ajax发送:letform=document.querySelector("form"),formData=newFormData(form),formData.append("fileName","photo.png");letxhr=newXMLHttpRequest();//假设上传文件的接口调用uploadxhr.open("POST","/上传");xhr.send(formData);如果使用jQuery,设置两个属性为false:$.ajax({url:"/upload",type:"POST",data:formData,processData:false,//不处理数据contentType:false//不处理设置内容类型});因为jQuery会自动对内容进行转义,自动根据数据设置请求mime类型,这里告诉jQuery直接使用xhr.send发出即可。观察控制台请求的数据:
如果xhr.send是FormData类型,则将自动设置enctype。如果使用默认表单提交上传文件,则必须在表单上设置该属性,因为上传文件只能使用POST这种编码。常用的POST编码是application/x-www-form-urlencoded,和GET一样。发送的数据中,参数之间用&连接,如:key1=value1&key2=value2,使用特殊字符作为转义,将此数据POSTed放在请求体中,在url上拼写GET。如果你使用jq,jq会帮助你拼写和逃生。用于上传文件的multipart/form-data,参数之间用同一个字符串隔开。上面是使用:——WebKitFormBoundary72yvM25iSPYZ4a3F这个字符通常比较长,比较随机,因为需要保证这个字符串不会出现在正常的内容中,这样内容中的特殊字符就不需要转义了。请求的contentType被浏览器设置为:Content-Type:multipart/form-data;boundary=——WebKitFormBoundary72yvM25iSPYZ4a3F后端服务通过这个就知道怎么解析这么一段数据了。(通常由使用的框架处理,具体接口如何解析不需要关心)如果读取的结果是ArrayBuffer,也可以直接用xhr.send发送,但一般我们不会直接发送文件的内容发送出去,但使用与文件内容相同的字段名。如果读成ArrayBuffer再上传,其实效果不是很大。最好直接用formData添加一个File对象的内容,因为以上三种方法都可以得到File对象。如果一开始是ArrayBuffer,可以转成blob再追加到FormData中。用的比较多的应该是base64,因为前端经常需要处理图片,读取成base64后,可以绘制成canvas,然后可以做一些处理,比如压缩,裁剪,旋转等***然后用canvas导出一张base64格式的图片,那么如何上传base64格式的图片呢?首先是结合一个form上传的multipart/form-data的格式,然后使用xhr.sendAsBinary发送出去,如下代码:letbase64Data=base64Data.replace(/^data:image\/[^;]+;base64,/,"");letboundary="----------boundaryasoifvlkasldvavoadv";xhr.sendAsBinary([//name=databoundary,'Content-Disposition:form-data;name="data";filename="'+fileName+'"','Content-Type:'+"image/"+fileType,'',atob(base64Data),boundary,//name=imageTypeboundary,'Content-Disposition:form-data;name="imageType"','',fileType,boundary+'--'].join('\r\n'));上面的代码使用了window.atob的api,可以将base64还原为原始内容的字符串表示,如下图所示:btoa将内容转为base64编码,而atob则还原为base64。在调整atob之前,需要去掉表示内容格式的不属于base64内容的字符串,也就是上面代码第一行的replace处理。这类似于使用formData,但由于sendAsBinary已被弃用,因此不建议将此方法用于新代码。那我们该怎么办呢?您可以将base64转换为blob,然后附加到formData。以下函数(从b64到blob)可以将base64转换为blob:functionb64toBlob(b64Data,contentType,sliceSize){contentType=contentType||'';sliceSize=sliceSize||512;varbyteCharacters=atob(b64Data);varbyteArrays=[];for(varoffset=0;offset