前端本地文件操作與上傳

前端無法像原生APP一樣直接操作本地文件谊迄,否則的話打開個網(wǎng)頁就能把用戶電腦上的文件偷光了拇派,所以需要通過用戶觸發(fā)电爹,用戶可通過以下三種方式操作觸發(fā):

  1. 通過input type=”file” 選擇本地文件

  2. 通過拖拽的方式把文件拖過來

  3. 在編輯框里面復(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打印出來是這樣的:

image

可以看到文件的路徑是一個假的路徑,也就是說在瀏覽器無法獲取到文件的真實存放位置元咙。同時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對象打印出來是這樣的:

image

它是一個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打印出來是這樣的:

image

可以看到弛槐,它對前端開發(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>

這將在頁面顯示一個框:

image

然后監(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傳遞的宅此,它是直接在輸入框里面添加一張圖片,如下圖所示:

image

它新建了一個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打印出來是這樣的:

image

能得到它的大小和類型枝嘶,但是具體內(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:

image

這種數(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)容稍浆,最后得到:

  1. FormData格式

  2. 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ù):

image

可以看到這是一種區(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)容的字符串表示盈咳,如下圖所示:

image

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é)沒有提及到惕稻,讀者可通過本文列的方向自行實踐竖共。如果有其它的上傳方式還請告知。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俺祠,一起剝皮案震驚了整個濱河市公给,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜘渣,老刑警劉巖妓布,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宋梧,居然都是意外死亡,警方通過查閱死者的電腦和手機狰挡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門捂龄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來释涛,“玉大人,你說我怎么就攤上這事倦沧〈角耍” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵展融,是天一觀的道長窖认。 經(jīng)常有香客問我,道長告希,這世上最難降的妖魔是什么扑浸? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮燕偶,結(jié)果婚禮上喝噪,老公的妹妹穿的比我還像新娘。我一直安慰自己指么,他們只是感情好酝惧,可當(dāng)我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著伯诬,像睡著了一般晚唇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盗似,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天哩陕,我揣著相機與錄音,去河邊找鬼桥言。 笑死萌踱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的号阿。 我是一名探鬼主播并鸵,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼扔涧!你這毒婦竟也來了园担?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤枯夜,失蹤者是張志新(化名)和其女友劉穎弯汰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湖雹,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡咏闪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了摔吏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸽嫂。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡纵装,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出据某,到底是詐尸還是另有隱情橡娄,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布癣籽,位于F島的核電站挽唉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏筷狼。R本人自食惡果不足惜瓶籽,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桑逝。 院中可真熱鬧棘劣,春花似錦、人聲如沸楞遏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寡喝。三九已至糙俗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間预鬓,已是汗流浹背巧骚。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留格二,地道東北人劈彪。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像顶猜,于是被迫代替她去往敵國和親沧奴。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前端無法像原生APP一樣直接操作本地文件长窄,否則的話打開個網(wǎng)頁就能把用戶電腦上的文件偷光了滔吠,所以需要通過用戶觸發(fā),用...
    雷波_viho閱讀 823評論 0 1
  • 文件操作一直是早期瀏覽器的痛點挠日,全封閉式的不給JavaScript操作的空間疮绷。隨著H5新接口的推出這個壁壘被打破了...
    JunChow520閱讀 1,482評論 0 2
  • 在javascript的世界里無法處理二進制數(shù)據(jù),如果需要處理嚣潜,只能使用charCodeAt()方法冬骚,一個個字節(jié)地...
    我是上帝可愛多閱讀 609評論 0 4
  • 參考1-HTML5實現(xiàn)圖片壓縮上傳功能參考2-移動前端—圖片壓縮上傳實踐參考3-移動端H5圖片壓縮上傳 大體步驟 ...
  • 前幾天剛剛寫過一篇題為:“我命由我不由天”的文章,其中我引用過大澤鄉(xiāng)陳勝吳廣起義時說的名言“王侯將相寧有種乎”...
    Richardzxb閱讀 592評論 3 3