AJAX+JSF組件 實現高效能的檔案上傳

來源:互聯網
上載者:User
一、 引言

  基於瀏覽器的檔案上傳,特別是對於通過<input type="file">標籤包含到Web頁面來實現上傳的情況,還存在較嚴重的效能問題。我們知道,超過10MB的上傳檔案經常導致一種非常痛苦的使用者體驗。一旦使用者提交了檔案,在瀏覽器把檔案上傳到伺服器的過程中,介面看上去似乎處於靜止狀態。由於這一切發生在後台,所以許多沒有耐心的使用者開始認為伺服器"掛"了,因而再次提交檔案,這當然使得情況變得更糟糕。

  為了儘可能使得檔案上感測覺更友好些,一旦使用者提交檔案,許多網站將顯示一個中間過程動畫(例如一旋轉表徵圖)。儘管這一技術在上傳提交到伺服器時起一些作用,但它還是提供了太少的有關檔案上傳狀態的資訊。解決這個問題的另外一種嘗試是實現一個applet——它通過FTP把檔案上傳到伺服器。這一方案的缺點是:限制了你的使用者,必須要有一個支援Java的瀏覽器。

  在本文中,我們將實現一個具有AJAX能力的組件——它不僅實現把檔案上傳到伺服器,而且"即時地"監視檔案上傳的實際過程。這個組件工作的四個階段顯示於下面的圖1,2,3和4中:


圖1.階段1:選擇檔案上傳

圖2.階段2:上傳該檔案到伺服器

圖3.階段3:上傳完成

圖4.階段4:檔案上傳摘要

   二、 實現該組件

  首先,我們分析建立多部分過濾的過程,它將允許我們處理並且監視檔案上傳。然後,我們將繼續實現JavaServer Faces(JSF)組件-它將提供給使用者連續的回饋,以支援AJAX的進度條方式。

  (一) 多部分過濾:UploadMultipartFilter

  多部分過濾的任務是攔截到來的檔案上傳並且把該檔案寫到一個伺服器上的臨時目錄中。同時,它還將監視接收的位元組數並且確定已經上傳該檔案的程度。幸運的是,現在有一個優秀的Jakarta-Commons開源庫可以利用(FileUpload),可以由它來負責分析一個HTTP多部分請求並且把檔案上傳到伺服器。我們要做的是擴充該庫並且加入我們需要的"鉤子"來監視已經處理了多少位元組。

public class UploadMultipartFilter implements Filter{
 public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
 throws IOException, ServletException {
  HttpServletRequest hRequest = (HttpServletRequest)request;
  //檢查是否我們在處理一個多部分請求
  String contentHeader = hRequest.getHeader("content-type");
  boolean isMultipart = ( contentHeader != null && contentHeader.indexOf("multipart/form-data") != -1);
  if(isMultipart == false){
   chain.doFilter(request,response);
  }else{
   UploadMultipartRequestWrapper wrapper = new UploadMultipartRequestWrapper(hRequest);
   chain.doFilter(wrapper,response);
  }
  ...
 }

  正如你所見,UploadMultipartFilter類簡單地檢查了當前的請求是否是一個多部分請求。如果該請求不包含檔案上傳,該請求將被傳遞到請求鏈中的下一個過濾,而不進行任何另外的處理。否則,該請求將被封裝在一個UploadMultipartRequestWrapper中。

  (二) UploadMultipartRequestWrapper類

public class UploadMultipartRequestWrapper
extends HttpServletRequestWrapper{
 private Map<String,String> formParameters;
 private Map<String,FileItem> fileParameters;
 public UploadMultipartRequestWrapper(HttpServletRequest request) {
  super(request);
  try{
   ServletFileUpload upload = new ServletFileUpload();
   upload.setFileItemFactory(new ProgressMonitorFileItemFactory(request));
   List fileItems = upload.parseRequest(request);
   formParameters = new HashMap<String,String>();
   fileParameters = new HashMap<String,FileItem>();
   for(int i=0;i<fileItems.size();i++){
    FileItem item = (FileItem)fileItems.get(i);
    if(item.isFormField() == true){
     formParameters.put(item.getFieldName(),item.getString());
    }else{
     fileParameters.put(item.getFieldName(),item);
     request.setAttribute(item.getFieldName(),item);
    }
   }
   }catch(FileUploadException fe){
    //請求時間超過-使用者可能已經轉到另一個頁面。
    //作一些記錄
    //...
   }
   ...

  在UploadMultipartRequestWrapper類中,我們將初始化ServletFileUpload類,它負責分析我們的請求並且把檔案寫到伺服器上的預設臨時目錄。ServletFileUpload執行個體針對在該請求中遇到的每一個欄位建立一個FileItem執行個體(它們包含檔案上傳和正常的表單元素)。之後,一個FileItem執行個體用於檢索一個提交欄位的屬性,或者,在檔案上傳的情況下,檢索一個到底層的臨時檔案的InputStream。總之,UploadMultipartRequestWrapper負責分析該檔案並且設定任何FileItem-它在該請求中把檔案上傳描述為屬性。然後,這些屬性由JSF組件所進一步收集,而正常表單欄位的行為保持不變。

  預設情況下,通用FileUpload庫將使用DiskFileItems類的執行個體來處理檔案上傳。儘管DiskFileItem在處理整個臨時檔案業務時是很有用的,但在準確監視該檔案已經處理程度方面存在很少支援。自版本1.1以來,通用FileUpload庫能夠使開發人員指定用於建立FileItem的工廠。我們將使用ProgressMonitorFileItemFactory和ProgressMonitorFileItem類來重載預設行為並監視檔案上傳過程。

  (三) ProgressMonitorFileItemFactory類

public class ProgressMonitorFileItemFactory extends DiskFileItemFactory {
 private File temporaryDirectory;
 private HttpServletRequest requestRef;
 private long requestLength;
 public ProgressMonitorFileItemFactory(HttpServletRequest request) {
  super();
  temporaryDirectory = (File)request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
  requestRef = request;
  String contentLength = request.getHeader("content-length");
  if(contentLength != null){requestLength = Long.parseLong(contentLength.trim());}
 }
 public FileItem createItem(String fieldName, String contentType,boolean isFormField, String fileName) {
  SessionUpdatingProgressObserver observer = null;
  if(isFormField == false) //這必須是一檔案上傳.
   observer = new SessionUpdatingProgressObserver(fieldName,fileName);
   ProgressMonitorFileItem item = new ProgressMonitorFileItem(
     fieldName,contentType,isFormField,
     fileName,2048,temporaryDirectory,
     observer,requestLength);
    return item;
 }
 ...
 public class SessionUpdatingProgressObserver implements ProgressObserver {
  private String fieldName;
  private String fileName;
  ...
  public void setProgress(double progress) {
   if(request != null){
    request.getSession().setAttribute("FileUpload.Progress."+fieldName,progress);
    request.getSession().setAttribute("FileUpload.FileName."+fieldName,fileName);
   }
  }
 }
}

  ProgressMonitorFileItemFactory Content-Length頭由瀏覽器設定並且假定它是被設定的上傳檔案的精確長度。這種確定檔案長度的方法確實限制了你在每次請求中上傳的檔案-如果有多個檔案在該請求中被編碼的話,不過這個值是不精確的。這是由於,瀏覽器僅僅發送一個Content-Length頭,而不考慮上傳的檔案數目。

  除了建立ProgressMonitorFileItem執行個體之外,ProgressMonitorFileItemFactory還註冊了一個ProgressObserver執行個體,它將由ProgressMonitorFileItem來傳送檔案上傳過程中的更新。我們所使用的ProgressObserver的實現(SessionUpdatingProgressObserver)針對被提交欄位的id把進度百分數設定到使用者的會話中。然後,這個值可以由JSF組件存取以便把更新發送給使用者。

  (四) ProgressMonitorFileItem類

public class ProgressMonitorFileItem extends DiskFileItem {
 private ProgressObserver observer;
 private long passedInFileSize;
 ...
 private boolean isFormField;
 ...
 @Override
 public OutputStream getOutputStream() throws IOException {
  OutputStream baseOutputStream = super.getOutputStream();
  if(isFormField == false){
   return new BytesCountingOutputStream(baseOutputStream);
  }else{return baseOutputStream;}
 }
 ...
 private class BytesCountingOutputStream extends OutputStream{
  private long previousProgressUpdate;
  private OutputStream base;
  public BytesCountingOutputStream(OutputStream ous){ base = ous; }
  ...
  private void fireProgressEvent(int b){
   bytesRead += b;
   ...
   double progress = (((double)(bytesRead)) / passedInFileSize);
   progress *= 100.0
   observer.setProgress();
  }
 }
}

  ProgressMonitorFileItem把DiskFileItem的預設OutputStream封裝到一個BytesCountingOutputStream中,這可以在每次讀取一定數目的位元組後更新相關的ProgressObserver。

  (五) 支援AJAX的JavaServer Faces(JSF)上傳組件

  這個組件負責產生HTML檔案上傳標籤,顯示一個進度條以監視檔案上傳,並且產生一旦檔案上傳成功需要被顯示的組件。使用JavaServer Faces實現這個組件的一個主要優點是,大多數複雜性被隱藏起來。開發人員只需要把組件標籤添加到JSP,而後由組件負責所有的AJAX及相關的進度條監控細節問題。下面的JSP代碼片斷用於把上傳組件添加到頁面上。

<comp:fileUpload
 value="#{uploadPageBean.uploadedFile}"
 uploadIcon="images/upload.png"
 styleClass="progressBarDiv"
 progressBarStyleClass="progressBar"
 cellStyleClass="progressBarCell"
 activeStyleClass="progressBarActiveCell">
<%--下面是一旦檔案上傳完成將成為可見的組件--%>
<h:panelGrid columns="2" cellpadding="2" cellspacing="0" width="100%">
<f:facet name="header">
<h:outputText styleClass="text"
value="檔案上傳成功." />
</f:facet>
<h:panelGroup style="text-align:left;display:block;width:100%;">
<h:commandButton action="#{uploadPageBean.reset}"
image="images/reset.png"/>
</h:panelGroup>
<h:panelGroup style="text-align:right;display:block;width:100%;">
<h:commandButton action="#{uploadPageBean.nextPage}"
image="images/continue.png"/>
</h:panelGroup>
</h:panelGrid>
</comp:fileUpload>

  檔案上傳組件的value屬性需要用一個擁有一個FileItem的屬性綁定到一個bean上。組件只有在該檔案被伺服器成功收到時才顯示。

   三、 實現AJAX檔案上傳組件

  實質上,上傳組件或者產生一個完整的自已,或者在一個AJAX請求的情況下,只產生部分XML以更新在頁面上進度條的狀態。為了防止JavaServer Faces產生完整的組件樹(這會帶來不必要的負荷),我們還需要實現一個PhaseListener(PagePhaseListener)以取消該faces的請求處理的其它部分-如果遇到一個AJAX請求的話。我在本文中略去了所有的關於標準配置(faces-config.xml和標籤庫)的討論,因為它們相當直接且已經在以前討論過;而且這一切都包含在隨同本文的源碼中,你可以詳細分析。

  (一) AJAX檔案上傳組件產生器

  該組件和標籤類的實現比較簡單。大量的邏輯被包含到產生器中,具體地說,它負責以下:

  · 編碼整個的上傳組件(和完整的HTML檔案上傳標籤)、檔案被上傳完成後要顯示的組件,還有實現AJAX請求的用戶端JavaScript代碼。

  · 適當地處理部分AJAX請求並且發送回必要的XML。

  · 解碼一個檔案上傳並且把它設定為一個FileItem執行個體。

  (二) 編碼整個上傳組件

  前面已經提及,檔案上傳組件由三個階段組成。在該組件的整個編碼期間,我們將詳細分析這三個階段的編碼。注意,在頁面上的該組件的可視化(使用CSS顯示)屬性將由AJAX JavaScript來控制。

  (三) 階段一

  圖5顯示了該上傳組件的第一個階段。


圖5.選擇檔案上傳


  在第一階段中,我們需要產生HTML檔案Upload標籤和點擊Upload按鈕時相應的執行代碼。一旦使用者點擊了Upload按鈕,表單將被一個IFRAME(為防止頁面阻塞)提交並初始化第二個階段。下面是產生代碼的一部分:

//檔案上傳組件
writer.startElement("input", component);
writer.writeAttribute("type", "file", null);
writer.writeAttribute("name", component.getClientId(context), "id");
writer.writeAttribute("id", component.getClientId(context),"id");
if(input.getValue() != null){
 //如果可用,則產生該檔案名稱.
 FileItem fileData = (FileItem)input.getValue();
 writer.writeAttribute("value", fileData.getName(), fileData.getName());
}
writer.endElement("input");
String iconURL = input.getUploadIcon();
//產生映像,並把JavaScript事件依附到其上.
writer.startElement("div", component);
writer.writeAttribute("style","display:block;width:100%;text-align:center;", "style");
writer.startElement("img", component);
writer.writeAttribute("src",iconURL,"src");
writer.writeAttribute("type","image","type");
writer.writeAttribute("style","cursor:hand;cursor:pointer;","style");
UIForm form = FacesUtils.getForm(context,component);
if(form != null) {
 String getFormJS = "document.getElementById('" + form.getClientId(context) + "')";
 String jsFriendlyClientID = input.getClientId(context).replace(":","_");
 //設定表單的編碼為multipart以用於檔案上傳,並且通過一個IFRAME
 //來提交它的內容。該組件的第二個階段也在500毫秒後被初始化.
 writer.writeAttribute("onclick",getFormJS + ".encoding='multipart/form-data';" +
getFormJS + ".target='" + iframeName + "';" + getFormJS + ".submit();" +
getFormJS + ".encoding='application/x-www-form-urlencoded';" +
getFormJS + ".target='_self';" +
"setTimeout('refreshProgress" + jsFriendlyClientID + "();',500);",null);
}
...
writer.endElement("img");
//現在實現我們將要把該檔案/表單提交到的IFRAME.
writer.startElement("iframe", component);
writer.writeAttribute("id", iframeName, null);
writer.writeAttribute("name",iframeName,null);
writer.writeAttribute("style","display:none;",null);
writer.endElement("iframe");
writer.endElement("div");
writer.endElement("div"); //階段1結束

  (四) 階段二

  第二階段是顯示當前百分比的進度條和標籤,如圖6所示。該進度條是作為一個具有100個內嵌span標籤的div標籤實現的。這些將由AJAX JavaScript根據來自於伺服器的響應進行設定。


圖6.上傳檔案到伺服器

writer.startElement("div",component);
writer.writeAttribute("id", input.getClientId(context) + "_stage2", "id");
...
writer.writeAttribute("style","display:none", "style");
String progressBarID = component.getClientId(context) + "_progressBar";
String progressBarLabelID = component.getClientId(context) + "_progressBarlabel";
writer.startElement("div", component);
writer.writeAttribute("id",progressBarID,"id");
String progressBarStyleClass = input.getProgressBarStyleClass();
if(progressBarStyleClass != null)
writer.writeAttribute("class",progressBarStyleClass,"class");
for(int i=0;i<100;i++){
 writer.write("<span> </span>");
}
writer.endElement("div");
writer.startElement("div",component);
writer.writeAttribute("id",progressBarLabelID,"id");
...
writer.endElement("div");
writer.endElement("div"); //階段2結束

  (五) 階段三

  最後,作為階段三,一旦檔案成功上傳,需要被顯示的組件即被產生,見圖7。這些是在產生器的encodeChildren方法中實現的。


圖7.上傳完成

public void encodeChildren(FacesContext context,
UIComponent component) throws IOException {
 ResponseWriter writer = context.getResponseWriter();
 UIFileUpload input = (UIFileUpload)component;
 //一旦檔案上傳成功,處理將被顯示的子結點
 writer.startElement("div", component);
 writer.writeAttribute("id", input.getClientId(context) + "_stage3", "id"); //階段3.
 if(input.getValue() == null){
  writer.writeAttribute("style","display:none;",null);
 }else{
  writer.writeAttribute("style","display:block",null);
 }
 List<UIComponent> children = input.getChildren();
 for(UIComponent child : children){
  FacesUtils.encodeRecursive(context,child);
 }
 writer.endElement("div"); //階段3結束
}

   四、處理AJAX請求

  AJAX請求的產生是在這個組件的解碼方法中處理的。我們需要檢查這是否是一個實際的AJAX請求(為了區別於正常的編譯行為),然後基於由ProgressMonitorFileItemFactory類的SessionUpdatingProgressObserver執行個體設定在會話中的值把一個XML響應發送回用戶端。

public void decode(FacesContext context, UIComponent component) {
 UIFileUpload input = (UIFileUpload) component;
 //檢查是否這是一個上傳進度請求,或是一個實際的上傳請求.
 ExternalContext extContext = context.getExternalContext();
 Map parameterMap = extContext.getRequestParameterMap();
 String clientId = input.getClientId(context);
 Map requestMap = extContext.getRequestParameterMap();
 if(requestMap.get(clientId) == null){
  return;//什麼也不做,返回
 }
 if(parameterMap.containsKey(PROGRESS_REQUEST_PARAM_NAME)){
  //這是一個在該檔案請求中的得到進度資訊的請求.
  //得到該進度資訊並把它產生為XML
  HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
  //設定響應的頭資訊
  response.setContentType("text/xml");
  response.setHeader("Cache-Control", "no-cache");
  try {
   ResponseWriter writer = FacesUtils.setupResponseWriter(context);
   writer.startElement("progress", input);
   writer.startElement("percentage", input);
   //從會話中獲得當前進度百分數(由過濾器所設定).
   Double progressCount = (Double)extContext.getSessionMap().
   get("FileUpload.Progress." +input.getClientId(context));
   if(progressCount != null){
    writer.writeText(progressCount, null);
   }else{
    writer.writeText("1", null);//我們還沒有收到上傳
   }
   writer.endElement("percentage");
   writer.startElement("clientId", input);
   writer.writeText(input.getClientId(context), null);
   writer.endElement("clientId");
   writer.endElement("progress");
  } catch(Exception e){
   //做一些錯誤記錄...
  }
  }else{
   //正常的解碼請求.
  ...

   五、 正常的解碼行為

  在正常的編譯期間,檔案上傳產生器從請求屬性中檢索FileItem,正是在此處它被過濾器所設定,並且更新該組件的值綁定。然後,該會話中的進度被更新到100%,這樣在頁面上的JavaScript就可以把組件送入第3個階段。

//正常的解碼請求.
if(requestMap.get(clientId).toString().equals("file")){
try{
 HttpServletRequest request = (HttpServletRequest)extContext.getRequest();
 FileItem fileData = (FileItem)request.getAttribute(clientId);
 if(fileData != null) input.setSubmittedValue(fileData);
 //現在我們需要清除與該項相關的任何進度
 extContext.getSessionMap().put("FileUpload.Progress." + input.getClientId(context),new Double(100));
}catch(Exception e){
 throw new RuntimeException("不能處理檔案上傳" +" - 請配置過濾器.",e);
}
}

  用戶端JavaScript負責向伺服器發出進度請求並通過不同階段來移動組件。為了簡化處理所有的瀏覽器特定的XMLHttpRequest對象的問題,我選用了Matt Krause提供的AjaxRequest.js庫。該庫最大限度地減少我們需要編寫的JavaScript代碼的數量,同時可以使這個組件正常工作。也許把這部分JavaScript代碼打包為該組件的一部分,然後從PhaseListener產生它更好一些,但是,我已經通過定義一個到JSP頁面上的JavaScript庫的連結來儘力使得它簡單。

  組件中的getProgressBarJavaScript方法被調用以產生JavaScript。使JavaScript正常工作通常是實現AJAX組件最困難的部分;不過我想,下面的代碼已經非常清晰易於理解了。儘管在我的樣本中JavaScript是嵌入到Java代碼中的,但是把它放到一個外部獨立的檔案中也許更好一些。在本文中,我只是想使問題更為簡單些且只關心本文的主題。下面是一個將由組件產生的JavaScript的樣本。其中假定,fileUpload1是被賦值到該檔案組件的用戶端JSF Id,而uploadForm是HTML表單的Id。

function refreshProgress(){
 // 假定我們正在進入到階段2.
 document.getElementById('fileUpload1_stage1').style.display = 'none';
 document.getElementById('fileUpload1_stage2').style.display = '';
 document.getElementById('fileUpload1_stage3').style.display = 'none';
 //建立AJAX寄送
 AjaxRequest.post(
 {
  //指定正確的參數,以便
  //該組件在伺服器端被正確處理
  'parameters':{ 'uploadForm':'uploadForm',
  'fileUpload1':'fileUpload1',
  'jsf.component.UIFileUpload':'1',
  'ajax.abortPhase':'4' } //Abort at Phase 4.
  //指定成功處理相應的回調方法.
  ,'onSuccess':function(req) {
  var xml = req.responseXML;
  if( xml.getElementsByTagName('clientId').length == 0) {
   setTimeout('refreshProgress()',200); return;
  }
  var clientId = xml.getElementsByTagName('clientId');
  clientId = clientId[0].firstChild.nodeValue + '_progressBar';
  //從XML擷取百分比
  var percentage = xml.getElementsByTagName('percentage')[0].firstChild.nodeValue;
  var innerSpans = document.getElementById(clientId).getElementsByTagName('span');
  document.getElementById(clientId + 'label').innerHTML = Math.round(percentage) + '%';
  //基於當前進度,設定這些span的式樣類。
  for(var i=0;i<innerSpans.length;i++){
   if(i < percentage){
    innerSpans[i].className = 'active';
   }else{
    innerSpans[i].className = 'passive';
   }
  }
  //如果進度不是100,我們需要繼續查詢服務器以實現更新.
  if(percentage != 100){
   setTimeout('refreshProgress()',400);
  } else {
   //檔案上傳已經完成,我們現在需要把該組件送入到第3個階段.
   document.getElementById('fileUpload1_stage1').style.display = 'none';
   document.getElementById('fileUpload1_stage2').style.display = 'none';
   document.getElementById('fileUpload1_stage3').style.display = '';
  }
 }
});
}
return builder.toString();

   六、 結論

  我很希望,本文能夠在有關如何使得檔案上傳更具有方便使用性,並且把AJAX和JavaServer Faces用於實現進階使用者介面組件的可能性方面引發你的進一步思考。毫無疑問,本文中的方案比較冗長並且有可能得到進一步的改進。我希望你能詳細地分析一下本文中所提供的完整的原始碼來深入理解本文中所討論的概念。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.