標籤:https 設定 val java檔案上傳 append plain models 瀏覽器 響應
本文轉載自 檔案上傳與 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(檔案API)為Web 開發人員提供一種安全的方式來訪問使用者電腦中的檔案,並更好地對這些檔案執行操作。
具體來講,File API 在表單中的檔案輸入欄位的基礎上,又添加了一些直接存取檔案資訊的介面。HTML5 在 DOM 中為檔案輸入元素添加了一個 files 集合。在通過檔案輸入欄位選擇了一或多個檔案時,files 集合中將包含一組 File 對象,每個 File 對象對應著一個檔案。
構造一個檔案上傳的表單,通過如下 jQuery 代碼:
$("input[type=‘file‘]")[0].files
在 chrome 瀏覽器控制台中可以看到獲得的資訊如下:
可以看到選取的檔案 testfile.txt 的相關資訊,因此可以通過上述方式來獲得上傳的檔案。
關於 File API 的更多敘述可以在這裡獲得。
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檔案上傳