前端沒法像原生APP同樣直接操做本地文件,不然的話打開個網頁就能把用戶電腦上的文件偷光了,因此須要經過用戶觸發,用戶可經過如下三種方式操做觸發:html
第一種是最經常使用的手段,一般還會自定義一個按鈕,而後蓋在它上面,由於type="file"的input很差改變樣式。以下代碼寫一個選擇控件,並放在form裏面:前端
<form> <input type="file" id="file-input" name="fileContent"> </form>複製代碼
而後就能夠用FormData獲取整個表單的內容:git
$("#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打印出來是這樣的:github
能夠看到文件的路徑是一個假的路徑,也就是說在瀏覽器沒法獲取到文件的真實存放位置。同時FormData打印出來是一個空的Objet,但並非說它的內容是空的,只是它對前端開發人員是透明的,沒法查看、修改、刪除裏面的內容,只能append添加字段。web
FormData沒法獲得文件的內容,而使用FileReader能夠讀取整個文件的內容。用戶選擇文件以後,input.files就能夠獲得用戶選中的文件,以下代碼:ajax
$("#file-input").on("change", function() { let fileReader = new FileReader(), fileType = this.files[0].type; fileReader.onload = function() { if (/^image/.test(fileType)) { // 讀取結果在fileReader.result裏面 $(`<img src="${this.result}">`).appendTo("body"); } } // 打印原始File對象 console.log(this.files[0]); // base64方式讀取 fileReader.readAsDataURL(this.files[0]); });複製代碼
把原始的File對象打印出來是這樣的:json
它是一個window.File的實例,包含了文件的修改時間、文件名、文件的大小、文件的mime類型等。若是須要限制上傳文件的大小就能夠經過判斷size屬性有沒有超,單位是字節,而要判斷是否爲圖片文件就能夠經過type類型是否以image開頭。經過判斷文件名的後綴可能會不許,而經過這種判斷會比較準。上面的代碼使用了一個正則判斷,若是是一張圖片的話就把它賦值給img的src,並添加到dom裏面,但其實這段代碼有點問題,就是web不是全部的圖片都能經過img標籤展現出來,一般是jpg/png/gif這三種,因此你應該須要再判斷一下圖片格式,如能夠把判斷改爲:canvas
/^image\/[jpeg|png|gif]/.test(this.type)複製代碼
而後實例化一個FileReader,調它的readAsDataURL並把File對象傳給它,監聽它的onload事件,load完讀取的結果就在它的result屬性裏了。它是一個base64格式的,可直接賦值給一個img的src.後端
使用FileReader除了可讀取爲base64以外,還能讀取爲如下格式:api
// 按base64的方式讀取,結果是base64,任何文件均可轉成base64的形式
fileReader.readAsDataURL(this.files[0]);
// 以二進制字符串方式讀取,結果是二進制內容的utf-8形式,已被廢棄了
fileReader.readAsBinaryString(this.files[0]);
// 以原始二進制方式讀取,讀取結果可直接轉成整數數組
fileReader.readAsArrayBuffer(this.files[0]);複製代碼
其它的主要是能讀取爲ArrayBuffer,它是一個原始二進制格式的結果。把ArrayBuffer打印出來是這樣的:
能夠看到,它對前端開發人員也是透明的,不可以直接讀取裏面的內容,但能夠經過ArrayBuffer.length獲得長度,還能轉成整型數組,就能知道文件的原始二進制內容了:
let buffer = this.result; // 依次每字節8位讀取,放到一個整數數組 let view = new Uint8Array(buffer); console.log(view);複製代碼
若是是經過第二種拖拽的方式,應該怎麼讀取文件呢?以下html(樣式略):
<div class="img-container"> drop your image here </div> 複製代碼
這將在頁面顯示一個框:
而後監聽它的拖拽事件:
$(".img-container").on("dragover", function (event) { event.preventDefault(); }) .on("drop", function(event) { event.preventDefault(); // 數據在event的dataTransfer對象裏 let file = event.originalEvent.dataTransfer.files[0]; // 而後就可使用FileReader進行操做 fileReader.readAsDataURL(file); // 或者是添加到一個FormData let formData = new FormData(); formData.append("fileContent", file); })複製代碼
數據在drop事件的event.dataTransfer.files裏面,拿到這個File對象以後就能夠和輸入框進行同樣的操做了,即便用FileReader讀取,或者是新建一個空的formData,而後把它append到formData裏面。
第三種粘貼的方式,一般是在一個編輯框裏操做,如把div的contenteditable設置爲true:
<div contenteditable="true"> hello, paste your image here </div>複製代碼
粘貼的數據是在event.clipboardData.files裏面:
$("#editor").on("paste", function(event) { let file = event.originalEvent.clipboardData.files[0]; });複製代碼
可是Safari的粘貼不是經過event傳遞的,它是直接在輸入框裏面添加一張圖片,以下圖所示:
它新建了一個img標籤,並把img的src指向一個blob的本地數據。什麼是blob呢,如何讀取blob的內容呢?
blob是一種類文件的存儲格式,它能夠存儲幾乎任何格式的內容,如json:
let data = {hello: "world"}; let blob = new Blob([JSON.stringify(data)], {type : 'application/json'}); 複製代碼
爲了獲取本地的blob數據,咱們能夠用ajax發個本地的請求:
$("#editor").on("paste", function(event) { // 須要setTimeout 0等圖片出來了再處理 setTimeout(() => { let img = $(this).find("img[src^='blob']")[0]; console.log(img.src); // 用一個xhr獲取blob數據 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打印出來是這樣的:
能獲得它的大小和類型,可是具體內容也是不可見的,它有一個slice的方法,可用於切割大文件。和File同樣,可使用FileReader讀取它的內容:
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,常常和Service Worker配套使用,由於SW裏面經常要解析url。以下代碼:
function readBlob(blobImg) { let urlCreator = window.URL || window.webkitURL; // 獲得base64結果 let imageUrl = urlCreator.createObjectURL(this.response); return imageUrl; } readBlob(this.response); 複製代碼
關於src使用的是blob連接的,除了上面提到的img以外,另一個很常見的是video標籤,如youtobe的視頻就是使用的blob:
這種數據不是直接在本地的,而是經過持續請求視頻數據,而後再經過blob這個容器媒介添加到video裏面,它也是經過URL的API建立的:
let mediaSource = new MediaSource(); video.src = URL.createObjectURL(mediaSource); let sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"'); sourceBuffer.appendBuffer(buf);複製代碼
具體我也沒實踐過,再也不展開討論。
上面,咱們使用了三種方式獲取文件內容,最後獲得:
若是直接就是一個FormData了,那麼直接用ajax發出去就好了,不用作任何處理:
let form = document.querySelector("form"), formData = new FormData(form), formData.append("fileName", "photo.png"); let xhr = new XMLHttpRequest(); // 假設上傳文件的接口叫upload xhr.open("POST", "/upload"); xhr.send(formData);複製代碼
若是用jQuery的話,要設置兩個屬性爲false:
$.ajax({ url: "/upload", type: "POST", data: formData, processData: false, // 不處理數據 contentType: false // 不設置內容類型 });複製代碼
由於jQuery會自動把內容作一些轉義,而且根據data自動設置請求mime類型,這裏告訴jQuery直接用xhr.send發出去就好了。
觀察控制檯發請求的數據:
能夠看到這是一種區別於用&鏈接參數的方式,它的編碼格式是multipart/form-data,就是上傳文件form表單寫的enctype:
<form enctype="multipart/form-data" method="post"> <input type="file" name="fileContent"> </form>複製代碼
若是xhr.send的是FormData類型話,它會自動設置enctype,若是你用默認表單提交上傳文件的話就得在form上面設置這個屬性,由於上傳文件只能使用POST的這種編碼。經常使用的POST編碼是application/x-www-form-urlencoded,它和GET同樣,發送的數據裏面,參數和參數之間使用&鏈接,如:
key1=value1&key2=value2
特殊字符作轉義,這個數據POST是放在請求body裏的,而GET是拼在url上面的,若是用jq的話,jq會幫你拼並作轉義。
而上傳文件用的這種multipart/form-data,參數和參數之間是且一個相同的字符串隔開的,上面的是使用:
------WebKitFormBoundary72yvM25iSPYZ4a3F
這個字符一般會取得比較長、比較隨機,由於要保證正常的內容裏面不會出現這個字符串,這樣內容的特殊字符就不用作轉義了。
請求的contentType被瀏覽器設置成:
Content-Type:multipart/form-data; boundary=----WebKitFormBoundary72yvM25iSPYZ4a3F
後端服務經過這個就知道怎麼解析這麼一段數據了。(一般是使用的框架處理了,而具體的接口不須要關心應該怎麼解析)
若是讀取結果是ArrayBuffer的話,也是能夠直接用xhr.send發送出去的,可是通常咱們不會直接把一個文件的內容發出去,而是用某個字段名等於文件內容的方式。若是你讀取爲ArrayBuffer的話再上傳的話其實做用不是很大,還不如直接用formData添加一個File對象的內容,由於上面三種方式均可以拿到File對象。若是一開始就是一個ArrayBuffer了,那麼能夠轉成blob而後再append到FormData裏面。
使用比較多的應該是base64,由於前端常常要處理圖片,讀取爲base64以後就能夠把它畫到一個canvas裏面,而後就能夠作一些處理,如壓縮、裁剪、旋轉等。最後再用canvas導出一個base64格式的圖片,那怎麼上傳base64格式的呢?
第一種是拼一個表單上傳的multipart/form-data的格式,再用xhr.sendAsBinary發出去,以下代碼:
let base64Data = base64Data.replace(/^data:image\/[^;]+;base64,/, ""); 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還原成原始內容的字符串表示,以下圖所示:
btoa是把內容轉化成base64編碼,而atob是把base64還原。在調atob以前,須要把表示內容格式的不屬於base64內容的字符串去掉,即上面代碼第一行的replace處理。
這樣就和使用formData相似了,可是因爲sendAsBinary已經被deprecated了,因此新代碼不建議再使用這種方式。那怎麼辦呢?
能夠把base64轉化成blob,而後再append到一個formData裏面,下面的函數(來自b64-to-blob)能夠把base64轉成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的格式數據了。
上面處理和上傳文件的API能夠兼容到IE10+,若是要兼容老的瀏覽器應該怎麼辦呢?
能夠藉助一個iframe,原理是默認的form表單提交會刷新頁面,或者跳到target指定的那個url,可是若是把ifrmae的target指向一個iframe,那麼刷新的就是iframe,返回結果也會顯示在ifame,而後獲取這個ifrmae的內容就可獲得上傳接口返回的結果。
以下代碼:
let iframe = document.createElement("iframe"); iframe.display = "none"; iframe.name = "form-iframe"; document.body.appendChild(iframe); // 改變form的target form.target = "form-iframe"; iframe.onload = function() { //獲取iframe的內容,即服務返回的數據 let responseText = this.contentDocument.body.textContent || this.contentWindow.document.body.textContent; }; form.submit();複製代碼
form.submit會觸發表單提交,當請求完成(成功或者失敗)以後就會觸發iframe的onload事件,而後在onload事件獲取返回的數據,若是請求失敗了的話,iframe裏的內容就爲空,能夠用這個判斷請求有沒有成功。
使用iframe沒有辦法獲取上傳進度,使用xhr能夠獲取當前上傳的進度,這個是在XMLHttpRequest 2.0引入的:
xhr.upload.onprogress = function (event) { if (event.lengthComputable) { // 當前上傳進度的百分比 duringCallback ((event.loaded / event.total)*100); } }; 複製代碼
這樣就能夠作一個真實的loading進度條。
本文討論了3種交互方式的讀取方式,經過input控件在input.files能夠獲得File文件對象,經過拖拽的是在drop事件的event.dataTransfer.files裏面,而經過粘貼的paste事件在event.clipboardData.files裏面,Safari這個怪胎是在編輯器裏面插入一個src指向本地的img標籤,能夠經過發送一個請求加載本地的blob數據,而後再經過FileReader讀取,或者直接append到formData裏面。獲得的File對象就能夠直接添加到FormData裏面,若是須要先讀取base64格式作處理的,那麼能夠把處理後的base64轉化爲blob數據再append到formData裏面。對於老瀏覽器,可使用一個iframe解決表單提交刷新頁面或者跳頁的問題。
總之,前端處理和上傳本地文件應該差很少就是這些內容了,可是應該還有好多細節沒有說起到,讀者可經過本文列的方向自行實踐。若是有其它的上傳方式還請告知。