文件的上传与下载

HTML5 大文件上传

用到的库

File 文件
FileRead 读取文件
XMLHttpRequest 上传文件
FormData 上传时所需数据
spark-md5 用于MD5 HASH值计算(第三方插件)
本次实现拖拽上传,用到了页面拖拽事件:ondragenter,ondragleave,ondragover,ondrop

实现思路

建立一个上传队列。
通过拖拽获取得到文件。
为每个文件添加各自的上传方法。
秒传判断
上传分片文件 (后台存放临时分片文件)
合并文件
添加文件到上传队列。
HTML
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title></title>
    <style>
        * {
            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;
        }
    </style>
</head>
<body>
<div id="dropZone" class="dropZone"> drop file</div>
<div>
    <h1>fileList</h1>
    <ul id="uploadList"></ul>
</div>
</body>
</html>

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);
    };
}();

检查浏览器是否完全支持 File API

// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
  // Great success! All the File APIs are supported.
} else {
  alert('The File APIs are not fully supported in this browser.');
}

使用表单输入进行选择

<input type="file" id="files" name="files[]" multiple />
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    var files = evt.target.files; // FileList object

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate.toLocaleDateString(), '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

使用拖放操作进行选择

<div id="drop_zone">Drop files here</div>
<output id="list"></output>

<script>
  function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.dataTransfer.files; // FileList object.

    // files is a FileList of File objects. List some properties.
    var output = [];
    for (var i = 0, f; f = files[i]; i++) {
      output.push('<li><strong>', escape(f.name), '</strong> (', f.type || 'n/a', ') - ',
                  f.size, ' bytes, last modified: ',
                  f.lastModifiedDate.toLocaleDateString(), '</li>');
    }
    document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>';
  }

  function handleDragOver(evt) {
    evt.stopPropagation();
    evt.preventDefault();
    evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
  }

  // Setup the dnd listeners.
  var dropZone = document.getElementById('drop_zone');
  dropZone.addEventListener('dragover', handleDragOver, false);
  dropZone.addEventListener('drop', handleFileSelect, false);
</script>

参考链接:

1.https://developer.mozilla.org/zh-CN/docs/Web/API/FormData/Using_FormData_Objects
2.https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Submitting_forms_and_uploading_files