DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> AJAX入門 >> AJAX基礎知識 >> 掌控上傳進度的AJAX Upload
掌控上傳進度的AJAX Upload
編輯:AJAX基礎知識     
動機:

        2006年底Google了一下AJAX Upload實現,結果沒有發現很完整的Java實現。碩果僅存的就是TELIO公司的Pierre-Alexandre發表的《AJAX Upload progress monitor for Commons-FileUpload Example》文中提供的ajax-upload-1.0.war。

        雖然上文中完成Upload工作的是Apache的Common-FileUpload組件,但在其代碼中所使用的FileUpload1.1版本並沒有1.2版本所提供的上傳處理Listener功能,這就對檢測文件上傳情況造成了困難。我想正是這個原因致使Pierre-Alexandre使用了DWR+MonitoredDiskFileItem、MonitoredDiskFileItemFactory類(分別繼承DiskFileItem、DiskFileItemFactory)的方式:前者負責在web客戶端進行Remote Call;後者在進行文件數據讀取時統計數據總量、讀取數據量、處理文件總數,並保存於Session中,以供web客戶端通過DWR遠程調用UploadMonitor類的getUploadInfo方法進行輪詢(Poll)。

        從本人觀點出發,Pierre-Alexandre實現的不足之處:
        1.沒有用戶取消上傳功能;
        2.完全的DWR實現,沒有使用Prototype,對於不會使用DWR的開發者來講有一定的知識局限性,而且由於DWR的個性而造成不便將此實現集成到項目中。



Prototype+Servlet的實現:

image
Prototype+Servlet的Example


        所以出於研究Prototype之目的,本人經過仔細思考,嘗試實現了一個Prototype+Servlet的簡單Example。其工作流程很簡單:
1.在Form提交上傳文件Field的同時,使用AJAX周期性地從Servlet輪詢上傳狀態信息;
2.然後,根據此信息更新進度條和相關文字,及時反映文件傳輸狀態;
3.如果用戶取消上傳操作,則進行相應的現場清理工作:刪除已經上傳的文件,在Form提交頁面中顯示相關信息;
4.如果上傳完畢,在Form提交頁面中顯示已經上傳的文件內容(或鏈接),也可以與一些AJAX SlideShow應用結合在一起。

服務器端代碼:

        Bean序列化/反序列化工作:XmlUnSerializer這個類雖然不能夠通吃任何模樣的Bean,但應付一般的Bean、具有Collection類型屬性的Bean和Bean List來講還是夠用的。
        {XmlUnSerializer類的核心方法serializeBean和serializeBeanList}:

        /**
         * 將bean系列化為UTF-8編碼的xml
         * @param beanObj
         * @return
         * @throws IOException
         */
        public static String serializeBean(Object beanObj) throws IOException{
        …
        }
        /**
         * 將bean列表序列化為UTF-8編碼的xml
         * @param beanObj
         * @return
         * @throws IOException
         */
        public static String serializeBeanList(Object beanListObj) throws IOException{
        …
        }
        文件上傳狀態Bean:使用FileUploadStatus這個類記錄文件上傳狀態,並將其作為服務器端與web客戶端之間通信的媒介物:通過對這個類對象進行XML序列化作為服務器回應發送給web客戶端,web客戶端使用JavaScript對其進行反序列化處理獲得JavaScript版本的文件上傳狀態對象。
        {FileUploadStatus的屬性}:

        //上傳總量
        private long uploadTotalSize=0;
        //讀取上傳總量
        private long readTotalSize=0;
        //當前上傳文件號
        private int currentUploadFileNum=0;
        //成功讀取上傳文件數
        private int successUploadFileCount=0;
        //狀態
        private String status="";
        //處理起始時間
        private long processStartTime=0l;
        //處理終止時間
        private long processEndTime=0l;
        //處理執行時間
        private long processRunningTime=0l;
        //上傳文件URL列表
        private List uploadFileUrlList=new ArrayList();
        //取消上傳
        private boolean cancel=false;
        //上傳base目錄
        private String baseDir="";


        文件上傳狀態監視工作:使用Common-FileUpload 1.2版本(20070103)。此版本與1.1版的區別在於提供了能夠監視文件上傳情況的ProcessListener接口,使開發者通過FileUploadBase類對象的setProcessListener方法植入自己的Listener,而且實現這個Listener很簡單。
        {FileUploadListener主要方法update}:

/**
* 更新狀態
* @param pBytesRead 讀取字節總數
* @param pContentLength 數據總長度
* @param pItems 當前正在被讀取的field號
*/
public void update(long pBytesRead, long pContentLength, int pItems){
FileUploadStatus fuploadStatus=BackGroundService.takeOutFileUploadStatusBean(this.session);
logger.debug("當前正在處理第" + pItems+"個文件");
fuploadStatus.setUploadTotalSize(pContentLength);
    //讀取完成
if (pContentLength == -1) {
   logger.debug("讀取完成:讀取了 " + pBytesRead + " bytes.");
   fuploadStatus.setStatus("完成對" + pItems+"個文件的讀取:讀取了 " + pBytesRead + " bytes.");
   fuploadStatus.setReadTotalSize(pBytesRead);
   fuploadStatus.setSuccessUploadFileCount(pItems);
   fuploadStatus.setProcessEndTime(System.currentTimeMillis());
   fuploadStatus.setProcessRunningTime(fuploadStatus.getProcessEndTime());
            //讀取中
    } else {
     logger.debug("讀取進行中:已經讀取了 " + pBytesRead + " / " + pContentLength+ " bytes.");
     fuploadStatus.setStatus("當前正在處理第" + pItems+"個文件:已經讀取了 " + pBytesRead
+ " / " + pContentLength+ " bytes.");
     fuploadStatus.setReadTotalSize(pBytesRead);
     fuploadStatus.setCurrentUploadFileNum(pItems);
     fuploadStatus.setProcessRunningTime(System.currentTimeMillis());
            }
BackGroundService.storeFileUploadStatusBean(this.session,fuploadStatus);
        }

 

 

       很清楚,我也把FileUploadStatus這個Bean存取於Session中。

        Servlet實現:BackGroundService這個Servlet類負責接收Form Post數據、回應狀態輪詢請求、處理取消文件上傳的請求。盡管可以把這些功能相互分離開來(比如構造一個FileUploadManager類),但出於簡單明了、便於閱讀之目的,還是將它們放到Servlet中,只是由不同的方法進行分割。
        {BackGroundService中的processFileUpload方法用於處理文件上傳請求}:


/**
* 處理文件上傳
* @param request
* @param response
* @throws IOException
* @throws ServletException
*/
private void processFileUpload(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException{
      DiskFileItemFactory factory = new DiskFileItemFactory();
      //設置內存閥值,超過後寫入臨時文件
      factory.setSizeThreshold(10240000);
      //設置臨時文件存儲位置
      factory.setRepository(new File(request.getRealPath("/upload/temp")));
      ServletFileUpload upload = new ServletFileUpload(factory);
      //設置單個文件的最大上傳size
      upload.setFileSizeMax(10240000);
      //設置整個request的最大size
      upload.setSizeMax(10240000);
      upload.setProgressListener(new FileUploadListener(request.getSession()));
      //保存初始化後的FileUploadStatus Bean
        storeFileUploadStatusBean(request.getSession(),initFileUploadStatusBean(request));
                
        String forwardURL="";
        try {
             List items = upload.parseRequest(request);
             //獲得返回url
               for(int i=0;i<items.size();i++){
                   FileItem item=(FileItem)items.get(i);
                   if (item.isFormField()){
                      logger.debug("form Field["+item.getFieldName()+"]="+item.getString());
                      forwardURL=item.getString();
                      break;
                                }
                        }
            //處理文件上傳
              for(int i=0;i<items.size();i++){
                 FileItem item=(FileItem)items.get(i);

                   //取消上傳
                 if (takeOutFileUploadStatusBean(request.getSession()).getCancel()){
                     deleteUploadedFile(request);
                     break;
                    }
                   //保存文件
                 else if (!item.isFormField() && item.getName().length()>0){
                         String fileName=takeOutFileName(item.getName());
                         logger.debug("處理文件["+fileName+"]:保存路徑為"
                         +request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
                         File uploadedFile =
new File(request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
                         item.write(uploadedFile);
                         //更新上傳文件列表
                         FileUploadStatus fUploadStatus=takeOutFileUploadStatusBean
(request.getSession());
                         fUploadStatus.getUploadFileUrlList().add(fileName);
                         storeFileUploadStatusBean(request.getSession(),fUploadStatus);
                         Thread.sleep(500);
                          }
                 }
                
       } catch (FileUploadException e) {
              logger.error("上傳文件時發生錯誤:"+e.getMessage());
              e.printStackTrace();
              uploadExceptionHandle(request,"上傳文件時發生錯誤:"+e.getMessage());
        } catch (Exception e) {
               // TODO Auto-generated catch block
               logger.error("保存上傳文件時發生錯誤:"+e.getMessage());
               e.printStackTrace();
               uploadExceptionHandle(request,"保存上傳文件時發生錯誤:"+e.getMessage());
         }
          if (forwardURL.length()==0){
             forwardURL=DEFAULT_UPLOAD_FAILURE_URL;
                }
        request.getRequestDispatcher(forwardURL).forward(request,response);
        }


        {BackGroundService中的responseFileUploadStatusPoll方法用於處理對文件上傳狀態的輪詢請求}:

/**
* 回應上傳狀態查詢
* @param request
* @param response
* @throws IOException
*/
private void responseFileUploadStatusPoll(HttpServletRequest request,HttpServletResponse response)
throws IOException{
response.setContentType("text/xml");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
logger.debug("發送上傳狀態回應");
response.getWriter().write(XmlUnSerializer.serializeBean(
          request.getSession().getAttribute(UPLOAD_STATUS)));
}


        {BackGroundService中的processCancelFileUpload方法用於處理取消文件上傳的請求}:

/**
* 處理取消文件上傳
* @param request
* @param response
* @throws IOException
*/
private void processCancelFileUpload(HttpServletRequest request,HttpServletResponse response)
throws IOException{
FileUploadStatus fUploadStatus=(FileUploadStatus)request.getSession().getAttribute(UPLOAD_STATUS);
fUploadStatus.setCancel(true);
request.getSession().setAttribute(UPLOAD_STATUS, fUploadStatus);
responseFileUploadStatusPoll(request,response);
 }


Web客戶端代碼:

image
Prototype給開發者更多的自由選擇



web客戶端使用了基於Prototype的AjaxWrapper類和XMLDomForAjax類,前者實現了對Ajax.Request功能的封裝,而後者實現了對來自服務器的XML Response的反序列化(反序列化為JavaScript對象)。

 

 

       為了避免在AjaxWrapper的回調方法中發生this被重寫的問題,我使用了ClassUtils類給任何類的每個方法注冊一個對類對象自身引用,詳見《解開JavaScript生命的達芬奇密碼》和《Prototype.AjaxRequest的調用堆棧重寫問題》:
        {ClassUtils類代碼}:


//類工具
var ClassUtils=Class.create();
ClassUtils.prototype={
        _ClassUtilsName:'ClassUtils',
        initialize:function(){
        },
        /**
         * 給類的每個方法注冊一個對類對象的自我引用
         * @param reference 對類對象的引用
         */
        registerFuncSelfLink:function(reference){
                for (var n in reference) {
                var item = reference[n];                        
                if (item instanceof Function)
                                item.$ = reference;
            }
        }
}


        {將XML反序列化為JavaScript對象的XMLDomForAjax類代碼}:

var XMLDomForAjax=Class.create();
XMLDomForAjax.prototype={
        isDebug:false,
        //dom節點類型常量
        ELEMENT_NODE:1,
        ATTRIBUTE_NODE:2,
    TEXT_NODE:3,
    CDATA_SECTION_NODE:4,
    ENTITY_REFERENCE_NODE:5,
    ENTITY_NODE:6,
    PROCESSING_INSTRUCTION_NODE:7,
    COMMENT_NODE:8,
    DOCUMENT_NODE:9,
    DOCUMENT_TYPE_NODE:10,
    DOCUMENT_FRAGMENT_NODE:11,
    NOTATION_NODE:12,
    
        initialize:function(isDebug){
                new ClassUtils().registerFuncSelfLink(this);
                this.isDebug=isDebug;
        },
        /**
         * 建立跨平台的dom解析器
         * @param xml xml字符串
         * @return dom解析器
         */
        createDomParser:function(xml){
                // code for IE
                if (window.ActiveXObject){
                  var doc=new ActiveXObject("Microsoft.XMLDOM");
                  doc.async="false";
                  doc.loadXML(xml);
                }
                // code for Mozilla, Firefox, Opera, etc.
                else{
                  var parser=new DOMParser();
                  var doc=parser.parseFromString(xml,"text/xml");
                }
                return doc;
        },
        /**
         * 反向序列化xml到javascript Bean
         * @param xml xml字符串
         * @return javascript Bean
         */
        deserializedBeanFromXML:function (xml){
                var funcHolder=arguments.callee.$;
                var doc=funcHolder.createDomParser(xml);
                // documentElement總表示文檔的root
                var objDomTree=doc.documentElement;
                var obj=new Object();
            for (var i=0; i<objDomTree.childNodes.length; i++) {
                    //獲得節點
                    var node=objDomTree.childNodes[i];
                    //取出其中的field元素進行處理
                if ((node.nodeType==funcHolder.ELEMENT_NODE) && (node.tagName == 'field')) {
                        var nodeText=funcHolder.getNodeText(node);
                        if (funcHolder.isDebug){
           alert(node.getAttribute('name')+' type:'+node.getAttribute('type')+' text:'+nodeText);
                        }
                    var objFieldValue=null;
                    //如果為列表
                    if (node.getAttribute('type')=='java.util.List'){
                            if (objFieldValue && typeof(objFieldValue)=='Array'){
                                    if (nodeText.length>0){
                                                        objFieldValue[objFieldValue.length]=nodeText;
                                                }
                                        }
                                        else{
                                                objFieldValue=new Array();
                                        }
                                }
                                else if (node.getAttribute('type')=='long'
                                        || node.getAttribute('type')=='java.lang.Long'
                                        || node.getAttribute('type')=='int'
                                        || node.getAttribute('type')=='java.lang.Integer'){
                                        
                                        objFieldValue=parseInt(nodeText);
                                }
                                else if (node.getAttribute('type')=='double'
                                        || node.getAttribute('type')=='float'
                                        || node.getAttribute('type')=='java.lang.Double'
                                        || node.getAttribute('type')=='java.lang.Float'){
                                        
                                        objFieldValue=parseFloat(nodeText);
                                }
                                else if (node.getAttribute('type')=='java.lang.String'){
                                        objFieldValue=nodeText;
                                }
                                else{
                                        objFieldValue=nodeText;
                                }
                                //賦值給對象
                                obj[node.getAttribute('name')]=objFieldValue;
                                if (funcHolder.isDebug){
                                        alert(eval('obj.'+node.getAttribute('name')));
                                }
                }
                else if (node.nodeType == funcHolder.TEXT_NODE){
                        if (funcHolder.isDebug){
                                //alert('TEXT_NODE');
                        }
                        
                }
                else if (node.nodeType == funcHolder.CDATA_SECTION_NODE){
                        if (funcHolder.isDebug){
                                //alert('CDATA_SECTION_NODE');
                        }
                }
            }
            return obj;
        },
        /**
         * 獲得dom節點的text
         */
        getNodeText:function (node) {
                var funcHolder=arguments.callee.$;
            // is this a text or CDATA node?
     if (node.nodeType == funcHolder.TEXT_NODE || node.nodeType == funcHolder.CDATA_SECTION_NODE) {
                return node.data;
            }
            var i;
            var returnValue = [];
            for (i = 0; i < node.childNodes.length; i++) {
                    //采用遞歸算法
                returnValue.push(funcHolder.getNodeText(node.childNodes[i]));
            }
            return returnValue.join('');
        }
}



 

 

        {AjaxWrapper類的主要方法putRequest和callBackHandler}:


         /**
         * 以get的方式向server發送request
         * @param url
         * @param params
         * @param callBackFunction 發送成功後回調的函數或者函數名
         */
        putRequest:function(url,params,callBackFunction){
                var funcHolder=arguments.callee.$;
            var xmlHttp = new Ajax.Request(url,
                        {
                                method: 'get',
                            parameters: params,
                                requestHeaders:['my-header-encoding','utf-8'],
                            onFailure: function(){
                                        alert('對不起,網絡通訊失敗,請重新刷新!');
                                },
                                onSuccess: function(transport){
                                },
                                onComplete: function(transport){
                         funcHolder.callBackHandler.apply(funcHolder,[transport,callBackFunction]);
                                }
                        });
        },
        /**
         * 遠程調用的回調處理
         * @param transport xmlhttp的transport
         * @param callBackFunction 回調時call的方法,可以是函數也可以是函數名
         */
        callBackHandler:function(transport,callBackFunction){
                var funcHolder=arguments.callee.$;
                if(transport.status!=200){
                        alert("獲得回應失敗,請求狀態:"+transport.status);
                }
                else{
                        funcHolder.xml_source=transport.responseText;
                        if (funcHolder.debug_flag)
                                alert('call callback function');
                        if (typeof(callBackFunction)=='function'){
                                if (funcHolder.debug_flag){
                                        alert('invoke callbackFunc');
                                }
                                callBackFunction(transport.responseText);
                        }
                        else{
                                if (funcHolder.debug_flag){
                                        alert('evalFunc callbackFunc');
                                }
                                new execute().evalFunc(callBackFunction,transport.responseText);
                        }
                        if (funcHolder.debug_flag)
                                alert('end callback function');
                }
        }


        {頁面中主要的JavaScript方法:refreshUploadStatus和startProcess/cancelProcess}:

//刷新上傳狀態
function refreshUploadStatus(){
        var ajaxW = new AjaxWrapper(false);
        ajaxW.putRequest(
                './uploadStatus.action',
                'uploadStatus=',
                function(responseText){
                        var deserialor=new XMLDomForAjax(false);
                        var uploadInfo=deserialor.deserializedBeanFromXML(responseText);
                        var progressPercent = Math.ceil(
                                (uploadInfo.readTotalSize) / uploadInfo.uploadTotalSize * 100);

                $('progressBarText').innerHTML = ' 上傳處理進度: '+progressPercent+'% ['+
                        (uploadInfo.readTotalSize)+'/'+uploadInfo.uploadTotalSize + ' bytes]'+
                        ' 正在處理第'+uploadInfo.currentUploadFileNum+'個文件'+
                        ' 耗時: '+(uploadInfo.processRunningTime-uploadInfo.processStartTime)+' ms';
                $('progressStatusText').innerHTML=' 反饋狀態: '+uploadInfo.status;
                $('totalProgressBarBoxContent').style.width = parseInt(progressPercent * 3.5) + 'px';
                }
        );
}
//上傳處理
function startProgress(){
        Element.show('progressBar');
    $('progressBarText').innerHTML = ' 上傳處理進度: 0%';
    $('progressStatusText').innerHTML=' 反饋狀態:';
    $('uploadButton').disabled = true;
    var periodicalExe=new PeriodicalExecuter(refreshUploadStatus,2);
    return true;
}
//取消上傳處理
function cancelProgress(){
        $('cancelUploadButton').disabled = true;
        var ajaxW = new AjaxWrapper(false);
        ajaxW.putRequest(
                './uploadStatus.action',
                'cancelUpload=true',
                //因為form的提交,這可能不會執行
                function(responseText){
                        var deserialor=new XMLDomForAjax(false);
                        var uploadInfo=deserialor.deserializedBeanFromXML(responseText);
                        $('progressStatusText').innerHTML=' 反饋狀態: '+uploadInfo.status;
                        if (msgInfo.cancel=='true'){
                                alert('刪除成功!');
                                window.location.reload();
                        };
                }
        );
}


運行界面:

image
起始頁面



image
上傳進行中…



image
上傳完成後的文件列表



image
用戶取消上傳後顯示的頁面




image
上傳過程中出錯(上傳文件過大)頁面


源代碼下載:

AjaxUpload.zip

相關鏈接:
        AJAX Upload progress monitor for Commons-FileUpload Example
        Apache Common FileUpload組件
        Prototype官方網站
        IBM的AJAX SlideShow應用
        解開JavaScript生命的達芬奇密碼
        Prototype.AjaxRequest的調用堆棧重寫問題

感謝閱讀此文

        請支持cleverpig發起的image

 

XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved