前端無法像原生APP一樣直接操作本地文件谊迄,否則的話打開個網(wǎng)頁就能把用戶電腦上的文件偷光了拇派,所以需要通過用戶觸發(fā)电爹,用戶可通過以下三種方式操作觸發(fā):
通過input type=”file” 選擇本地文件
通過拖拽的方式把文件拖過來
在編輯框里面復(fù)制粘貼
第一種是最常用的手段睦番,通常還會自定義一個按鈕喜爷,然后蓋在它上面当窗,因為type=”file”的input不好改變樣式够坐。如下代碼寫一個選擇控件,并放在form里面:
<form> <input type="file" id="file-input" name="fileContent"> </form>
然后就可以用FormData獲取整個表單的內(nèi)容:
$("#file-input").on("change", function() { console.log(`file name is ${this.value}`); let formData = new FormData(this.form); formData.append("fileName", this.value); console.log(formData); });
把input的value和formData打印出來是這樣的:
可以看到文件的路徑是一個假的路徑,也就是說在瀏覽器無法獲取到文件的真實存放位置元咙。同時FormData打印出來是一個空的Objet梯影,但并不是說它的內(nèi)容是空的,只是它對前端開發(fā)人員是透明的庶香,無法查看甲棍、修改、刪除里面的內(nèi)容赶掖,只能append添加字段感猛。
FormData無法得到文件的內(nèi)容,而使用FileReader可以讀取整個文件的內(nèi)容奢赂。用戶選擇文件之后陪白,input.files就可以得到用戶選中的文件,如下代碼:
$("#file-input").on("change", function() { let fileReader = new FileReader(), fileType = this.files[0].type; fileReader.onload = function() { if (/^image/.test(fileType)) { // 讀取結(jié)果在fileReader.result里面 $(`<img src="${this.result}">`).appendTo("body"); } } // 打印原始File對象 console.log(this.files[0]); // base64方式讀取 fileReader.readAsDataURL(this.files[0]); });
把原始的File對象打印出來是這樣的:
它是一個window.File的實例膳灶,包含了文件的修改時間咱士、文件名、文件的大小轧钓、文件的mime類型等序厉。如果需要限制上傳文件的大小就可以通過判斷size屬性有沒有超,單位是字節(jié)聋迎,而要判斷是否為圖片文件就可以通過type類型是否以image開頭脂矫。通過判斷文件名的后綴可能會不準(zhǔn),而通過這種判斷會比較準(zhǔn)霉晕。上面的代碼使用了一個正則判斷,如果是一張圖片的話就把它賦值給img的src捞奕,并添加到dom里面牺堰,但其實這段代碼有點問題,就是web不是所有的圖片都能通過img標(biāo)簽展示出來颅围,通常是jpg/png/gif這三種伟葫,所以你應(yīng)該需要再判斷一下圖片格式,如可以把判斷改成:
/^image\/[jpeg|png|gif]/.test(this.type)
然后實例化一個FileReader院促,調(diào)它的readAsDataURL并把File對象傳給它筏养,監(jiān)聽它的onload事件,load完讀取的結(jié)果就在它的result屬性里了常拓。它是一個base64格式的渐溶,可直接賦值給一個img的src.
使用FileReader除了可讀取為base64之外,還能讀取為以下格式:
fileReader.readAsDataURL(this.files[0]); // 以二進制字符串方式讀取弄抬,結(jié)果是二進制內(nèi)容的utf-8形式茎辐,已被廢棄了 fileReader.readAsBinaryString(this.files[0]); // 以原始二進制方式讀取,讀取結(jié)果可直接轉(zhuǎn)成整數(shù)數(shù)組 fileReader.readAsArrayBuffer(this.files[0]);
其它的主要是能讀取為ArrayBuffer,它是一個原始二進制格式的結(jié)果拖陆。把ArrayBuffer打印出來是這樣的:
可以看到弛槐,它對前端開發(fā)人員也是透明的,不能夠直接讀取里面的內(nèi)容依啰,但可以通過ArrayBuffer.length得到長度乎串,還能轉(zhuǎn)成整型數(shù)組,就能知道文件的原始二進制內(nèi)容了:
let buffer = this.result; // 依次每字節(jié)8位讀取速警,放到一個整數(shù)數(shù)組 let view = new Uint8Array(buffer); console.log(view);
如果是通過第二種拖拽的方式叹誉,應(yīng)該怎么讀取文件呢?如下html(樣式略):
<div class="img-container"> drop your image here </div>
這將在頁面顯示一個框:
然后監(jiān)聽它的拖拽事件:
$(".img-container").on("dragover", function (event) { event.preventDefault(); }) .on("drop", function(event) { event.preventDefault(); // 數(shù)據(jù)在event的dataTransfer對象里 let file = event.originalEvent.dataTransfer.files[0]; // 然后就可以使用FileReader進行操作 fileReader.readAsDataURL(file); // 或者是添加到一個FormData let formData = new FormData(); formData.append("fileContent", file); })
數(shù)據(jù)在drop事件的event.dataTransfer.files里面坏瞄,拿到這個File對象之后就可以和輸入框進行一樣的操作了桂对,即使用FileReader讀取,或者是新建一個空的formData鸠匀,然后把它append到formData里面蕉斜。
第三種粘貼的方式,通常是在一個編輯框里操作缀棍,如把div的contenteditable設(shè)置為true:
<div contenteditable="true"> hello, paste your image here </div>
粘貼的數(shù)據(jù)是在event.clipboardData.files里面:
$("#editor").on("paste", function(event) { let file = event.originalEvent.clipboardData.files[0]; });
但是Safari的粘貼不是通過event傳遞的宅此,它是直接在輸入框里面添加一張圖片,如下圖所示:
它新建了一個img標(biāo)簽爬范,并把img的src指向一個blob的本地數(shù)據(jù)父腕。什么是blob呢,如何讀取blob的內(nèi)容呢青瀑?
blob是一種類文件的存儲格式璧亮,它可以存儲幾乎任何格式的內(nèi)容,如json:
let data = {hello: "world"}; let blob = new Blob([JSON.stringify(data)], {type : 'application/json'});
為了獲取本地的blob數(shù)據(jù)斥难,我們可以用ajax發(fā)個本地的請求:
$("#editor").on("paste", function(event) { // 需要setTimeout 0等圖片出來了再處理 setTimeout(() => { let img = $(this).find("img[src^='blob']")[0]; console.log(img.src); // 用一個xhr獲取blob數(shù)據(jù) let xhr = new XMLHttpRequest(); xhr.open("GET", img.src); // 改變mime類型 xhr.responseType = "blob"; xhr.onload = function () { // response就是一個Blob對象 console.log(this.response); }; xhr.send(); }, 0); });
上面代碼把blob打印出來是這樣的:
能得到它的大小和類型枝嘶,但是具體內(nèi)容也是不可見的,它有一個slice的方法哑诊,可用于切割大文件群扶。和File一樣,可以使用FileReader讀取它的內(nèi)容:
function readBlob(blobImg) { let fileReader = new FileReader(); fileReader.onload = function() { console.log(this.result); } fileReader.onerror = function(err) { console.log(err); } fileReader.readAsDataURL(blobImg); } readBlob(this.response);
除此镀裤,還能使用window.URL讀取竞阐,這是一個新的API,經(jīng)常和Service Worker配套使用暑劝,因為SW里面常常要解析url骆莹。如下代碼:
function readBlob(blobImg) { let urlCreator = window.URL || window.webkitURL; // 得到base64結(jié)果 let imageUrl = urlCreator.createObjectURL(this.response); return imageUrl; } readBlob(this.response);
關(guān)于src使用的是blob鏈接的,除了上面提到的img之外铃岔,另外一個很常見的是video標(biāo)簽汪疮,如youtobe的視頻就是使用的blob:
這種數(shù)據(jù)不是直接在本地的峭火,而是通過持續(xù)請求視頻數(shù)據(jù),然后再通過blob這個容器媒介添加到video里面智嚷,它也是通過URL的API創(chuàng)建的:
let mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); let sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"'); sourceBuffer.appendBuffer(buf);
具體我也沒實踐過卖丸,不再展開討論。
上面盏道,我們使用了三種方式獲取文件內(nèi)容稍浆,最后得到:
FormData格式
FileReader讀取得到的base64或者ArrayBuffer二進制格式
如果直接就是一個FormData了,那么直接用ajax發(fā)出去就行了猜嘱,不用做任何處理:
let form = document.querySelector("form"), formData = new FormData(form), formData.append("fileName", "photo.png"); let xhr = new XMLHttpRequest(); // 假設(shè)上傳文件的接口叫upload xhr.open("POST", "/upload"); xhr.send(formData);
如果用jQuery的話衅枫,要設(shè)置兩個屬性為false:
$.ajax({ url: "/upload", type: "POST", data: formData, processData: false, // 不處理數(shù)據(jù) contentType: false // 不設(shè)置內(nèi)容類型 });
因為jQuery會自動把內(nèi)容做一些轉(zhuǎn)義,并且根據(jù)data自動設(shè)置請求mime類型朗伶,這里告訴jQuery直接用xhr.send發(fā)出去就行了弦撩。
觀察控制臺發(fā)請求的數(shù)據(jù):
可以看到這是一種區(qū)別于用&連接參數(shù)的方式,它的編碼格式是multipart/form-data论皆,就是上傳文件form表單寫的enctype:
<form enctype="multipart/form-data" method="post"> <input type="file" name="fileContent"> </form>
如果xhr.send的是FormData類型話益楼,它會自動設(shè)置enctype,如果你用默認(rèn)表單提交上傳文件的話就得在form上面設(shè)置這個屬性点晴,因為上傳文件只能使用POST的這種編碼感凤。常用的POST編碼是application/x-www-form-urlencoded,它和GET一樣粒督,發(fā)送的數(shù)據(jù)里面陪竿,參數(shù)和參數(shù)之間使用&連接,如:
key1=value1&key2=value2
特殊字符做轉(zhuǎn)義屠橄,這個數(shù)據(jù)POST是放在請求body里的族跛,而GET是拼在url上面的,如果用jq的話锐墙,jq會幫你拼并做轉(zhuǎn)義庸蔼。
而上傳文件用的這種multipart/form-data,參數(shù)和參數(shù)之間是且一個相同的字符串隔開的贮匕,上面的是使用:
——WebKitFormBoundary72yvM25iSPYZ4a3F
這個字符通常會取得比較長、比較隨機花枫,因為要保證正常的內(nèi)容里面不會出現(xiàn)這個字符串刻盐,這樣內(nèi)容的特殊字符就不用做轉(zhuǎn)義了。
請求的contentType被瀏覽器設(shè)置成:
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundary72yvM25iSPYZ4a3F
后端服務(wù)通過這個就知道怎么解析這么一段數(shù)據(jù)了劳翰。(通常是使用的框架處理了敦锌,而具體的接口不需要關(guān)心應(yīng)該怎么解析)
如果讀取結(jié)果是ArrayBuffer的話,也是可以直接用xhr.send發(fā)送出去的佳簸,但是一般我們不會直接把一個文件的內(nèi)容發(fā)出去乙墙,而是用某個字段名等于文件內(nèi)容的方式颖变。如果你讀取為ArrayBuffer的話再上傳的話其實作用不是很大,還不如直接用formData添加一個File對象的內(nèi)容听想,因為上面三種方式都可以拿到File對象腥刹。如果一開始就是一個ArrayBuffer了,那么可以轉(zhuǎn)成blob然后再append到FormData里面汉买。
使用比較多的應(yīng)該是base64衔峰,因為前端經(jīng)常要處理圖片,讀取為base64之后就可以把它畫到一個canvas里面蛙粘,然后就可以做一些處理垫卤,如壓縮、裁剪出牧、旋轉(zhuǎn)等穴肘。最后再用canvas導(dǎo)出一個base64格式的圖片,那怎么上傳base64格式的呢舔痕?
第一種是拼一個表單上傳的multipart/form-data的格式评抚,再用xhr.sendAsBinary發(fā)出去,如下代碼:
let boundary = "----------boundaryasoifvlkasldvavoadv"; xhr.sendAsBinary([ // name=data boundary, 'Content-Disposition: form-data; name="data"; filename="' + fileName + '"', 'Content-Type: ' + "image/" + fileType, '', atob(base64Data), boundary, //name=imageType boundary, 'Content-Disposition: form-data; name="imageType"', '', fileType, boundary + '--' ].join('\r\n'));
上面代碼使用了window.atob的api赵讯,它可以把base64還原成原始內(nèi)容的字符串表示盈咳,如下圖所示:
btoa是把內(nèi)容轉(zhuǎn)化成base64編碼,而atob是把base64還原边翼。在調(diào)atob之前鱼响,需要把表示內(nèi)容格式的不屬于base64內(nèi)容的字符串去掉,即上面代碼第一行的replace處理组底。
這樣就和使用formData類似了丈积,但是由于sendAsBinary已經(jīng)被deprecated了,所以新代碼不建議再使用這種方式债鸡。那怎么辦呢江滨?
可以把base64轉(zhuǎn)化成blob,然后再append到一個formData里面厌均,下面的函數(shù)(來自b64-to-blob)可以把base64轉(zhuǎn)成blob:
function b64toBlob(b64Data, contentType, sliceSize) { contentType = contentType || ''; sliceSize = sliceSize || 512; var byteCharacters = atob(b64Data); var byteArrays = []; for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { var slice = byteCharacters.slice(offset, offset + sliceSize); var byteNumbers = new Array(slice.length); for (var i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } var byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } var blob = new Blob(byteArrays, {type: contentType}); return blob; }
然后就可以append到formData里面:
let blob = b64toBlob(b64Data, "image/png"), formData = new FormData(); formData.append("fileContent", blob);
這樣就不用自己去拼一個multipart/form-data的格式數(shù)據(jù)了唬滑。
上面處理和上傳文件的API可以兼容到IE10+,如果要兼容老的瀏覽器應(yīng)該怎么辦呢棺弊?
可以借助一個iframe晶密,原理是默認(rèn)的form表單提交會刷新頁面,或者跳到target指定的那個url模她,但是如果把ifrmae的target指向一個iframe稻艰,那么刷新的就是iframe,返回結(jié)果也會顯示在ifame侈净,然后獲取這個ifrmae的內(nèi)容就可得到上傳接口返回的結(jié)果尊勿。
如下代碼:
iframe.display = "none"; iframe.name = "form-iframe"; document.body.appendChild(iframe); // 改變form的target form.target = "form-iframe"; iframe.onload = function() { //獲取iframe的內(nèi)容僧凤,即服務(wù)返回的數(shù)據(jù) let responseText = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent; }; form.submit();
form.submit會觸發(fā)表單提交,當(dāng)請求完成(成功或者失斣印)之后就會觸發(fā)iframe的onload事件躯保,然后在onload事件獲取返回的數(shù)據(jù),如果請求失敗了的話摇展,iframe里的內(nèi)容就為空吻氧,可以用這個判斷請求有沒有成功。
使用iframe沒有辦法獲取上傳進度咏连,使用xhr可以獲取當(dāng)前上傳的進度盯孙,這個是在XMLHttpRequest 2.0引入的:
xhr.upload.onprogress = function (event) { if (event.lengthComputable) { // 當(dāng)前上傳進度的百分比 duringCallback ((event.loaded / event.total)*100); } };
這樣就可以做一個真實的loading進度條。
本文討論了3種交互方式的讀取方式祟滴,通過input控件在input.files可以得到File文件對象振惰,通過拖拽的是在drop事件的event.dataTransfer.files里面,而通過粘貼的paste事件在event.clipboardData.files里面垄懂,Safari這個怪胎是在編輯器里面插入一個src指向本地的img標(biāo)簽骑晶,可以通過發(fā)送一個請求加載本地的blob數(shù)據(jù),然后再通過FileReader讀取草慧,或者直接append到formData里面桶蛔。得到的File對象就可以直接添加到FormData里面,如果需要先讀取base64格式做處理的漫谷,那么可以把處理后的base64轉(zhuǎn)化為blob數(shù)據(jù)再append到formData里面仔雷。對于老瀏覽器,可以使用一個iframe解決表單提交刷新頁面或者跳頁的問題舔示。
總之碟婆,前端處理和上傳本地文件應(yīng)該差不多就是這些內(nèi)容了,但是應(yīng)該還有好多細(xì)節(jié)沒有提及到惕稻,讀者可通過本文列的方向自行實踐竖共。如果有其它的上傳方式還請告知。