HTML 大文件上传

2,800 阅读3分钟

文件上传已是老生常谈了。
文件上传,原始的方法有:

  • form表单提交。
  • 通过iframe+form表单进行模拟异步上传。

由于Http协议的限制,在处理大文件上传会存在超时的现象。在旧的浏览器中是无法读取文件二进制数据。无法将文件以分片的方式进行上传。

最近由于产品需求,在不安装插件的情况下上传大文件(百度webuploader开源项目做得已经很不错了,但在秒传和低版本浏览器上使用flash不太符合要求)。
于是重新撸了一把html大文件上传(整个项目是spring boot搭建)。

先来简单的介绍用了哪些javascript类库

  • File 文件
  • FileRead 读取文件
  • XMLHttpRequest 上传文件
  • FormData 上传时所需数据
  • spark-md5 用于MD5 HASH值计算(第三方插件)

本次实现拖拽上传,用到了页面拖拽事件:ondragenter,ondragleave,ondragover,ondrop

实现思路

  • 建立一个上传队列。
  • 通过拖拽获取得到文件。
  • 为每个文件添加各自的上传方法。
    • 秒传判断
    • 上传分片文件 (后台存放临时分片文件)
    • 合并文件
  • 添加文件到上传队列。

最终效果

20170208961732017-02-08_21-05-45-1.gif

代码部分

html




    
    
    
    
    
        * {
            margin: 0;
            padding: 0;
        }
        .dropZone {
            width: 300px;
            height: 200px;
            line-height: 200px;
            margin: 10px auto;
            border: 2px dashed #ccc;
            color: #cccccc;
            text-align: center;
        }
        .dropZone.dropOver {
            border-color: #000;
            color: #000;
        }
        button {
            margin-right: 10px;
        }
    


drop file

fileList

    MD5

    //md5
        !function (win, sparkMD5) {
            let Md5File = function (options) {
                this._init(options);
                this.fileMd5Hash();
            };
            Md5File.prototype = {
                _init: function (opts) {
                    this.file = opts.file;
                    this.fileSliceLength = opts.fileSliceLength || 1024 * 1024;
                    this.chunks = opts.chunks || 10;
                    this.chunkSize = parseInt(this.file.size / this.chunks, 10);
                    this.currentChunk = 0;
                    this.md5Complete = opts.md5Complete;
                    this.spark = new sparkMD5.ArrayBuffer();
                    this.sparks = [];
                },
                fileMd5Hash: function () {
                    this._readFileMd5Hash();
                },
                _readFileMd5Hash: function () {
                    let self = this;
                    console.group("currentChunk:", self.currentChunk,
                        'chunkSize', self.chunkSize,
                        'fileSize:', self.file.size,
                        'fileSliceLength:', self.fileSliceLength);
                    let start = self.currentChunk * self.chunkSize;
                    let end = Math.min(start + self.chunkSize, self.file.size);
                    if (self.currentChunk < self.chunks) {
                        end = start + self.fileSliceLength;
                    }
                    console.log('start:', start, 'end:', end, 'get chunk:', end - start);
                    console.groupEnd();
                    let fileReader = new FileReader();
                    fileReader.onload = function (e) {
                        let spark = new sparkMD5.ArrayBuffer();
                        spark.append(e.target.result);
                        let tempSpark = spark.end();
                        console.log(tempSpark);
                        self.sparks.push(tempSpark);
                        self.currentChunk++;
                        if (self.currentChunk < self.chunks) {
                            self._readFileMd5Hash();
                        } else {
                            let endSparkStr = self.sparks.join('');
                            console.log(endSparkStr);
                            self.md5Complete && typeof self.md5Complete == 'function' && self.md5Complete(endSparkStr);
                        }
                        /*          self.spark.append(e.target.result);
                         self.currentChunk++;
                         if (self.currentChunk < self.chunks) {
                         self._readFileMd5Hash();
                         } else {
                         self.md5Complete && typeof self.md5Complete == 'function' && self.md5Complete(self.spark.end());
                         }*/
                    };
                    fileReader.readAsArrayBuffer(self.file.slice(start, end));
                }
            };
            win.md5File = function (options) {
                return new Md5File(options);
            }
        }(window, SparkMD5);

    上传类

    //文件上传类
        !function (win) {
            'use strict';
            let FileUpload = function (options) {
                if (typeof options != 'object') {
                    throw Error('options not is object');
                }
                this.init(options);
                if (this.autoUpload) {
                    this.start();
                }
            };
            FileUpload.prototype = {
                init: function (opts) {
                    this.file = opts.file;
                    this.url = opts.url;
                    this.compileFileUrl = opts.compileFileUrl;
                    this.autoUpload = opts.autoUpload || 0;
                    this.chunk = opts.chunk || (10 * 1024 * 1024);
                    this.chunks = Math.ceil(this.file.size / this.chunk);
                    this.currentChunk = 0;
                    this.taskName = opts.taskName || win.uuid();
                    this.isUploading = false;
                    this.isUploaded = false;
                    this.upProgress = opts.upProgress; //上传进度
                    this.upComplete = opts.upComplete; //上传完成
                    this.timeHandle = opts.timeHandle; // 时间处理
                    this.timeInfo = {
                        h: 0,
                        m: 0,
                        s: 0
                    };
                    this.md5FileHash = null;
                    this.remoteMd5FileHash = opts.remoteMd5FileHash; //远程对比文件,是http请求函数
                    this.isSendCompleteFile = opts.isSendCompleteFile || 0; //是否发送合并文件指令
                    return this;
                },
                start: function () {
                    let self = this;
                    //秒传判断
                    if (!self.md5FileHash) {
                        win.md5File({
                            file: self.file,
                            md5Complete: function (hash) {
                                self.md5FileHash = hash;
                                //后台进行判断hash值是否一致,如何一致则直接上传完成。
                                if (self.remoteMd5FileHash && typeof self.remoteMd5FileHash == 'function') {
                                    self.remoteMd5FileHash(self.file.name, self.md5FileHash, function (result) {
                                        if (result) {
                                            self.initInterval();
                                            self.isUploaded = true;
                                            self.isUploading = true;
                                            self.upProgress && typeof self.upProgress == 'function' && self.upProgress(1, 1);
                                            if (self.upComplete && typeof self.upComplete == 'function') {
                                                if (self.time) win.clearInterval(self.time);
                                                self.upComplete();
                                            }
                                        } else {
                                            if (!self.isUploading && !self.isUploaded) {
                                                self.initInterval();
                                                self._upload();
                                            }
                                        }
                                    });
                                }
                            }
                        });
                    } else {
                        //上传
                        if (!self.isUploading && !self.isUploaded) {
                            self.initInterval();
                            self._upload();
                        }
                    }
                },
                pause: function () {
                    if (this.isUploading) {
                        this.xhr && this.xhr.abort();
                        this.isUploading = false;
                        if (this.time) win.clearInterval(this.time);
                    }
                },
                _initXhr: function () {
                    let self = this;
                    self.xhr = new XMLHttpRequest();
                    self.xhrLoad = function () {
                        if (self.end == self.file.size) {
                            self.isUploaded = true;
                            if (self.upComplete && typeof self.upComplete == 'function') {
                                if (self.time) win.clearInterval(self.time);
                                self.upComplete();
                            }
                            self.isSendCompleteFile && self.compileFile();
                        } else {
                            if (self.upProgress && typeof self.upProgress == 'function') {
                                self.upProgress((self.currentChunk + 1), self.chunks);
                            }
                            self.currentChunk++;
                            self._upload();
                        }
                    };
                    self.xhrError = function () {
                        console.log('xhr error');
                    };
                    self.xhrAbort = function () {
                        console.log('xhr abort');
                    };
                    self.xhr.onload = this.xhrLoad;
                    self.xhr.onerror = this.xhrError;
                    self.xhr.onabort = this.xhrAbort;
                },
                _upload: function () {
                    let self = this;
                    self.isUploading = true;
                    self._initXhr();
                    self.xhr.open('POST', self.url);
                    //计算上传开始位置或结束位置
                    self.begin = self.currentChunk * self.chunk;
                    self.end = Math.min((self.begin + self.chunk), self.file.size);
                    let blob = self.file.slice(self.begin, self.end, {type: 'text/plain'});
                    let formData = new FormData();
                    let tempFileName = 'temp-' + self.currentChunk + '-' + self.taskName;
                    formData.append('fileData', blob, tempFileName);
                    formData.append('tempFileName', tempFileName);
                    formData.append('taskName', self.taskName);//用于后台进行判断如果此片已存在则进行删除。
                    formData.append('fileName', self.file.name);
                    formData.append('position', self.begin);
                    formData.append('chunkIndex', (self.currentChunk + 1));
                    formData.append('chunks', self.chunks);
                    self.xhr.send(formData);
                },
                compileFile: function () {
                    let self = this;
                    self._initXhr();
                    self.xhrLoad = function () {
                        console.log(self.xhr.responseText);
                    };
                    self.xhr.onload = self.xhrLoad;
                    self.xhr.open('POST', self.compileFileUrl);
                    let formData = new FormData();
                    formData.append("fileName", (self.file.name));
                    formData.append('taskName', self.taskName);
                    self.xhr.send(formData);
                },
                initInterval: function () {
                    let self = this;
                    if (self.time) win.clearInterval(self.time);
                    self.time = win.setInterval(function () {
                        self.timeInfo.s++;
                        if (self.timeInfo.s == 60) {
                            self.timeInfo.s = 0;
                            self.timeInfo.m++;
                            if (self.timeInfo.m == 60) {
                                self.timeInfo.m = 0;
                                self.timeInfo.h++;
                            }
                        }
                        self.timeHandle && self.timeHandle(self.formatStr());
                    }, 1000);
                },
                formatStr: function () {
                    let _ = this,
                        sY = (_.timeInfo.h < 10) ? '0' + _.timeInfo.h : _.timeInfo.h,
                        sM = (_.timeInfo.m < 10) ? '0' + _.timeInfo.m : _.timeInfo.m,
                        sS = (_.timeInfo.s < 10) ? '0' + _.timeInfo.s : _.timeInfo.s;
                    return sY + ':' + sM + ':' + sS;
                }
            };
            win.fileUpload = function (options) {
                return new FileUpload(options);
            };
        }(window);

    拖拽

    //拖拽
        !function () {
            'use strict';
            let dropZone = document.querySelector('#dropZone');
            let uploadList = document.querySelector('#uploadList');
            dropZone.ondragenter = function () {
                this.className = 'dropZone dropOver';
            };
            dropZone.ondragleave = function () {
                this.className = 'dropZone';
                return false;
            };
            dropZone.ondragover = function () {
                return false;
            };
            let upComplete = function () {
                console.log('file up complete');
                this.progressDivIng.style.width = '100%';
            };
            let upProgress = function (chunkIndex, chunks) {
                console.log('up progress:', chunkIndex, chunks);
                //console.log(this.progressDivIng);
                this.progressDivIng.style.width = (chunkIndex / chunks) * 100 + '%';
            };
            let timeHandle = function (timeStr) {
                this.timeSpan.innerText = timeStr;
            };
            let remoteMd5FileHash = function (fileName, md5FileHash, callback) {
                let xhr = new XMLHttpRequest();
                xhr.onload = function () {
                    let data = xhr.responseText;
                    callback && callback(data == 'ok');
                };
                xhr.open('POST', '/fileHash');
                let formData = new FormData();
                formData.append("fileName", fileName);
                formData.append('md5Hash', md5FileHash);
                xhr.send(formData);
            };
            let tasks = {};
            let uploadHandler = function (files) {
                let x = 0;
                for (x; x < files.length; x++) {
                    let taskName = window.uuid();
                    let file = files[x];
                    tasks[taskName] = window.fileUpload({
                        file: files[x],
                        url: '/upload',
                        compileFileUrl: '/compileFile',
                        autoUpload: 0,
                        isSendCompleteFile: 1,
                        taskName: taskName,
                        upProgress: upProgress,
                        upComplete: upComplete,
                        timeHandle: timeHandle,
                        remoteMd5FileHash: remoteMd5FileHash
                    });
                    let li = document.createElement('li');
                    let startBtn = document.createElement('button');
                    startBtn.innerText = 'START';
                    startBtn.onclick = function () {
                        console.log('start');
                        tasks[taskName].start();
                    };
                    let endBtn = document.createElement('button');
                    endBtn.innerText = 'PAUSE';
                    endBtn.onclick = function () {
                        tasks[taskName].pause();
                    };
                    let cancelBtn = document.createElement('button');
                    cancelBtn.innerText = 'CANCEL';
                    cancelBtn.onclick = function () {
                        let currentUpTask = tasks[taskName];
                        if (currentUpTask.isUploaded) {
                            uploadList.removeChild(document.getElementById(taskName));
                        } else {
                            //TODO:向后台发送取消请求
                            currentUpTask.pause();
                            let xhr = new XMLHttpRequest();
                            xhr.onload = function () {
                                uploadList.removeChild(document.getElementById(taskName));
                            };
                            xhr.open('GET', '/cancel');
                            let formData = new FormData();
                            formData.append('taskName', taskName);
                            xhr.send(formData);
                        }
                    };
                    let progressDiv = document.createElement('div');
                    progressDiv.style.border = '1px solid #ccc';
                    progressDiv.style.height = '10px';
                    progressDiv.style.marginLeft = '5px';
                    progressDiv.style.marginRight = '5px';
                    let progressDivIng = document.createElement('div');
                    progressDivIng.style.background = 'red';
                    progressDivIng.style.width = '0px';
                    progressDivIng.style.height = '10px';
                    progressDiv.appendChild(progressDivIng);
                    let timeSpan = document.createElement('span');
                    timeSpan.innerText = '00:00:00';
                    timeSpan.style.paddingLeft = '10px';
                    timeSpan.style.paddingRight = '10px';
                    li.innerHTML = file.name;
                    li.setAttribute('id', taskName);
                    li.appendChild(timeSpan);
                    li.appendChild(startBtn);
                    li.appendChild(endBtn);
                    li.appendChild(cancelBtn);
                    li.appendChild(progressDiv);
                    uploadList.appendChild(li);
                    tasks[taskName].progressDivIng = progressDivIng;
                    tasks[taskName].timeSpan = timeSpan;
                }
            };
            let acceptFile = function (file) {
                let rExt = /\.\w+$/;
                return rExt.exec(file.name) && (file.size || file.type);
            };
            dropZone.ondrop = function (e) {
                e.preventDefault();
                let files = e.dataTransfer.files;
                let newFiles = [];
                for (let i = 0; i < files.length; i++) {
                    let file = files[i];
                    if (acceptFile(file)) {
                        newFiles.push(file);
                    }
                }
                uploadHandler(newFiles);
                //uploadHandler(e.dataTransfer.files);
            };
        }();