GWT使用HTML5实现批量上传和进度显示

GWT使用HTML5实现批量上传和进度显示

目标效果

嘿嘿,先放个酸葡萄,看看有没有人来啃。

  1. 准备上传
    准备上传

  2. 批量选择上传文件
    批量选择上传文件

  3. 上传中并显示整体进度
    上传中并显示整体进度

  4. 上传完毕并按格式插入内容
    上传完毕并按格式插入内容

  5. 单文件上传效果
    这里写图片描述
    这里写图片描述
    这里写图片描述

方案

比较了2种方案:
方案1:其实HTML5方案GWT都已经给我们准备好了,就是Elemental.jar的支持代码。但是要使用它,付出的代价就是必须使用SuperDevMode来开发,想想使用Chrome devTool调试模式和全量编译带来的效率影响。做了一个艰难的决定,不用Elemental!
方案2:不用Elemental是否可以用纯javascript?用js写,是非常简单。但是怎样用GWT去干呢?有办法,绝对有办法,办法是折腾出来的。请看下面的实现步骤。

实现步骤

  1. 首先要实现一些HTML5的基础类例如支持多文件及本地文件大小检测的FileUpload及File对象等,我们可以选择lib-gwt-file来实现一些基础的功能(https://www.vectomatic.org/libs/lib-gwt-file),有别人的肩膀,就不用关门造轮子。

  2. 在做所有事情之前,先来看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对象

  3. 把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实现。

  4. 有了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);
        }-*/;
    }
    
  5. 然后服务端怎样接收呢?其实和普通的上传没有两样,都是用multi-part的方式上传。例如spring mvc可以这样写:

    public FileInfDto upload(@RequestParam("file") MultipartFile file){
     //your code
    }
  6. 处理多文件发送。多文件发送怎么办呢?lib-gwt-file很好的处理了这个问题FileUploadExt当设置为multi时,能够通过getFiles()获取到各个File对象。并且能在发送前处理文件大小的判断和文件类型的判断。看看lib-gwt-file的example,在此不再赘述。

  7. 处理进度条这玩意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的侦听。

  8. 使用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);
    }
    
  9. 然后怎样触发上传呢,触发FileUploadExt的click即可:

            addButton.addSelectHandler(new SelectHandler() {
                @Override
                public void onSelect(SelectEvent event) {
                    getFileUploadExt().click();
                }
            });
  10. 然后就是编写单个上传以外的触发事件了。我的策略是定义一个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);
                                }
                              }
                            });
    
  11. 主要的流程和核心思想讲解到这,离目标还有一段路,剩下的UI布局及交互代码就要靠各位自己来完成了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值