ajax|js|性能我们将实现一个具有AJAX能力的组件——它不仅实现把文件上传到服务器,而且"实时地"监视文件上传的实际过程。 (四) 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 action="#{uploadPageBean.reset}" image="images/reset.png"/> </h:panelGroup> <h:panelGroup 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结束 } |