文件上傳與 Angular

最近項(xiàng)目需要使用 Angular,對(duì)于初學(xué) Angular 的我只能硬著頭皮上了拥诡,項(xiàng)目中有一個(gè)需求是文件上傳择克,磕磕絆絆之下也實(shí)現(xiàn)了,將實(shí)現(xiàn)過程中學(xué)習(xí)到的一些知識(shí)記錄下來以備將來查閱驼修。

與表單數(shù)據(jù)編碼相關(guān)的知識(shí)


通常,我們使用 HTML 的標(biāo)簽 <form> 來為用戶輸入創(chuàng)建一個(gè)表單诈铛,使用 <input type="file"> 作為文件上傳的控件乙各。

要將表單的數(shù)據(jù)發(fā)送給后臺(tái),不僅要通過指定 <form> 的屬性 method 來確定發(fā)送數(shù)據(jù)的 HTTP 方法而且需要通過指定 <form> 的屬性 enctype 來確定對(duì)發(fā)送數(shù)據(jù)的編碼方式幢竹。

下面對(duì)這兩個(gè)屬性進(jìn)行簡(jiǎn)單說明耳峦。

表單 form 的屬性 method

<form> 的屬性 method 規(guī)定用于發(fā)送 form-data 的 HTTP 方法,其值可以為 get 或者 post焕毫。get 請(qǐng)求會(huì)將表單的數(shù)據(jù)編碼后以 name1=value1&name2=value2 的形式附加到請(qǐng)求的 url 后面進(jìn)行發(fā)送蹲坷。post 請(qǐng)求會(huì)將表單的數(shù)據(jù)進(jìn)行編碼之后置于請(qǐng)求體中進(jìn)行發(fā)送驶乾。

本文接下來的討論主要基于 post 請(qǐng)求方式。

表單 form 的屬性 enctype

<form> 標(biāo)簽的屬性 entype 用來規(guī)定在發(fā)送表單數(shù)據(jù)之前應(yīng)該如何對(duì)其進(jìn)行編碼循签,其實(shí)就是用來指定請(qǐng)求的編碼類型级乐。

enctype屬性有 3 個(gè)取值,在 w3school 中對(duì)于其取值的描述如下:

取值 描述
application/x-www-form-urlencoded 空格轉(zhuǎn)換為 "+" 加號(hào)县匠,特殊符號(hào)轉(zhuǎn)換為 ASCII HEX 值
multipart/form-data 不對(duì)字符編碼风科。在使用包含文件上傳控件的表單時(shí),必須使用該值
text/plain 空格轉(zhuǎn)換為 "+" 加號(hào)聚唐,但不對(duì)特殊字符編碼

其中 application/x-www-form-urlencoded 是默認(rèn)采用的編碼的方式丐重,如果表單 <form> 中有用到文件上傳的控件,就要手動(dòng)指定編碼為 multipart/form-data杆查。

下面分別對(duì)上述這幾種編碼方式進(jìn)行舉例(均基于 post 請(qǐng)求方式)

  • 編碼為 application/x-www-form-urlencoded 的情況

首先扮惦,構(gòu)造一個(gè)表單:

<form method="post" action="/" enctype="application/x-www-form-urlencoded">
  <input type="text" name="name1" placeholder="name1">
  <input type="text" name="name2" placeholder="name2">
  <input type="submit">
</form>

在輸入框內(nèi)分別輸入 i'm name1name@2 ,根據(jù)編碼規(guī)則亲桦,提交表單的時(shí)候崖蜜,表單數(shù)據(jù)會(huì)被編碼成 name1=i%27m+name1&name2=name%402 置于請(qǐng)求體中進(jìn)行傳遞,在 chrome 瀏覽器中執(zhí)行結(jié)果也正如預(yù)期所示客峭。

以 `application/x-www-form-urlencoded` 編碼來發(fā)送的表單數(shù)據(jù)
  • 編碼為 multipart/form-data 的情況

編碼為 multipart/form-data 的情況又有所不同豫领,先來看看示例代碼的結(jié)果。

示例代碼:

<form method="post" action="/" enctype="multipart/form-data">
  <input type="text" name="name1" placeholder="name1">
  <input type="text" name="name2" placeholder="name2">
  <input type="file" name="inputfile">
  <input type="submit">
</form>

在輸入框內(nèi)分別輸入 i'm name1name@2 舔琅,再選擇一個(gè)名為 testfile.txt 的文件上傳等恐,可以在 chrome 中看到發(fā)送的請(qǐng)求如下:

以 `multipart/form-data` 編碼來發(fā)送的表單數(shù)據(jù)

注意圖片中的紅框部分,Content-Type 值為 multipart/form-data; boundary=----WebKitFormBoundaryBdpfgMg4VKAZat6C 备蚓,其中多了一個(gè)叫做 boundary 的字段课蔬,它是由瀏覽器隨機(jī)生成的一個(gè)字符串,作為表單數(shù)據(jù)的分割邊界來使用的郊尝,在服務(wù)器端會(huì)根據(jù)這個(gè) boundary 邊界字段來解析表單數(shù)據(jù)二跋。

可以明顯看到,以邊界分割的每一段均對(duì)應(yīng)于一項(xiàng)表單數(shù)據(jù)流昏,每項(xiàng)數(shù)據(jù)均包含有一個(gè) Content-Disposition 字段和一個(gè) name 字段扎即,而對(duì)于上傳的文件則會(huì)多一個(gè)指定上傳文件名字的 filename 的屬性和上傳文件的類型的 Content-Type 字段,由于例子中上傳的文件是 .txt 格式的文件况凉,因此 Content-Type 的值為 text/plain谚鄙,有關(guān)文件的擴(kuò)展名和 Content-Type 的對(duì)照表可以看這里

  • 編碼為 text/plain 的情況
    這種情況與編碼為 application/x-www-form-urlencoded 的情況類似刁绒,唯一的差別就在于 text/plain 不對(duì)特殊字符進(jìn)行編碼襟锐。

文件上傳的 Angular 實(shí)現(xiàn)


基于 FormData 的實(shí)現(xiàn)

實(shí)現(xiàn)的思路:通過 File API 獲取控件中上傳的文件,利用 FormData 類型構(gòu)造表單數(shù)據(jù)上傳膛锭。

基本知識(shí):File APIFormData 類型
  • File API

File API(文件API)為Web 開發(fā)人員提供一種安全的方式來訪問用戶計(jì)算機(jī)中的文件粮坞,并更好地對(duì)這些文件執(zhí)行操作。

具體來講初狰,File API 在表單中的文件輸入字段的基礎(chǔ)上莫杈,又添加了一些直接訪問文件信息的接口。HTML5DOM 中為文件輸入元素添加了一個(gè) files 集合奢入。在通過文件輸入字段選擇了一或多個(gè)文件時(shí)筝闹,files 集合中將包含一組 File 對(duì)象,每個(gè) File 對(duì)象對(duì)應(yīng)著一個(gè)文件腥光。

構(gòu)造一個(gè)文件上傳的表單关顷,通過如下 jQuery 代碼:

$("input[type='file']")[0].files

chrome 瀏覽器控制臺(tái)中可以看到獲得的信息如下:

可以看到選取的文件 testfile.txt 的相關(guān)信息,因此可以通過上述方式來獲得上傳的文件武福。

關(guān)于 File API 的更多敘述可以在這里獲得议双。

  • FormData 類型

FormData 是在 XMLHttpRequest Level 2 中定義的,為序列化表單以及創(chuàng)建與表單格式相同的數(shù)據(jù)(用于通過XHR 傳輸)提供了便利捉片。

下面這段對(duì)于 FormData 對(duì)象的描述引用自 MDN平痰,更多關(guān)于 FormData 類型的敘述可以在這里獲得。

XMLHttpRequest Level 2 添加了一個(gè)新的接口 FormData. 利用FormData 對(duì)象伍纫,我們可以通過 JavaScript 用一些鍵值對(duì)來模擬一系列表單控件宗雇,我們還可以使用 XMLHttpRequest 的 send() 方法來異步的提交這個(gè)"表單". 比起普通的 ajax, 使用 FormData 的最大優(yōu)點(diǎn)就是我們可以異步上傳一個(gè)二進(jìn)制文件.

可見,我們可以使用 FormData 對(duì)象來模擬實(shí)現(xiàn)文件上傳時(shí)候提交的表單數(shù)據(jù)莹规,而構(gòu)造提交的數(shù)據(jù)是通過 FormData 的方法 append() 實(shí)現(xiàn)的赔蒲,它用于給當(dāng)前 FormData 對(duì)象添加一個(gè)鍵/值對(duì)。

Angular 實(shí)現(xiàn)

有了上面所說的實(shí)現(xiàn)思路和基礎(chǔ)知識(shí)良漱,現(xiàn)在可以著手進(jìn)行代碼的實(shí)現(xiàn)了舞虱。

  • 首先,編寫一個(gè)指令用來獲取上傳文件的 File 對(duì)象债热。

代碼如下:

.directive( "fileModel", [ "$parse", function( $parse ){
  return {
    restrict: "A",
    link: function( scope, element, attrs ){
      var model = $parse( attrs.fileModel );
      var modelSetter = model.assign;

      element.bind( "change", function(){
        scope.$apply( function(){
          modelSetter( scope, element[0].files[0] );
          // console.log( scope );
        } )
      } )
    }
  }
}])

這個(gè)指令的使用方式如下:

<input type="file" file-model="fileToUpload">

對(duì)于 <input> 元素砾嫉,在它們失去焦點(diǎn)且 value 值改變時(shí)會(huì)觸發(fā) change 事件,因此我們?cè)谥噶畹?link 函數(shù)中監(jiān)聽元素上的 change 事件窒篱,在事件響應(yīng)函數(shù)中獲取用戶上傳的文件信息焕刮,并且將該文件賦值給 $scope 對(duì)象中與指令 fileModel 綁定的屬性(上例中為 fileToUpload)。

可以運(yùn)行例子中的代碼墙杯,選擇一個(gè)文件 filetest.txt配并,打印出賦值后的 $scope 對(duì)象如下:

將獲取的上傳文件賦給 `$scope` 對(duì)象

如紅框所示,$scope 的屬性 fileToUpload 即是上傳的文件 filetest.txt 的信息高镐。

  • 然后溉旋,編寫一個(gè)服務(wù)用于發(fā)送上傳文件的 multipart/form-data 請(qǐng)求。

代碼如下:

.service( "fileUpload", ["$http", function( $http ){
  this.uploadFileToUrl = function( file, uploadUrl ){
    var fd = new FormData();
    fd.append( "file", file )
    $http.post( uploadUrl, fd, {
      transformRequest: angular.identity,
      headers: { "Content-Type": undefined }
    })
    .success(function(){
      // blabla...
    })
    .error( function(){
      // blabla...
    })
  }
}])

在服務(wù) fileUpload 的方法 uploadFileToUrl 中嫉髓,通過 FormDataappend() 方法將上傳的文件序列化為表單數(shù)據(jù)观腊,然后通過 $http.post() 方法發(fā)送給后臺(tái)邑闲。

Angular 默認(rèn)的 transformRequest 方法會(huì)嘗試序列化我們的 FormData 對(duì)象,因此此處我們使用 angular.identity 函數(shù)來覆蓋它梧油;另外苫耸,angular 在發(fā)送 POST 請(qǐng)求的時(shí)候使用的默認(rèn) Content-Typeapplication/json,因此此處需要調(diào)整為 undefined儡陨,這時(shí)瀏覽器會(huì)自動(dòng)的幫我們?cè)O(shè)置成 multipart/form-data 的編碼方式褪子,同時(shí)還會(huì)生成一個(gè)合適的 boundary,如果手動(dòng)設(shè)置成 multipart/form-data 的話就不會(huì)生成 boundary 字段了骗村。

  • 最后嫌褪,在控制器的合適地方發(fā)送這個(gè)請(qǐng)求。

現(xiàn)在我們已經(jīng)獲得了上傳的文件的相關(guān)信息胚股,也有一個(gè)用于發(fā)送該文件的服務(wù)笼痛,那么只要在控制器中定義一個(gè)用于發(fā)送的函數(shù),然后在合適的時(shí)機(jī)調(diào)用它即可將文件上傳到后臺(tái)去了信轿。

舉個(gè)例子晃痴,在控制器的 $scope 里面定義一個(gè)發(fā)送請(qǐng)求的函數(shù) sendFile

.controller( "myCtrl", [ "$scope", "fileUpload", function( $scope, fileUpload ){
  $scope.sendFile = function(){
    var url = "/server",
        file = $scope.fileToUpload;
    if ( !file ) return;
    fileUpload.uploadFileToUrl( file, url );
  }
}])

然后我們可以定義一個(gè)按鈕,當(dāng)用戶點(diǎn)擊這個(gè)按鈕的時(shí)候就會(huì)將上傳的文件發(fā)送出去财忽。

<button type="button" ng-click="sendFile()">Submit</button>

結(jié)果是這樣的:

通過 `FormData` 上傳文件的請(qǐng)求
兼容性

由于 FormData 只兼容 IE10+ 倘核,因此上述方法也只是在 IE10+ 中可以使用。

如果你的應(yīng)用需要兼容 IE8 即彪,老老實(shí)實(shí)封裝一個(gè)含有 iframe 的指令即可紧唱,請(qǐng)接著往下看。

含有 iframe 的實(shí)現(xiàn)

指令代碼如下

.directive( "iframeFileUpload", [function(){
  var inner = "<div>";
      inner +=    "<form action=\"/server\" method=\"post\" enctype=\"multipart/form-data\" target=\"uploadIframe\">";
      inner +=        "<input type=\"file\" name=\"filename\">";
      inner +=        "<input type=\"submit\">";
      inner +=      "</form>";
      inner +=      "<iframe id=\"uploadIframe\" name=\"uploadIframe\" style=\"display:none\"></iframe>";
      inner +=    "</div>";
  return{
    restrict: "A",
    template: inner,
    // or
    // templateUrl: "components/iframeFileUpload.html",
    replace: true,
    scope: {},
    link: function( scope, element, attrs ){
      // blabla...
    }
  }
}])

調(diào)用方式大概是這樣的:

<div iframe-file-upload></div>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末隶校,一起剝皮案震驚了整個(gè)濱河市漏益,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌深胳,老刑警劉巖绰疤,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異舞终,居然都是意外死亡轻庆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門敛劝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來余爆,“玉大人,你說我怎么就攤上這事夸盟《攴剑” “怎么了?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長桩砰。 經(jīng)常有香客問我拓春,道長,這世上最難降的妖魔是什么五芝? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任痘儡,我火速辦了婚禮,結(jié)果婚禮上枢步,老公的妹妹穿的比我還像新娘。我一直安慰自己渐尿,他們只是感情好醉途,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著砖茸,像睡著了一般隘擎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凉夯,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天货葬,我揣著相機(jī)與錄音,去河邊找鬼劲够。 笑死震桶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的征绎。 我是一名探鬼主播蹲姐,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼人柿!你這毒婦竟也來了柴墩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤凫岖,失蹤者是張志新(化名)和其女友劉穎江咳,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哥放,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歼指,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了婶芭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片东臀。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖犀农,靈堂內(nèi)的尸體忽然破棺而出惰赋,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布赁濒,位于F島的核電站轨奄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拒炎。R本人自食惡果不足惜挪拟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望击你。 院中可真熱鬧玉组,春花似錦、人聲如沸丁侄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸿摇。三九已至石景,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間拙吉,已是汗流浹背潮孽。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留筷黔,地道東北人往史。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像必逆,于是被迫代替她去往敵國和親怠堪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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