1. 程式人生 > >Java文件上傳

Java文件上傳

鍵值 literal change 就會 9.png 知識 選擇 輸入框 school

本文轉載自 文件上傳與 Angular

最近項目需要使用 Angular,對於初學 Angular 的我只能硬著頭皮上了,項目中有一個需求是文件上傳,磕磕絆絆之下也實現了,將實現過程中學習到的一些知識記錄下來以備將來查閱。

與表單數據編碼相關的知識


通常,我們使用 HTML 的標簽 <form> 來為用戶輸入創建一個表單,使用 <input type="file"> 作為文件上傳的控件。

要將表單的數據發送給後臺,不僅要通過指定 <form> 的屬性 method 來確定發送數據的 HTTP 方法而且需要通過指定 <form> 的屬性 enctype

來確定對發送數據的編碼方式。

下面對這兩個屬性進行簡單說明。

表單 form 的屬性 method

<form> 的屬性 method 規定用於發送 form-data 的 HTTP 方法,其值可以為 get 或者 postget 請求會將表單的數據編碼後以 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 APIFormData 類型
  • File API

File API(文件API)為Web 開發人員提供一種安全的方式來訪問用戶計算機中的文件,並更好地對這些文件執行操作。

具體來講,File API 在表單中的文件輸入字段的基礎上,又添加了一些直接訪問文件信息的接口。HTML5DOM 中為文件輸入元素添加了一個 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 中,通過 FormDataappend() 方法將上傳的文件序列化為表單數據,然後通過 $http.post() 方法發送給後臺。

Angular 默認的 transformRequest 方法會嘗試序列化我們的 FormData 對象,因此此處我們使用 angular.identity 函數來覆蓋它;另外,angular 在發送 POST 請求的時候使用的默認 Content-Typeapplication/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文件上傳