GWT使用HTML5实现批量上传和进度显示
目标效果
嘿嘿,先放个酸葡萄,看看有没有人来啃。
准备上传
批量选择上传文件
上传中并显示整体进度
上传完毕并按格式插入内容
单文件上传效果
方案
比较了2种方案:
方案1:其实HTML5方案GWT都已经给我们准备好了,就是Elemental.jar的支持代码。但是要使用它,付出的代价就是必须使用SuperDevMode来开发,想想使用Chrome devTool调试模式和全量编译带来的效率影响。做了一个艰难的决定,不用Elemental!
方案2:不用Elemental是否可以用纯javascript?用js写,是非常简单。但是怎样用GWT去干呢?有办法,绝对有办法,办法是折腾出来的。请看下面的实现步骤。
实现步骤
首先要实现一些HTML5的基础类例如支持多文件及本地文件大小检测的FileUpload及File对象等,我们可以选择lib-gwt-file来实现一些基础的功能(https://www.vectomatic.org/libs/lib-gwt-file),有别人的肩膀,就不用关门造轮子。
在做所有事情之前,先来看javascript是怎样实现一个典型的html5的上传的。
function uploadFile(file){ var url = 'server/index.php'; var xhr = new XMLHttpRequest(); var fd = new FormData(); xhr.open("POST", url, true); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { // Every thing ok, file uploaded console.log(xhr.responseText); // handle response. } }; fd.append("upload_file", file); xhr.send(fd); }
从这个代码能够看到AJAX上传一个文件的流程:
a)需要XMLHttpRequest
b)需要FormData
c)需要将file对象添加到FormData
d)send这个file对象把js的流程弄明白后,接下来用GWT来打造它。FormData是没有这个的(Elemental有,但是方案2不使用它),没有类,自己造一个!由最基础的JavaScriptObject来扩展:
public class CcUploadFormData extends JavaScriptObject { protected CcUploadFormData(){} public final native void append(String name, File file) /*-{ this.append(name, file); }-*/; public static native CcUploadFormData create() /*-{ return new FormData(); }-*/; public final native void append(String name, String value) /*-{ this.append(name, value); }-*/; }
上面能够看到,其实也就是把javascript对象包装了下,这里我只扩展了几个需要的关键方法为java实现。
有了FormData,关联File的方法也有了。下一步要将对象通过XMLHttpRequest异步发送到服务器。蛋疼的是(不要担心GWT的蛋疼,因为GWT就是睾丸疼=蛋疼的缩写,疼多了就麻木了^_^)XMLHttpRequest是没有send这个FormData接口的。没关系,将js方法扩展一个即可。
public class CcXMLHttpRequest extends XMLHttpRequest { protected CcXMLHttpRequest(){} public final native void send(CcUploadFormData data) /*-{ this.send(data); }-*/; }
然后服务端怎样接收呢?其实和普通的上传没有两样,都是用multi-part的方式上传。例如spring mvc可以这样写:
public FileInfDto upload(@RequestParam("file") MultipartFile file){ //your code }
处理多文件发送。多文件发送怎么办呢?lib-gwt-file很好的处理了这个问题FileUploadExt当设置为multi时,能够通过getFiles()获取到各个File对象。并且能在发送前处理文件大小的判断和文件类型的判断。看看lib-gwt-file的example,在此不再赘述。
处理进度条这玩意lib-gwt-file没有支持。又得自己打造了,老规矩,先看明白javascript的实现:
xhr.upload.addEventListener("progress", function(e) { var pc = parseInt(100 - (e.loaded / e.total * 100)); progress.style.backgroundPosition = pc + "% 0"; }, false); xhr.onreadystatechange = function(e) { if (xhr.readyState == 4) { progress.className = (xhr.status == 200 ? "success" : "failure"); } };
如上,其实也很简单,就是添加侦听而已。除了这两个,还有一个error的侦听。
使用JSNI给XMLHttpRequest 添加侦听事件:
private native void addProgressHandler(ProgressCallback progressCallback)/*-{ var listener = function(event){ progressCallback.@org.ccframe.client.components.fileupload.ProgressCallback::onProcess(ZDD)(event.lengthComputable,event.loaded,event.total); }; this.@org.ccframe.client.components.fileupload.CcBaseFileUpload::xmlHttpRequest.upload.addEventListener("progress", listener, false); }-*/; private native void addCompleteHandler(Runnable callback)/*-{ var listener = function(event){ callback.@java.lang.Runnable::run()(); }; this.@org.ccframe.client.components.fileupload.CcBaseFileUpload::xmlHttpRequest.upload.addEventListener("load", listener, false); }-*/; private native void addCanceledHandler()/*-{ var listener = function(event){ alert('abort'); }; this.@org.ccframe.client.components.fileupload.CcBaseFileUpload::xmlHttpRequest.upload.addEventListener("abort", listener, false); }-*/;
为了方便扩展,我定义了一个ProgressCallback,用来专门处理进度的回调,因此可以扩展自己的进度条事件。
public interface ProgressCallback{ public abstract void onProcess(boolean lengthComputable, double loaded, double total); }
然后怎样触发上传呢,触发FileUploadExt的click即可:
addButton.addSelectHandler(new SelectHandler() { @Override public void onSelect(SelectEvent event) { getFileUploadExt().click(); } });
然后就是编写单个上传以外的触发事件了。我的策略是定义一个fileQueue用来记录待传输的文件,传输完一个从队列剔除一个。
private LinkedList<File> fileQueue = new LinkedList<File>();
然后每个上传的循环里是这样的:
Scheduler.get().scheduleFixedDelay(new RepeatingCommand(){ @Override public boolean execute() { if(!CcBaseFileUpload.this.isAttached() || fileQueue.isEmpty()){ //终止传输条件,关闭或者队列传输完成 return false; } if(workFile != null){ //retry条件:一个传输完成,等待下一个传输 return true; } workFile = fileQueue.remove(); onNextFileUpload((totalFileCount - fileQueue.size()), totalFileCount); try{ CcUploadFormData ccUploadFormData = CcUploadFormData.create(); ccUploadFormData.append("file", workFile); xmlHttpRequest.open("POST", UPLOAD_URL); final RequestCallback callback = new RequestCallback(){ @Override public void onResponseReceived(Request request, Response response) { switch(response.getStatusCode()){ case 404: break; case 500: JSONObject object500 = JSONParser.parseStrict(response.getText()).isObject(); if(object500 != null){ JSONValue errorObjectResp = object500.get("errorObjectResp"); if(errorObjectResp != null){ String errorText = errorObjectResp.isObject().get("errorText").isString().stringValue(); ViewUtil.error("错误", errorText.contains("SizeLimitExceededException") ? "上传文件超过限制" : errorText); } fileUploadCompleteHandler.onUploadError(workFile); } break; case 200: JSONObject object200 = JSONParser.parseStrict(response.getText()).isObject(); if(object200 != null){ FileInfDto fileInfBarDto = new FileInfDto(); fileInfBarDto.setFileNm(object200.get(FileInf.FILE_NM).isString().stringValue()); fileInfBarDto.setFilePath(object200.get(FileInf.FILE_PATH).isString().stringValue()); fileInfBarDto.setFileTypeNm(object200.get(FileInf.FILE_TYPE_NM).isString().stringValue()); fileInfBarDto.setFileUrl(object200.get(FileInfDto.FILE_URL).isString().stringValue()); if(fileUploadCompleteHandler != null){ fileUploadCompleteHandler.onUploadComplete(fileInfBarDto); } } break; } if(fileQueue.isEmpty()){ onQueueUploadFinish(); } } @Override public void onError(Request request, Throwable exception) { ViewUtil.error("上传失败", exception.toString()); } }; final CcRequest request = new CcRequest(xmlHttpRequest, 1800*1000, callback); //传输限制30分钟 xmlHttpRequest.setOnReadyStateChange(new ReadyStateChangeHandler() { public void onReadyStateChange(XMLHttpRequest xhr) { if (xhr.getReadyState() == XMLHttpRequest.DONE) { xhr.clearOnReadyStateChange(); request.fireOnResponseReceived(callback); } } });
- 主要的流程和核心思想讲解到这,离目标还有一段路,剩下的UI布局及交互代码就要靠各位自己来完成了。