最近項(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 name1
和 name@2
,根據(jù)編碼規(guī)則亲桦,提交表單的時(shí)候崖蜜,表單數(shù)據(jù)會(huì)被編碼成 name1=i%27m+name1&name2=name%402
置于請(qǐng)求體中進(jìn)行傳遞,在 chrome
瀏覽器中執(zhí)行結(jié)果也正如預(yù)期所示客峭。
- 編碼為
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 name1
和 name@2
舔琅,再選擇一個(gè)名為 testfile.txt 的文件上傳等恐,可以在 chrome
中看到發(fā)送的請(qǐng)求如下:
注意圖片中的紅框部分,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 API
和 FormData 類型
File API
File API
(文件API)為Web 開發(fā)人員提供一種安全的方式來訪問用戶計(jì)算機(jī)中的文件粮坞,并更好地對(duì)這些文件執(zhí)行操作。
具體來講初狰,File API
在表單中的文件輸入字段的基礎(chǔ)上莫杈,又添加了一些直接訪問文件信息的接口。HTML5
在 DOM
中為文件輸入元素添加了一個(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 的屬性 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
中嫉髓,通過 FormData
的 append()
方法將上傳的文件序列化為表單數(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-Type
是 application/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
只兼容 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>