2016年8月20日 星期六

使用Ajax上傳檔案 - (Javascript, JQuery or AngularJS) + FormData

在以前我們要在網頁中傳送檔案資料時,通常會需要設計一個html Form,並設定Form的encype=multipart/form-data和準備一個input type="file",在User按下submit按鈕後,跟據Form
所設定的action="URL",將整個頁面request移動到action所指定的地方,例如Servlet,
等處理完後在將User導至其他網頁頁面。

但有沒有可能不要讓User被導到其他頁面,留在原頁面就完成檔案資訊的傳送呢?
答案是可以的,這邊就要利用新的javascript類別FormData和Ajax的技術來達到Ajax傳
送檔案(也可順便傳遞其他input資料)資料。


在這邊我們要利用Netbeans、Servlet3.0、Tomcat 8來實做我們的範例,
實現了三個版本的檔案上傳:Javascript、JQuery、AngularJS
首頁是專案檔案結構,如下圖:

所用的版本JQuery為v2.0.3、AngularJS為v1.5.8,
主要的重要檔案有:

  1. fileUploadAjaxExample.html
    給User上傳檔案的html網頁。
  2. FileUploadAjax.js
    處理檔案資料上傳的Javascript File.
    包含Javascript、JQuery、AngularJS三個版本。
  3. FileUploadAction.java
    用來接收檔案資料的servlet,這裡設定URL patter為/fileUpload.do
  4. web.xml
    設定檔,其中必須在要接收檔案的servlet中設定的tag,
    tag裡可以有以下的子tag設定:
    <location>         檔案存放位置 (使用Part.write(fileName)可以在寫入檔案,但如果fileName
                               為絕對路徑則以絕對路徑為準)
    <max-file-size>  最大檔案size
    <max-request-size>   最大request size  (例如POST的request size)
    <file-size-threshold>
       超過file-size-threshold的檔案request將會以臨時暫存的方式存到硬                                       碟中,預設為0
接下來我以來各別看下每個實作檔案的內容:



  1. fileUploadAjaxExample.html
    <!DOCTYPE html>
    <html>
        <head>
            <title>File upload ajax example</title>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
    
            <script src="js/libs/angular.js/angular.js"></script>
            <script src="js/libs/jquery/jquery.js"></script>
    
            <script src="js/FileUploadAjax.js"></script>
        </head>
        <body>
            Javascript版:
            <form id="form_javascript" onsubmit="submitForm_javascript(); return false;" action="#">
                <input name="fileDescription" type="text" required/>
                <input name="fileData" type="file" required/>
                <input type="submit" />
            </form>
            JQuery版:
            <form id="form_jquery" onsubmit="submitForm_jquery(); return false;" action="#">
                <input name="fileDescription" type="text" required/>
                <input name="fileData" type="file" required/>
                <input type="submit" />
            </form>
            AngularJS版:
            <div ng-app="myApp" ng-controller="myController as myCtrl">
                <form ng-submit="myCtrl.submitForm()">
                    <input name="fileDescription" type="text" ng-model="myCtrl.myFormData.fileDescription" required/>
                    <input name="fileData" type="file" file-uploader ng-model="myCtrl.myFormData.fileData" required/>
                    <input type="submit" />
                </form>
            </div>
        </body>
    </html>

    可以看到,在fileUploadAjaxExample.html中寫了三個form,並且各自的submit動作都綁到了不同的事件,
    Javascript版本的綁定了submitForm_javascript()
    JQuery版本的綁定了submitForm_jquery()
    AngulrJS版本的綁定了myCtrl.submitForm(),並且還對每一個Input綁定了對應的model,
    這裡要注意的是,因為AngularJS並沒有實作intput type="file"的model-view雙向資料綁定,所以我們要實作一個自製的directive, "file-uploader" 來實現由View到model綁定。
  2. FileUploadAction.java:
    //////////Javascript version code - Start //////////
    function submitForm_javascript() {
        var form_javascript = document.getElementById("form_javascript");
        var formData = new FormData(form_javascript);
        var request = new XMLHttpRequest();
        request.open("POST", "fileUpload.do", true);
        request.onload = function (event) {
            if (request.status == 200) {
                console.log("OK_javascript");
            } else {
                console.log("Error");
            }
        };
        request.send(formData);
    }
    //////////Javascript version code - End //////////
    
    ////////// JQuery version code - Start //////////
    function submitForm_jquery() {
        var form_jquery = document.getElementById("form_jquery");
        var formData = new FormData(form_jquery);
        $.ajax({
            url: "fileUpload.do",
            method: "POST",
            data: formData,
            processData: false, // 告訴JQuery不要去處理發送的數據,不然會把data
                                //設置的物件轉換成查詢字符串以配合預設的application/x-www-form-urlencoded
                                //
            contentType: false, // 告訴JQuery不要去設置Content-Type請求Header,
                                //Header會自動適情況加上multipart/form-data
            success: function (response) {
                console.log("OK_jquery");
            },
            error: function (jqXHR, textStatus, errorMessage) {
                console.log(errorMessage);
            }
        });
    }
    //////////JQuery version code - End //////////
    
    //////////AngularJS version code - Start //////////
    (function () {
        var myApp = angular.module("myApp", []);
        myApp.controller("myController", ["$http", function ($http) {
                var self = this;
                self.myFormData = {};
                self.submitForm = function () {
                    //將myController.myFormData物件裡的資料都設定
                    //到Formdata物件中
                    var formData = getFormDataFormObject(self.myFormData);
                    $http({
                        url: "fileUpload.do",
                        method: "POST",
                        data: formData,
                        transformRequest: angular.identity,  //不對requet資料做處理,angular.identity是一個
                                                             //function,會返回第一個傳入參數 (即不對第一個參數
                                                             //做處理),例如像是:
                                                             //function (request){ return request; }
                        headers: {'Content-Type': undefined} //不要去設置Content-Type請求Header,
                                                            //Header會自動適情況加上multipart/form-data
                    }).success(function (response) {
                        console.log("OK_angularjs");
                    }).error(function (response) {
                        console.log(response);
                    });
                };
            }]);
        //自製的directive,用來處理AngularJS未實作的input type="file"
        //model-view數據綁定
        myApp.directive("fileUploader", ["$parse", function ($parse) {
                return {
                    restrict: "A",
                    require: 'ngModel',
                    link: function (scope, elm, attrs, ngModel) {
                        elm.bind("change", function () {
                            scope.$apply(function () {
                                if (elm[0].files.length > 0) {
                                    //將file資料設定到model上
                                    $parse(attrs.ngModel).assign(scope, elm[0].files[0]);
                                }
                            });
                        });
                    }
                }
            }]);
        //將object擁有的屬性全部設定到FormData物件上
        function getFormDataFormObject(object) {
            var formData = new FormData();
            Object.getOwnPropertyNames(object).forEach(function (value, index, array) {
                formData.append(value, object[value]);
            });
            return formData;
        }
    })();
    //////////AngularJS version code - End //////////
    
    在這邊我們將Javascript、JQuery、AngularJS三個版本的程式碼在同一個檔案中, 用註解分隔以利分別,其本上寫法非常相似,說明都寫在註譯中, 這邊做一些重點說明:
    1. 可以看到我們都沒有在Ajax的請求中設定header,這是為了讓header能自動因資
      料而被設定成multipart/form-data。
    2. 都設定不要對request的data做處理,因為multipart/form-data的編碼方式就是不要
      做任何的編碼。
    3. 因為AngularJS沒有對input type="file"實現model-view的資料綁定,所以在這邊我
      們自己實做一個directive,"fileUploader",來處理,
      請參考之前的文章:自製檢查file size的directive (input type="file") - AngularJs
  3. FileUploadAction.java
    這邊只貼重點部份的Code:
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            response.setContentType("text/html;charset=UTF-8");
            request.setCharacterEncoding("UTF-8");
            
            //使用request.getPart()來得到file資料
            Part uploadFile = request.getPart("fileData");
            //使用Part.getSubmittedFileName()得到上傳的file name
            String filename = uploadFile.getSubmittedFileName();
            //使用Part.getSize()得到上傳的file size
            int fileSize = (int) uploadFile.getSize();
            //使用request.getParameter()來得到input type不等於file的其他參數資料
            String fileDescription = request.getParameter("fileDescription");
            //使用Part.write()來輸出檔案,可使用相對路徑(參數直接給輸出檔案名稱)
            //或絕對路徑,
            //相對路徑會參考web.xml裡<multipart-config> --> <location> 的設置
            uploadFile.write(fileDescription + "_" + filename);
        }

    這邊需注意的是Part類別及可以用request.getParameter直接解析multipart/form-data請求等用法是Servlet3.0才有的用法。

源碼下載:
UploadFileAjaxExample.7z

參考資料:
  1. 使用FormData对象
  2. getPart()、getParts()
  3. Sending multipart/formdata with jQuery.ajax
  4. AngularJS图片上传功能的实现
  5. angularjs用FormData上传文件

沒有留言 :

張貼留言