基本思路
分片上传
利用File
对象的slice
方法,将文件分割成多个Blob
对象,通过XMLHttpRequest
异步上传sessionStorage
中没有记录已完成的Blob
,XMLHttpRequest
上传完成后触发onload
事件,在sessionStorage
中记录刚完成的Blob
。
断点续传即暂停功能
承接上文,在XMLHttpRequest
异步上传时,将XMLHttpRequest
对象保存于一个正在执行的任务容器中,当XMLHttpRequest
上传完成触发onload
事件后将其从任务容器中移除。当想要文件暂停上传时,只需要让任务容器中的每个XMLHttpRequest
对象被abort
即可,重新启动就是重新分片上传,它会略过已经上传的部分,所以可以接着原来的进度进行上传。
前置知识
Web Storage
LocalStorage
与SessionStorage
是HTML5提供的浏览器本地化存储的解决方案之一,存储容量可以达到5MB,使用简单。两者共用相同的API接口,但在作用域和生命周期上具有差别。LocalStorage
理论上不手动清理则会永久保存在本地,当页面的协议、主机名、端口相同时,读取到的是同一LocalStorage
。SessionStorage
在页面窗口被关闭时就会被清空,当然关闭浏览器也会清空数据,当页面的窗口、协议、主机名、端口相同时候共用同一SessionStorage
Storage以key, value
的形式进行数据存储,格式为字符串,所以存储内容会受到限制,可以使用JSON.stringfy、JSON.parse
方法进行转换。
API接口
sessionStorage/localStorage.
setItem(key, value) //添加数据
getItem(key) //查找数据
removeItem(key) //移除数据
clear //清空数据
File对象
File
对象一般来自<input type="file">
读取文件后获取的files
属性,该属性是以File
为元素的FileList
对象。File
对象是一个特殊的Blob
对象,这意味着它可以使用Blob
的任何属性和方法,且可以用在任意的Blob
类型的上下文中,例如FileReader、XMLHttpRequest.send()
。
File
对象在针对从本地硬盘选择文件的情况,更像是文件描述符,它的命名很容易让人误会File
对象已经把硬盘里的文件读取出来了,然而并没有,如果要读取到浏览器内存里,要使用FileReader
对象。我们在进行异步发送的时候无需先把文件读到浏览器内存里,再发送,XMLHttpRequest.send
会处理File
对象,传输时浏览器会将实际的文件发送过去。
常用属性和方法
File.
name //返回引用文件的名字
size //以字节为单位返回文件大小
type //以MIME Type返回文件类型
slice(start, end, contentType) //分割文件,三个参数都可选,分别表示文件开始的位置,文件结束的位置,新的文件类型。
FormData对象
FormData可以以一种表示表单数据的键值对key/value
的构造方式,key
并不需要唯一,通过XMLHttpRequest
将数据进行发送,如果送出时的编码类型被设multipart/form-data
,它会使用和表单一样的格式。代码里没使用是因为服务端那个文件接收框架默认给req
添加了该编码类型,添加了会出现错误。不直接发送File的原因是为了发送的过程中,带一些其他信息,比如文件的序号啊、检验码等。
API接口
new FormData().
append(name, value [,filename]) //添加数据,当value为blob类型可以通过filename指定blob的文件名
delete(name) //会删除该键名下的所有值
entries() //返回包含所有键值对的可迭代对象
keys() //返回包含所有键的可迭代对象
values() //返回包含所有值的可迭代对象
get(name) //返回与给定键关联的第一个值
getAll(name) //返回与给定键关联的所有值
set(name, value) //存在该键时,直接覆盖原来的值,不存在,则新添加该键值
代码实现
简易的服务端测试代码
const express = require('express');
const multer = require('multer');
const storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, './uploads/')
},
filename: function ( req, file, cb) {
cb(null, req.body.name)
}
})
const upload = multer({ storage })
const app = express();
const port = 5000;
app.use(express.json()) // for parsing application/json
app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
next();
});
app.post('/', upload.any(), (req, res)=> {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
console.log(req.files);
res.send('Ok');
})
app.listen(port, ()=> {
console.log(`Example app listening at http://localhost:${port}`)
})
前端代码
页面部分
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./upload.js"></script>
<title>文件上传测试</title>
</head>
<body>
<div class="upload-container">
<input type="file" multiple id="filename" name="file"/>
<span id="progress">0% </span>
<button onclick="submit()">上传</button>
<button id='pause'>暂停</button>
<button id='cancel'>取消</button>
</div>
<script>
var file;
function submit() {
file = document.getElementById('filename').files[0]
file = new webUploader(file, 'http://localhost:5000');
document.getElementById('pause').addEventListener('click', file.pause.bind(file));
document.getElementById('cancel').addEventListener('click', file.cancel.bind(file));
}
</script>
</body>
</html>
upload.js代码
function webUploader(file, url, headers={}){
this._tasks = this._init(file); //切分大文件
this._exeQueue = {}; //保存正在执行的xhr对象,以供abort使用
this._status = 'running'; //uploader的状态描述
this._url = url;
this._headers = headers;
this._size = {totalSize: file.size, loadedSize: 0};
document.getElementById('progress').innerHTML = '0%' + ' ';
this._run(); //开始上传
}
//切割大文件
webUploader.prototype._init = function (file) {
//返回文件切割的可迭代对象
let tasks = [];
let index = 0;
const SIZE = 20 * 1024 * 1024;
while(index < file.size) {
tasks.push({name: file.name + '_' + (index / SIZE + 1), file: file.slice(index, index + SIZE)});
index += SIZE;
}
return tasks;
}
//进行上传
webUploader.prototype._run = async function () {
let queue = [];
this._exeQueue = {};
for(task of this._tasks) {
//本地已检索到上传记录,则跳过
if(!sessionStorage.getItem(task.name) || sessionStorage.getItem(task.name)!='done') {
let formData = new FormData();
formData.append('name', task.name);
formData.append('file', task.file);
queue.push(this._request({url: this._url, formData: formData, headers: this._headers, exeQueue: this._exeQueue}));
}
}
Promise.all(queue).then( value => {
console.log('上传完毕');
}).catch (value => {
console.log('上传失败');
});
}
//暂停功能
webUploader.prototype.pause = function() {
if ( this._status == 'running') {
this._status = 'waiting';
for(key in this._exeQueue) {
this._exeQueue[key].abort();
}
this._exeQueue = {};
console.log('已暂停');
} else if( this._status == 'waiting') {
this._status = 'running';
console.log('已重启');
this._run();
} else {
}
}
//取消上传
webUploader.prototype.cancel = function () {
if(this._status != 'ending'){
this._status = 'running';
this.pause();
this._status = 'ending';
this._tasks.forEach( el=> {
sessionStorage.removeItem(el.name);
});
this._tasks = [];
this._url = '';
this._headers = {};
this._size = {totalSize: undefined, loadedSize: undefined};
document.getElementById('progress').innerHTML = '0%' + ' ';
console.log('已取消');
}
}
//简易封装异步请求
webUploader.prototype._request = function({url, formData, headers, exeQueue}) {
return new Promise( (resolve, reject) => {
let name = formData.get('name')
xhr = new XMLHttpRequest();
xhr.upload.onprogress = this._progressUpdate(this._size);
xhr.open('post', url);
Object.keys(headers).forEach( key => {
xhr.setRequestHeader(key, headers[key])
});
xhr.send(formData);
xhr.onload = e => {
// 上传完成后用sessionStore 存储任务信息
sessionStorage.setItem(name, 'done');
resolve(
{
data: e.target.response
}
);
delete exeQueue[name]; //上传完成后将xhr从中删除
}
xhr.onerror = e => {
reject('error')
}
exeQueue[name] = xhr;
} )
}
//更新进度条使用
webUploader.prototype._progressUpdate = function (size) {
let oldloaded = 0;
return e => {
let progress_dom = document.getElementById('progress'); //测试就直接获取了,简单些
size.loadedSize += (e.loaded-oldloaded);
oldloaded = e.loaded;
progress_dom.innerHTML = Math.floor(( size.loadedSize / size.totalSize) * 100) + '%' + ' ';
}
}