Java文件上傳
本文轉載自 文件上傳與 Angular
最近項目需要使用 Angular,對於初學 Angular 的我只能硬著頭皮上了,項目中有一個需求是文件上傳,磕磕絆絆之下也實現了,將實現過程中學習到的一些知識記錄下來以備將來查閱。
與表單數據編碼相關的知識
通常,我們使用 HTML 的標簽 <form>
來為用戶輸入創建一個表單,使用 <input type="file">
作為文件上傳的控件。
要將表單的數據發送給後臺,不僅要通過指定 <form>
的屬性 method
來確定發送數據的 HTTP 方法而且需要通過指定 <form>
的屬性 enctype
下面對這兩個屬性進行簡單說明。
表單 form 的屬性 method
<form>
的屬性 method
規定用於發送 form-data 的 HTTP 方法,其值可以為 get
或者 post
。get
請求會將表單的數據編碼後以 name1=value1&name2=value2
的形式附加到請求的 url 後面進行發送。post
請求會將表單的數據進行編碼之後置於請求體中進行發送。
本文接下來的討論主要基於 post
請求方式。
表單 form 的屬性 enctype
<form>
標簽的屬性 entype
用來規定在發送表單數據之前應該如何對其進行編碼,其實就是用來指定請求的編碼類型。
enctype
屬性有 3 個取值,在 w3school 中對於其取值的描述如下:
取值 | 描述 |
---|---|
application/x-www-form-urlencoded | 空格轉換為 "+" 加號,特殊符號轉換為 ASCII HEX 值 |
multipart/form-data | 不對字符編碼。在使用包含文件上傳控件的表單時,必須使用該值 |
text/plain | 空格轉換為 "+" 加號,但不對特殊字符編碼 |
其中 application/x-www-form-urlencoded
是默認采用的編碼的方式,如果表單 <form>
中有用到文件上傳的控件,就要手動指定編碼為 multipart/form-data
下面分別對上述這幾種編碼方式進行舉例(均基於 post
請求方式)
- 編碼為
application/x-www-form-urlencoded
的情況
首先,構造一個表單:
<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>
在輸入框內分別輸入 i‘m name1
和 [email protected]
,根據編碼規則,提交表單的時候,表單數據會被編碼成 name1=i%27m+name1&name2=name%402
置於請求體中進行傳遞,在 chrome
瀏覽器中執行結果也正如預期所示。
以
application/x-www-form-urlencoded
編碼來發送的表單數據
- 編碼為
multipart/form-data
的情況
編碼為 multipart/form-data
的情況又有所不同,先來看看示例代碼的結果。
示例代碼:
<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>
在輸入框內分別輸入 i‘m name1
和 [email protected]
,再選擇一個名為 testfile.txt 的文件上傳,可以在 chrome
中看到發送的請求如下:
以
multipart/form-data
編碼來發送的表單數據
註意圖片中的紅框部分,Content-Type
值為 multipart/form-data; boundary=----WebKitFormBoundaryBdpfgMg4VKAZat6C
,其中多了一個叫做 boundary
的字段,它是由瀏覽器隨機生成的一個字符串,作為表單數據的分割邊界來使用的,在服務器端會根據這個 boundary
邊界字段來解析表單數據。
可以明顯看到,以邊界分割的每一段均對應於一項表單數據,每項數據均包含有一個 Content-Disposition
字段和一個 name
字段,而對於上傳的文件則會多一個指定上傳文件名字的 filename
的屬性和上傳文件的類型的 Content-Type
字段,由於例子中上傳的文件是 .txt
格式的文件,因此 Content-Type
的值為 text/plain
,有關文件的擴展名和 Content-Type
的對照表可以看這裏。
- 編碼為
text/plain
的情況
這種情況與編碼為application/x-www-form-urlencoded
的情況類似,唯一的差別就在於text/plain
不對特殊字符進行編碼。
文件上傳的 Angular 實現
基於 FormData
的實現
實現的思路:通過 File API
獲取控件中上傳的文件,利用 FormData
類型構造表單數據上傳。
基本知識:File API
和 FormData 類型
File API
File API
(文件API)為Web 開發人員提供一種安全的方式來訪問用戶計算機中的文件,並更好地對這些文件執行操作。
具體來講,File API
在表單中的文件輸入字段的基礎上,又添加了一些直接訪問文件信息的接口。HTML5
在 DOM
中為文件輸入元素添加了一個 files
集合。在通過文件輸入字段選擇了一或多個文件時,files
集合中將包含一組 File
對象,每個 File
對象對應著一個文件。
構造一個文件上傳的表單,通過如下 jQuery
代碼:
$("input[type=‘file‘]")[0].files
在 chrome
瀏覽器控制臺中可以看到獲得的信息如下:
可以看到選取的文件 testfile.txt
的相關信息,因此可以通過上述方式來獲得上傳的文件。
關於 File API
的更多敘述可以在這裏獲得。
FormData
類型
FormData
是在 XMLHttpRequest Level 2
中定義的,為序列化表單以及創建與表單格式相同的數據(用於通過XHR 傳輸)提供了便利。
下面這段對於 FormData
對象的描述引用自 MDN,更多關於 FormData
類型的敘述可以在這裏獲得。
XMLHttpRequest Level 2 添加了一個新的接口 FormData. 利用FormData 對象,我們可以通過 JavaScript 用一些鍵值對來模擬一系列表單控件,我們還可以使用 XMLHttpRequest 的 send() 方法來異步的提交這個"表單". 比起普通的 ajax, 使用 FormData 的最大優點就是我們可以異步上傳一個二進制文件.
可見,我們可以使用 FormData
對象來模擬實現文件上傳時候提交的表單數據,而構造提交的數據是通過 FormData
的方法 append()
實現的,它用於給當前 FormData
對象添加一個鍵/值對。
Angular 實現
有了上面所說的實現思路和基礎知識,現在可以著手進行代碼的實現了。
- 首先,編寫一個指令用來獲取上傳文件的
File
對象。
代碼如下:
.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 );
} )
} )
}
}
}])
這個指令的使用方式如下:
<input type="file" file-model="fileToUpload">
對於 <input>
元素,在它們失去焦點且 value 值改變時會觸發 change
事件,因此我們在指令的 link
函數中監聽元素上的 change
事件,在事件響應函數中獲取用戶上傳的文件信息,並且將該文件賦值給 $scope
對象中與指令 fileModel
綁定的屬性(上例中為 fileToUpload
)。
可以運行例子中的代碼,選擇一個文件 filetest.txt
,打印出賦值後的 $scope
對象如下:
將獲取的上傳文件賦給
$scope
對象
如紅框所示,$scope 的屬性 fileToUpload
即是上傳的文件 filetest.txt
的信息。
- 然後,編寫一個服務用於發送上傳文件的
multipart/form-data
請求。
代碼如下:
.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...
})
}
}])
在服務 fileUpload
的方法 uploadFileToUrl
中,通過 FormData
的 append()
方法將上傳的文件序列化為表單數據,然後通過 $http.post()
方法發送給後臺。
Angular 默認的 transformRequest
方法會嘗試序列化我們的 FormData
對象,因此此處我們使用 angular.identity
函數來覆蓋它;另外,angular 在發送 POST 請求的時候使用的默認 Content-Type
是 application/json
,因此此處需要調整為 undefined
,這時瀏覽器會自動的幫我們設置成 multipart/form-data
的編碼方式,同時還會生成一個合適的 boundary
,如果手動設置成 multipart/form-data
的話就不會生成 boundary
字段了。
- 最後,在控制器的合適地方發送這個請求。
現在我們已經獲得了上傳的文件的相關信息,也有一個用於發送該文件的服務,那麽只要在控制器中定義一個用於發送的函數,然後在合適的時機調用它即可將文件上傳到後臺去了。
舉個例子,在控制器的 $scope
裏面定義一個發送請求的函數 sendFile
:
.controller( "myCtrl", [ "$scope", "fileUpload", function( $scope, fileUpload ){
$scope.sendFile = function(){
var url = "/server",
file = $scope.fileToUpload;
if ( !file ) return;
fileUpload.uploadFileToUrl( file, url );
}
}])
然後我們可以定義一個按鈕,當用戶點擊這個按鈕的時候就會將上傳的文件發送出去。
<button type="button" ng-click="sendFile()">Submit</button>
結果是這樣的:
通過
FormData
上傳文件的請求
兼容性
由於 FormData
只兼容 IE10+ ,因此上述方法也只是在 IE10+ 中可以使用。
如果你的應用需要兼容 IE8 ,老老實實封裝一個含有 iframe 的指令即可,請接著往下看。
含有 iframe 的實現
指令代碼如下
.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...
}
}
}])
調用方式大概是這樣的:
<div iframe-file-upload></div>
Java文件上傳