JavaScript File API应用——如何设计和实现文件上传组件

(1)精简“带进度条文件上传组件”的设计与实现

XMLHttpRequest第二版为我们提供了便利的progress事件,通过为xhr.upload.onprogress指定处理函数,可以快速制作进度条,JQuery插件参考代码如下:

(function($) {
    $.fn.uploader = function(userOptions) {
        var options = {
            id : "uploader",
            url : "uploadAction.action?fileName=",//处理上传文件的服务器url
            width : 540,//整个控件div层的宽度
            height : 180
        //整个控件div层的高度
        };
        $.extend(options, userOptions);

        var createUploader = function(where) {
            var uploader = $("<div id=‘" + options.id + "‘></div>").css({
                "display" : "block",
                "position" : "relative",
                "width" : options.width,
                "height" : options.height,
                "overflow" : "hidden"
            });

            //创建上传文件控件
            var fileUI = $("<input id=‘file‘ type=‘file‘>").addClass("");
            uploader.append(fileUI);

            //创建上传文件按钮,并实现点击事件
            var uploadBtn = $("<input id=‘uploadBtn‘ type=‘button‘ value=‘上传‘>")
                    .click(
                            function() {
                                var fileUI = document.getElementById("file");
                                if (fileUI.files.length > 0) {
                                    var file = fileUI.files[0];//取出文件blob

                                    var xhr = new XMLHttpRequest();
                                    //指定处理上传进度的函数
                                    xhr.upload.onprogress = updateProgress;
                                    //处理上传
                                    xhr.open("POST", options.url
                                            + encodeURIComponent(file.name),
                                            true);
                                    xhr.overrideMimeType("application/octet-stream");
                                    xhr.send(file);
                                    //处理响应
                                    xhr.onreadystatechange = function() {
                                        if (xhr.readyState == 4
                                                && xhr.status == 200) {
                                            console.log(xhr.responseText);
                                        } else {
                                            console.log(xhr.statusText);
                                        }
                                    };
                                    console.log("upload complete");
                                } else {
                                    alert("请选择上传文件!");
                                }

                            });
            uploader.append(uploadBtn);

            var progressBar = $("<div></div>").addClass("progressBar");
            var progress = $("<p></p>");
            progressBar.append(progress);
            uploader.append(progressBar);

            var updateProgress = function(event) {
                if (event.lengthComputable) {
                    var percentage = Math.round((event.loaded * 100)
                            / event.total);
                    console.log("percentage:" + percentage);
                    if (percentage < 100) {
                        progress.css("width", (percentage * 2) + "px");
                        progress.text(percentage + "%");
                    } else {
                        progress.css("width", "200px");
                        progress.text("100%");
                    }
                }
            };
            where.append(uploader);
        }
        createUploader(this);
    }
}(jQuery));

服务器端可以利用struts2对上传请求进行处理,将其保存到文件系统,参考代码如下:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionSupport;

public class FileUploadAction extends ActionSupport {

    private static String SEPARATOR = File.separator;

    public InputStream inputStream = null;

    public InputStream getIn() {
        return inputStream;
    }

    @Override
    public String execute() {

        BufferedInputStream fileIn = null;
        BufferedOutputStream fileOut = null;
        String fileName = ServletActionContext.getRequest().getParameter("fileName");
        String path = ServletActionContext.getRequest().getSession().getServletContext().getRealPath("/");

        try{
            fileIn = new BufferedInputStream(
                    ServletActionContext.getRequest().getInputStream());
            File file = new File(path + SEPARATOR + fileName);

            byte[] buf = new byte[1024];
            fileOut = new BufferedOutputStream(
                    new FileOutputStream(file, true));
            int length = 0;
            while ((length = fileIn.read(buf)) != -1) {
                fileOut.write(buf, 0, length);
            }

        } catch(IOException e){

            e.printStackTrace();
            return ERROR;

        } finally {
            if(fileIn != null){
                try {
                    fileIn.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if(fileOut != null){
                try {
                    fileOut.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

        try {
            inputStream = new ByteArrayInputStream("upload finished".getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return SUCCESS;
    }
}

Struts2配置参考:

<package name="myjson" namespace="" extends="json-default">
    <action name="uploadAction" class="FileUploadAction">
        <result type="stream">
            <param name="contentType">
                text/html
            </param>
            <param name="inputName">
                inputStream
            </param>
        </result>
    </action>
</package>

进度条可以简单的利用div进行实现,参考样式如下:

.progressBar {
    width: 200px;
    height: 20px;
    border: 1px solid #000;
    -moz-border-radius: 8px;
    margin-bottom: 10px;
}

.progressBar p {
    margin: 0px;
    width: 0px;
    height: 20px;
    -moz-border-radius: 8px;
    background-color: #00FF00;
}

使用插件实现文件上传功能:

(2)改进一下,如何实现暂停和重启

暂停和重启功能,前端需要依赖Blob对象(事实上一个File原型链的上一层就是Blob)的分割(slice)方法,典型的分割函数如下:

var sliceBlob = function(blob, start, end, type) {
             var force_saveable_type = ‘application/octet-stream‘;
             if (type && type == force_saveable_type) {
                 var slice = blob.slice || blob.webkitSlice || blob.mozSlice;
                 blob = slice.call(blob, start, end, type);
             }
             return blob;
         }

通过分割函数我们就可以完成对文件进行分块上传并且控制是否暂停和进度条的显示逻辑,JQuery插件参考代码如下:

(function($){
     $.fn.uploader = function(userOptions){
         var options = {
             id : "uploader",
             url: "uploadAction.action?fileName=",
             width : 540,//整个控件div层的宽度
             height : 180//整个控件div层的高度
         };
         $.extend(options, userOptions);

         var sliceBlob = function(blob, start, end, type) {
             var force_saveable_type = ‘application/octet-stream‘;
             if (type && type == force_saveable_type) {
                 var slice = blob.slice || blob.webkitSlice || blob.mozSlice;
                 blob = slice.call(blob, start, end, type);
             }
             return blob;
         }

         var sender = {
             pause : false,
             send : function(uploadXhr){
                 uploadXhr.onreadystatechange = function(){
                     if ( uploadXhr.readyState == 4 && uploadXhr.status == 200 ) {

                         sender.updateProgress(sender.file.size, sender.end);
                         sender.send(uploadXhr);//如果HTTP响应状态正常,继续发送文件
                         console.log( uploadXhr.responseText );
                     } else if(uploadXhr.status == 500){
                         console.log( uploadXhr.statusText );
                     } else {
                         console.log( uploadXhr.statusText );
                     }
                 };
                 if(this.start < this.file.size && !this.pause){//判断是否为文件末尾或用户暂停操作

                     uploadXhr.open("POST",
                         this.url + encodeURIComponent(this.file.name) + "&uuid=" + this.uuid + "&status=" + this.finished,
                         true);
                     uploadXhr.overrideMimeType("application/octet-stream");

                     uploadXhr.send(sliceBlob(this.file, this.start, this.end, "application/octet-stream"));//使用sliceBlob对文件进行分割上传

                     this.start = this.end;

                     if((this.end + 1000 * 1000) > this.file.size){//判断是否分割到文件末尾
                         this.end = this.file.size;//最后分割到文件末尾
                         this.finished = true;
                         $("#uploadBtn").attr("disabled","");
                     } else {
                         this.end += 1000 * 1000;//按固定大小继续连续分割文件块
                     }

                     return;

                 } else {

                     return;
                 }
             },
             updateProgress : function (total, loaded) {
                 var progressBar = $(".progressBar p:first");
                 var percentage = Math.round(( loaded * 100) / total);
                 console.log("percentage:" + percentage);
                 if (percentage < 100)
                 {
                     progressBar.css("width",(percentage * 2) + "px");
                     progressBar.text(percentage + "%");
                 } else {
                     progressBar.css("width", "200px");
                     progressBar.text("100%");
                 }
             },
             uuid : "",
             file : null,
             start : 0,
             end : 1000 * 1000,
             url : options.url,
             finished : false
         };
         var createUploader = function(where) {
             var uploader = $("<div id=‘" + options.id + "‘></div>")
                 .css({
                     "display": "block",
                     "position": "relative",
                     "width": options.width,
                     "height": options.height,
                     "overflow": "hidden"
                 });

             var fileUI = $("<input id=‘file‘ type=‘file‘>").addClass("");
             uploader.append(fileUI);

             /**
              * 创建上传按钮
              */
             var uploadBtn = $("<input id=‘uploadBtn‘ type=‘button‘ value=‘上传‘>").click(
                 function () {
                     var fileUI = document.getElementById("file");
                     if(fileUI.files.length > 0){
                         uploadBtn.attr("disabled","disabled");

                         var file = fileUI.files[0];
                         sender.file = file;
                         $.get("getUuidAction.action?fileName=" + encodeURIComponent(file.name), function(data, status){//上传生成uuid
                             if(status == "success") {
                                 var uuid = data;
                                 console.log(data);

                                 var uploadXhr = new XMLHttpRequest();
                                 sender.uuid = uuid;
                                 sender.start = 0;
                                 sender.end = 1000 * 1000;
                                 sender.url = options.url;
                                 sender.finished = false;
                                 sender.send(uploadXhr);

                             } else {

                                 console.log(status);
                             }
                         });
                         console.log("upload complete");
                     } else {
                         alert("请选择上传文件!");
                     }

                 }
             );
             uploader.append(uploadBtn);

             /**
              * 创建暂停按钮
              */
             var pauseBtn = $("<input id=‘pauseBtn‘ type=‘button‘ value=‘暂停‘>").click(function(){
                 if(pauseBtn.attr("value") == "暂停") {
                     sender.pause = true;
                     pauseBtn.attr("value","启动");
                 } else {
                     sender.pause = false;
                     var uploadXhr = new XMLHttpRequest();
                     console.log(sender.pause);
                     sender.send(uploadXhr);
                     pauseBtn.attr("value","暂停");
                 }
             });
             uploader.append(pauseBtn);

             var progressBar = $("<div></div>").addClass("progressBar");
             var progress = $("<p></p>");
             progressBar.append(progress);
             uploader.append(progressBar);

             where.append(uploader);

         }
         //创建整个控件
         createUploader(this);
     }
 }(jQuery));

后台构建处理逻辑,需要通过简单地建立FileUploader对象池来保持上传会话的一致性,不会因为暂停和重启操作破坏文件完整性,为每一个上传文件指定UUID来确保唯一性,对象池实际是一个ConcurrentHashMap类构造的键值对象,参考代码如下:

public class PauseableFileUploaderMap {
    private static int MAX_FILE_NUM = 100;

    private static ConcurrentHashMap<String, PauseableFileUploader> objMap =
            new ConcurrentHashMap<String, PauseableFileUploader>(MAX_FILE_NUM);

    public void put(String uuid, PauseableFileUploader uploader){
        objMap.put(uuid, uploader);
    }

    public void remove(String uuid){
        objMap.remove(uuid);
    }

    public PauseableFileUploader get(String uuid){
        return objMap.get(uuid);
    }
}

在控制时,采用有限实例模式方法来实现对FileUploader对象的使用,参考代码如下:

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class PauseableFileUploader {

    private static int MAX_BUFFER = 1024;

    private PauseableFileUploader(){};

    private static PauseableFileUploaderMap map = new PauseableFileUploaderMap();

    /**
     * 有限实例模式,实例按uuid数量建立
     * @param uuid
     * @return uuid对应的PauseableFileUploader对象
     */
    public static PauseableFileUploader getInstance(String uuid){

        PauseableFileUploader uploader = null;

        //Lazy处理
        if((uploader = map.get(uuid)) != null){//存在uuid对应的实例则直接返回该实例
            return uploader;
        } else {//没有,则新建实例
            uploader = new PauseableFileUploader();
            map.put(uuid, uploader);
            return uploader;
        }
    } 

    /**
     * 销毁uuid对应的PauseableFileUploader对象
     * @param uuid
     */
    public void destoryInstance(String uuid){
        map.remove(uuid);
    }

    /**
     * 处理上传
     * @param file 待保存的文件对象
     * @param blob 文件上传分块流
     */
    public void upload(File file, InputStream blob){

        byte[] buf = new byte[MAX_BUFFER];
        BufferedInputStream fileIn = null;
        BufferedOutputStream fileOut = null;
        try {
            fileIn = new BufferedInputStream(blob);
            fileOut = new BufferedOutputStream(new FileOutputStream(file, true));
            int length = 0;
            while ((length = fileIn.read(buf)) != -1) {

                fileOut.write(buf, 0, length);
            }
            fileOut.flush();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileIn != null) {
                try {
                    fileIn.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            if (fileOut != null) {
                try {
                    fileOut.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }

    }
}

服务器端可以仍然利用struts2对上传请求进行处理,参考代码如下:

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionSupport;

public class PauseableFileUploadAction extends ActionSupport{
    private static String SEPARATOR = File.separator;
    public InputStream inputStream = null;

    public InputStream getInputStream() {
        return inputStream;
    }
    /**
     * 生成UUID,并根据UUID新建目录和文件
     * @return struts2状态
     */
    public String getUuid(){
        try {
            //Linux OS:
            //String fileName = new String(ServletActionContext.getRequest().getParameter("fileName").getBytes("ISO8859-1"));
            String fileName = ServletActionContext.getRequest().getParameter("fileName");
            String path = ServletActionContext.getRequest().getSession().getServletContext().getRealPath("/");

            //生成UUID并创建上传目录
            UUID uuid = UUID.randomUUID();
            File dir = new File(path + uuid.toString());
            if(dir.exists()){
                dir.delete();
            }
            dir.mkdir();

            //创建上传文件
            File file = new File(path + uuid.toString() + SEPARATOR + fileName);
            file.createNewFile();

            //Response返回UUID
            inputStream = new ByteArrayInputStream(uuid.toString().getBytes());

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return ERROR;
        } finally {

            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return SUCCESS;
    }

    /**
     * 处理文件上传
     * @return struts2状态
     */
    public String upload(){
        try {

            String fileName = new String(ServletActionContext.getRequest().getParameter("fileName"));
            String uuid = ServletActionContext.getRequest().getParameter("uuid");
            String path = ServletActionContext.getRequest().getSession().getServletContext().getRealPath("/");

            File file = new File(path + uuid + SEPARATOR + fileName);

            //获取上传UUID对应的PauseableFileUploader对象
            PauseableFileUploader pfu = PauseableFileUploader.getInstance(uuid);
            //处理分块上传
            synchronized(pfu){
                pfu.upload(file, ServletActionContext.getRequest().getInputStream());

                inputStream = new ByteArrayInputStream(uuid.toString().getBytes());
            }

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return ERROR;
        } finally{
            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        return SUCCESS;
    }
}

Struts2配置参考:

<package name="myjson" namespace="" extends="json-default">
    <action name="getUuidAction" class="PauseableFileUploadAction" method="getUuid">
        <result type="stream">
        <param name="contentType">
                    text/html
                </param>
                <param name="inputName">
                    inputStream
                </param>
        </result>
    </action>
    <action name="uploadAction" class="PauseableFileUploadAction" method="upload">
        <result type="stream">
        <param name="contentType">
                    text/html
                </param>
                <param name="inputName">
                    inputStream
                </param>
        </result>
    </action>
</package>

使用插件实现文件上传功能:

(3)扩展思路,如何实现并行异步上传

我们为什么不一口气将所有的文件分块发送给服务器处理呢?

实现分块并行异步上传,需要处理一些额外的细节:

  • 由于浏览器端JavaScript的处理是单线程的,多个XMLHttpRequest对象一起向服务器端发送请求会造成“假死”现象,因此,需要调节发送间隔;
  • 服务器端需要处理分块发送顺序问题,来保证文件拼接顺序的正确;
  • 暂停和进度条的控制逻辑需要交予服务器进行处理。

    和上节一样仍需要一个FileUploader对象池来保持上传会话的一致性,不同的是FileUploader对象中的上传方法只是一个伪方法,将所有分块按顺序号保存到一个Map中,以待真正的处理方法将其按顺序保存到文件系统,参考代码如下:

public class MultiFileUploader {

    private static MultiFileUploaderMap map = new MultiFileUploaderMap();

    private FileInputStreamMap inputStreamMap = new FileInputStreamMap();
    private MultiFileUploadHandleThread thread = new MultiFileUploadHandleThread();
    private int fileUploadProgress = 0;

    private MultiFileUploader(){};

    /*
     * 有限实例模式,实例按uuid数量建立
     */
    public static MultiFileUploader getInstance(String uuid){

        MultiFileUploader uploader = null;

        if((uploader = map.get(uuid)) != null){//存在uuid对应的实例则直接返回该实例
            return uploader;
        } else {//没有,则新建实例
            uploader = new MultiFileUploader();
            map.put(uuid, uploader);
            return uploader;
        }
    } 

    public void destoryInstance(String uuid){
        map.remove(uuid);
    }
    /**
     * 伪上传处理
     * @param seq  分块顺序号
     * @param blob 上传的文件分块
     */
    public void upload(int seq, byte[] blob){
        //伪上传处理,将文件分块blob放入内存中,真实的处理由UploadHandlerThread完成
        inputStreamMap.put(seq, blob);
    }

    public FileInputStreamMap getInputStreamMap() {
        return inputStreamMap;
    }

    public MultiFileUploadHandleThread getHandleThread() {
        return thread;
    }

    public int getFileUploadProgress() {
        return fileUploadProgress;
    }

    public void setFileUploadProgress(int fileUploadProgress) {
        this.fileUploadProgress = fileUploadProgress;
    }

}

真正的文件保存方法是在创建文件UUID时,启动一个线程进行定时轮询分块Map来实现的,该线程还要负责处理对暂停和进度条的控制,参考代码如下:

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class MultiFileUploadHandleThread extends Thread {

    private volatile Thread linker = null;
    private static int MAX_BUFFER = 1024;
    private static int SLEEP_TIME = 10;

    private Object lock = new Object();
    private File file = null;
    private String uuid = null;
    private int num;

    private boolean pause = false;
    /**
     * 初始化线程
     * @param file 待保存的文件对象
     * @param uuid 对应的uuid
     * @param num 一共需处理保存的文件分块总数
     */
    public void init(File file, String uuid, int num){
        this.file = file;
        this.uuid = uuid;
        this.num = num;
    }

    @Override
    public void start() {
        linker = new Thread(this);
        linker.start();
    }

    @Override
    public void run() {

        Thread thread = Thread.currentThread();
        MultiFileUploader mfu = MultiFileUploader.getInstance(uuid);

        int seq = 1;

        while (seq != num + 1 && thread == linker) {
            // 轮询处理

            synchronized(lock){//利用锁来处理暂停
                if( pause ){
                    try {

                        lock.wait();

                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                        break;//使用interrupt停止该线程,跳出循环
                    }
                }
            }

            try {
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                break;//使用interrupt停止该线程,跳出循环
            }

            if (mfu.getInputStreamMap().get(seq) != null) {

                try {
                    upload(file, mfu.getInputStreamMap().get(seq));
                    mfu.setFileUploadProgress(progress(seq));

                } catch (IOException e) {
                    //处理IO异常,销毁对象,并结束线程
                    mfu.destoryInstance(uuid);
                    e.printStackTrace();
                    break;
                }
                //保存分块后,清除引用
                mfu.getInputStreamMap().remove(seq);

                seq++;//顺序处理下一个分块
            }
        }

        //需要客户端判断
        //mfu.destoryInstance(uuid);
    }

    /**
     * 终止线程
     */
    public void stopThread() {
        Thread tmpThread = linker;
        linker = null;
        if (tmpThread != null) {
            tmpThread.interrupt();
        }
    }

    /**
     * 处理文件上传
     * @param file 待保存的文件对象
     * @param blob 文件分块
     * @throws IOException
     */
    private void upload(File file, byte[] blob) throws IOException{
        BufferedOutputStream fileOut = null;
        ByteArrayInputStream fileIn = null;

        byte[] buf = new byte[MAX_BUFFER];
        try {

            fileIn = new ByteArrayInputStream(blob);
            fileOut = new BufferedOutputStream(new FileOutputStream(file, true));

            int length = 0;
            while ((length = fileIn.read(buf)) != -1) {
                fileOut.write(buf, 0, length);
            }

            fileOut.flush();

        } finally {
            if (fileIn != null) {

                fileIn.close();

            }

            if (fileOut != null) {

                fileOut.close();

            }
        }

    }

    /**
     * 计算上传进度
     * @param seq 上传分块的顺序号
     * @return
     */
    private int progress(int seq){

        return (int)((double)seq / this.num * 100);
    }

    /**
     * 暂停线程
     */
    public void pause() {
        this.pause = true;
    }

    /**
     * 重启线程
     */
    public void restart(){
        this.pause = false;
        synchronized(lock){

            lock.notify();
        }
    }
}

服务器端可以仍然利用struts2对上传请求进行处理,参考代码如下:

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.UUID;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionSupport;

public class MultiFileUploadAction extends ActionSupport{
    private static String SEPARATOR = File.separator;

    public InputStream inputStream = null;

    public InputStream getInputStream() {
        return inputStream;
    }

    /**
     * 生成UUID,并根据UUID新建目录和文件
     * @return struts2状态
     */
    public String getUuid(){
        try {
            //Linux OS:
            //String fileName = new String(ServletActionContext.getRequest().getParameter("fileName").getBytes("ISO8859-1"));
            String fileName = ServletActionContext.getRequest().getParameter("fileName");
            String path = ServletActionContext.getRequest().getSession().getServletContext().getRealPath("/");
            int num = Double.valueOf(ServletActionContext.getRequest().getParameter("num")).intValue();

            UUID uuid = UUID.randomUUID();
            File dir = new File(path + uuid.toString());
            if(dir.exists()){
                dir.delete();
            }
            dir.mkdir();

            File file = new File(path + uuid.toString() + SEPARATOR + fileName);
            file.createNewFile();

            MultiFileUploader mfu = MultiFileUploader.getInstance(uuid.toString());
            MultiFileUploadHandleThread  thread = mfu.getHandleThread();
            thread.init(file, uuid.toString(), num);
            thread.start();

            inputStream = new ByteArrayInputStream(uuid.toString().getBytes());

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            return ERROR;
        } finally {

            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return SUCCESS;
    }

    /**
     * 处理文件上传
     * @return struts2状态
     */
    public String upload(){
        try {

            String uuid = ServletActionContext.getRequest().getParameter("uuid");
            int seq = Integer.valueOf(ServletActionContext.getRequest().getParameter("seq"));//该分块顺序号
            MultiFileUploader mfu = MultiFileUploader.getInstance(uuid);
            BufferedInputStream in = null;

            ByteArrayOutputStream out = null;
            ByteBuffer bb = ByteBuffer.allocateDirect(1000 * 1000);
            byte[] buf = new byte[1024];
                try {

                    synchronized(mfu){

                        in = new BufferedInputStream(ServletActionContext.getRequest().getInputStream());
                        //也可以使用ByteArrayOutputStream处理文件分块输入流,但使用ByteBuffer可以在JVM外部申请额外内存
                        //out = new ByteArrayOutputStream(1024);
                        int length = 0;
                        bb.clear();
                        while ((length = in.read(buf)) != -1) {

                            //out.write(buf, 0, length);
                            bb.put(buf, 0, length);
                        }
                        //byte[] content = out.toByteArray();
                        byte[] content = new byte[bb.position()];
                        bb.flip();
                        bb.get(content);
                        mfu.upload(seq, content);
                    }

                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }

            inputStream = new ByteArrayInputStream(uuid.toString().getBytes());

        } finally{

            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

        return SUCCESS;
    }
    /**
     * 处理暂停
     * @return struts2状态
     */
    public String pause(){

        String uuid = ServletActionContext.getRequest().getParameter("uuid");
        MultiFileUploader mfu = MultiFileUploader.getInstance(uuid);
        mfu.getHandleThread().pause();
        try{

            inputStream = new ByteArrayInputStream(uuid.toString().getBytes());

        }finally{

            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        return SUCCESS;
    }

    /**
     * 处理重启上传
     * @return struts2状态
     */
    public String restart(){

        String uuid = ServletActionContext.getRequest().getParameter("uuid");
        MultiFileUploader mfu = MultiFileUploader.getInstance(uuid);
        mfu.getHandleThread().restart();
        try{

            inputStream = new ByteArrayInputStream(uuid.toString().getBytes());

        }finally{

            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return SUCCESS;
    }

    /**
     * 获取服务器端保存进度
     * @return struts2状态
     */
    public String getProgress(){
        String uuid = ServletActionContext.getRequest().getParameter("uuid");
        MultiFileUploader mfu = MultiFileUploader.getInstance(uuid);
        int progressStatus = mfu.getFileUploadProgress();
        try{

            inputStream = new ByteArrayInputStream(String.valueOf(progressStatus).getBytes());

        }finally{

            try {
                inputStream.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return SUCCESS;
    }
}

Struts2配置参考:

<package name="myjson" namespace="" extends="json-default">
    <action name="getUuidAction" class="MultiFileUploadAction" method="getUuid">
        <result type="stream">
        <param name="contentType">
                    text/html
        </param>
        <param name="inputName">
                    inputStream
        </param>
        </result>
        </action>
        <action name="uploadAction" class="MultiFileUploadAction" method="upload">
        <result type="stream">
        <param name="contentType">
                    text/html
                </param>
                <param name="inputName">
                    inputStream
                </param>
        </result>
        </action>
        <action name="getProgressAction" class="MultiFileUploadAction" method="getProgress">
        <result type="stream">
        <param name="contentType">
                    text/html
                </param>
                <param name="inputName">
                    inputStream
                </param>
        </result>
        </action>
        <action name="pauseAction" class="MultiFileUploadAction" method="pause">
        <result type="stream">
        <param name="contentType">
                    text/html
                </param>
                <param name="inputName">
                    inputStream
                </param>
        </result>
        </action>
        <action name="restartAction" class="MultiFileUploadAction" method="restart">
        <result type="stream">
        <param name="contentType">
                    text/html
                </param>
                <param name="inputName">
                    inputStream
                </param>
        </result>
        </action>
</package>

最后,前端JQuery插件需要根据后端的变化加以变化,参考代码如下:

(function($) {
    $.fn.uploader = function(userOptions) {
        var options = {
            id : "uploader",
            url : "uploadAction.action?fileName=",
            width : 540,//整个控件div层的宽度
            height : 180
        //整个控件div层的高度
        };
        $.extend(options, userOptions);

        var sliceBlob = function(blob, start, end, type) {
            var force_saveable_type = ‘application/octet-stream‘;
            if (type && type == force_saveable_type) {
                var slice = blob.slice || blob.webkitSlice || blob.mozSlice;
                blob = slice.call(blob, start, end, type);
            }
            return blob;
        };

        var sender = {
            pause : false,
            send : function() {
                var num = this.file.size / this.end + 1;
                var progressBar = $(".progressBar p:first");

                while (this.start < this.file.size && !this.pause) {//并发处理文件分块上传

                    var uploadXhr = new XMLHttpRequest();
                    var size = this.end - this.start;
                    var progress = 1;
                    uploadXhr.open("POST", this.url
                            + encodeURIComponent(this.file.name) + "&uuid="
                            + this.uuid + "&end=" + false + "&seq=" + this.seq
                            + "&size=" + size, true);
                    uploadXhr.overrideMimeType("application/octet-stream");

                    uploadXhr.onreadystatechange = function() {
                        if (uploadXhr.readyState == 4
                                && uploadXhr.status == 200) {
                            console.log(uploadXhr.responseText);
                        } else if (uploadXhr.status == 500) {
                            console.log(uploadXhr.statusText);
                            $("#uploadBtn").attr("disabled", "");

                        } else {
                            console.log(uploadXhr.statusText);
                        }
                    };

                    //模拟前端异步,以保证可以有效向服务器发送暂停/重启命令,以及接收上传进度信息
                    //注:如果不需要控制暂停和重启可以去除该模拟过程
                    setTimeout((function(file, start, end, uploadXhr) {
                        return function() {
                            uploadXhr.send(sliceBlob(file, start, end,
                                    "application/octet-stream"));
                        }
                    })(this.file, this.start, this.end, uploadXhr),
                            50 * this.seq);

                    this.start = this.end;
                    this.seq++;
                    if ((this.end + 1000 * 1000) > this.file.size) {
                        this.end = this.file.size;
                    } else {
                        this.end += 1000 * 1000;
                    }

                }

            },
            updateProgress : function(num) { //从服务器端获取进度条数据来更新进度条
                var progressBar = $(".progressBar p:first");
                var id = setInterval(function() {

                    $.get("getProgressAction.action?uuid=" + sender.uuid,
                            function(data, status) {
                                if (status == "success" && !sender.pause) {

                                    var percentage = Math.floor(data);

                                    if (percentage < 100) {
                                        progressBar.css("width",
                                                (percentage * 2) + "px");
                                        progressBar.text(percentage + "%");
                                    } else {
                                        progressBar.css("width", "200px");
                                        progressBar.text("100%");
                                        clearInterval(id);
                                        $("#uploadBtn").attr("disabled", "");
                                    }
                                } else {
                                    console.log(status);
                                }

                            });
                }, 200);
                return id;
            },
            uuid : "",
            file : null,
            seq : 1,
            start : 0,
            end : 1000 * 1000,
            url : options.url,
            finished : false
        };
        var createUploader = function(where) {
            var uploader = $("<div id=‘" + options.id + "‘></div>").css({
                "display" : "block",
                "position" : "relative",
                "width" : options.width,
                "height" : options.height,
                "overflow" : "hidden"
            });

            var fileUI = $("<input id=‘file‘ type=‘file‘>").addClass("");
            uploader.append(fileUI);

            /**
             * 创建上传按钮
             */
            var uploadBtn = $("<input id=‘uploadBtn‘ type=‘button‘ value=‘上传‘>")
                    .click(
                            function() {
                                var fileUI = document.getElementById("file");
                                if (fileUI.files.length > 0) {
                                    uploadBtn.attr("disabled", "disabled");
                                    var file = fileUI.files[0];
                                    sender.file = file;
                                    var num = file.size / (1000 * 1000) + 1;
                                    $.get("getUuidAction.action?fileName="
                                            + encodeURIComponent(file.name)
                                            + "&num=" + num, function(data,
                                            status) {//上传生成uuid
                                        if (status == "success") {
                                            var uuid = data;
                                            console.log(data);

                                            sender.uuid = uuid;
                                            sender.start = 0;
                                            sender.end = 1000 * 1000;
                                            sender.url = options.url;
                                            sender.finished = false;
                                            sender.seq = 1;
                                            sender.updateProgress(num);
                                            sender.send();

                                        } else {

                                            console.log(status);
                                        }
                                    });
                                    console.log("upload complete");
                                } else {
                                    alert("请选择上传文件!");
                                }

                            });
            uploader.append(uploadBtn);

            /**
             * 创建暂停按钮
             */
            var pauseBtn = $("<input id=‘pauseBtn‘ type=‘button‘ value=‘暂停‘>")
                    .click(function() {
                        if (pauseBtn.attr("value") == "暂停") {
                            sender.pause = true;
                            $.get("pauseAction.action?uuid=" + sender.uuid);
                            pauseBtn.attr("value", "启动");
                        } else {
                            sender.pause = false;
                            console.log(sender.pause);
                            sender.send();
                            $.get("restartAction.action?uuid=" + sender.uuid);
                            pauseBtn.attr("value", "暂停");
                        }
                    });
            uploader.append(pauseBtn);

            var progressBar = $("<div></div>").addClass("progressBar");
            var progress = $("<p></p>");
            progressBar.append(progress);
            uploader.append(progressBar);

            where.append(uploader);

        }
        //创建整个控件
        createUploader(this);
    }
}(jQuery));

(4)还需要什么?

  • 分块上传带来的问题,比如增大了文件完整性被破坏的风险,需要增加校验功能来监视上传前后的分块是否一致;
  • 取消上传过程的功能;
  • 拖拽上传操作的功能;
  • 你还能做的更多……

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-13 04:31:17

JavaScript File API应用——如何设计和实现文件上传组件的相关文章

ASP.NET Core WEB API 使用element-ui文件上传组件el-upload执行手动文件文件,并在文件上传后清空文件

前言: 从开始学习Vue到使用element-ui-admin已经有将近快两年的时间了,在之前的开发中使用element-ui上传组件el-upload都是直接使用文件选取后立即选择上传,今天刚好做了一个和之前类似的文件选择上传的需求,不过这次是需要手动点击按钮把文件上传到服务器中进行数据导入,而且最多只能够选择一个文件进行上传,上传成功后需要对file-list中的文件列表数据进行清空操作,在这里服务端使用的是ASP.NET Core WEB API来进行文件流数据接收和保存. 一.简单概述e

resumable.js —— 基于 HTML 5 File API 的文件上传组件 支持续传后台c#实现

在git上提供了java.nodejs.c#后台服务方式:在这里我要用c#作为后台服务:地址请见:https://github.com/23/resumable.js 我现在visual studio的环境是2013,mvc4,framwork4.0:我们来看git上给出的c#示例: samples/DotNET/ 在文件中包含两个文件Controllers Models,在两个文件里各有一个文件: 我把他部署到自己项目里边,却没有达到自己预定的效果,我是这样更改的: 一.我先把我的类库项目调成

jQuery File Upload文件上传插件使用

jQuery File Upload 是一个Jquery文件上传组件,支持多文件上传.取消.删除,上传前缩略图预览.列表显示图片大小,支持上传进度条显示:支持各种动态语言开发的服务器端.官网链接:https://github.com/blueimp/jQuery-File-Upload/wiki 特点:拖放支持:上传进度条:图像预览:可定制和可扩展的:兼容任何服务器端应用平台(PHP, Python, Ruby on Rails, Java, Node.js, Go etc.). 使用方法: 1

文件上传下载—servlet API实现

servlet API实现文件上传下载需要的jar包: UploadServlet.java package com.ymw.web.servlet; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.UUID; import javax.servl

用jQuery重置用于文件上传的input (type=&quot;file&quot;)

页面中有如下标签: <input type="file" id="upload"/> 此标签本用于文件上传,现在有需要将其值重置为空.于是想当然地写出如下代码: $('#upload').val(''); 但经测试,该处理方法对IE无效,因为IE不允许javascript改变type为file的input的值,又是让人蛋疼的IE... 在浏览器兼容性方面本人是白痴一个,遂放狗一搜,有解决方案如下: if(ie) { // 此处判断是否是IE $('#up

input file实现多选,限制文件上传类型,图片上传前预览功能

限制上传类型 & 多选:① accept 属性只能与 <input type="file" /> 配合使用.它规定能够通过文件上传进行提交的文件类型. ② multiple 属性规定输入字段可选择多个值. 示例: <!-- image/* 所有图片 image/png png图片 image/jpg jpg图片 image/gif gir动图 application/msword Word文档(.doc) application/vnd.openxmlform

javascript input type=file 文件上传

在JS中,input type=file 是常用的文件上传API,但感觉W3C说的不是很清楚,同时网上的资料也比较乱. 由于做微信开发,所以网页打算尽量少用第三方库或者插件,以加快网页的加载速度.因为微信企业号本身想实现的功能也很纯粹,不需要太多乱七八糟的东西. 我这里只用了JQuery. 总结如下: 1.用户选择文件后,一般只显示一个fakepath,不会显示一个完整的文件目录.用$("input[type='file']")[0].files[0].name即可显示出文件名. 2.

javascript input file文件上传

<body> <input type="file" id="myFile" onchange="beforeUpload()"> <button onclick="selectFile()">上传</button> <script> var fileInput = document.getElementById("myFile"); // 选择上传

7 款基于 JavaScript/AJAX 的文件上传插件

本文整理了7款基于JavaScript和AJAX的文件上传插件,这些插件基本上都能实现以下功能: 多文件上传 拖拽操作 实时上传进度 自定义上传限制 希望能为你的开发工作带来帮助. 1.  jQuery File Upload 具有多文件上传.拖拽.进度条和图像预览功能的文件上传插件,支持跨域.分块.暂停恢复和客户端图像缩放.可与任何服务端平台(如PHP.Python.Ruby on Rails.Java.Node.js.Go等)一起使用,支持标准的HTML表单文件上传. 2.  Pixelco