前言
文件上传是开发中经常遇到的,市面上也有很多插件。直接封装了上传的方法,使用起来很简单。使用vue和react技术栈的同学,都使用了element和antd的上传,因此,拖拽上传和一般上传,今天这篇文章不做解释。今天主要总结一下剪切板上传和大文件分片上传及断点续传的内容。
一、剪切板上传
剪切板上传就是复制电脑上的图片或者文件,或者网络中的在线图片,然后粘贴到指定位置上传的方式。
关于剪切板,我之前文章有介绍过:https://www.haorooms.com/post/js_focus_position_copy 假如对光标位置和剪切板复制不清楚的同学,可以看这篇文章。
前台可以这么写:
var haoroomsbox = document.getElementById('haoronms-edit');
haoroomsbox.addEventListener('paste',function (event) {
var data = (event.clipboardData || window.clipboardData);
var items = data.items;
var fileList = [];//存储文件数据
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
fileList.push(items[i].getAsFile());
}
}
window.willUploadFileList = fileList;
event.preventDefault();
submitUpload();
});
function submitUpload() {
var fileList = window.willUploadFileList||[];
if(!fileList.length){
console.log('当前无粘贴文件');
return;
}
var haoroomsformData = new FormData(); //构造FormData对象
for(var i =0;i<fileList.length;i++){
haoroomsformData.append('filename', fileList[i]);//支持多文件上传
}
// http请求,当然你也可以用第三方的axios等
var xhr = new XMLHttpRequest(); //创建对象
xhr.open('POST', 'http://haorooms.com:8100/fileupload', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
var obj = JSON.parse(xhr.responseText); //返回值
if(obj.fileUrl.length){
var img = document.createElement('img');
img.src= obj.fileUrl[0];
img.style.width='100px';//这里可以自定义图片宽度,也可以不用写
insertNodeToEditor(box,img);
// alert('上传成功');
}
}
}
//注意 send 一定要写在最下面,否则 onprogress 只会执行最后一次 也就是100%的时候
xhr.send(haoroomsformData);//发送时 Content-Type默认就是: multipart/form-data;
}
//光标处插入 dom 节点
function insertNodeToEditor(editor,ele) {
//插入dom 节点
var range;//记录光标位置对象
var node = window.getSelection().anchorNode;
// 这里判断是做是否有光标判断,因为弹出框默认是没有的
if (node != null) {
range = window.getSelection().getRangeAt(0);// 获取光标起始位置
range.insertNode(ele);// 在光标位置插入该对象
} else {
editor.append(ele);
}
}
后端以Koa为例
var app = new Koa();
var port = process.env.PORT || '8100';
var uploadHost= `http://localhost:${port}/uploads/`;
app.use(koaBody({
formidable: {
//设置文件的默认保存目录,不设置则保存在系统临时目录下
uploadDir: path.resolve(__dirname, '../static/uploads')
},
multipart: true // 支持文件上传
}));
app.use(koaStatic(
path.resolve(__dirname, '../static')
));
//允许跨域
app.use(async (ctx, next) => {
ctx.set('Access-Control-Allow-Origin', ctx.headers.origin);
ctx.set("Access-Control-Max-Age", 864000);
// 设置所允许的HTTP请求方法
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
// 字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段.
ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type");
await next();
})
//二次处理文件,修改名称
app.use((ctx) => {
console.log(ctx.request.files);
var files = ctx.request.files.f1;//得到上传文件的数组
var result=[];
console.log(files);
if(!Array.isArray(files)){//单文件上传容错
files=[files];
}
files && files.forEach(item=>{
var path = item.path.replace(/\\/g, '/');
var fname = item.name;//原文件名称
var nextPath = path + fname;
if (item.size > 0 && path) {
//得到扩展名
var extArr = fname.split('.');
var ext = extArr[extArr.length - 1];
var nextPath = path + '.' + ext;
//重命名文件
fs.renameSync(path, nextPath);
result.push(uploadHost+ nextPath.slice(nextPath.lastIndexOf('/') + 1));
}
});
ctx.body = `{
"fileUrl":${JSON.stringify(result)}
}`;
})
二、大文件上传
大文件上传其实就是将一个大文件拆分成多个小文件再上传。 我之前文件有讲过Blob,二进制数据,提供了slice,而file继承了Blob的功能,光晕blob相关文章,请看:https://www.haorooms.com/post/js_blobdownload
思路步骤
把大文件进行分段 比如2M,发送到服务器携带一个标志,暂时用当前的时间戳,用于标识一个完整的文件
服务端保存各段文件
浏览器端所有分片上传完成,发送给服务端一个合并文件的请求
服务端根据文件标识、类型、各分片顺序进行文件合并
删除分片文件
例如代码如下:
html
选择文件:
<input type="file" id="haoroomsFileinput"/><br/><br/>
<div id="progress">
<span class="red"></span>
</div>
<button type="button" id="btn-submit">上 传</button>
js代码
//思路概括
//把大文件分成每2m 一块进行上传,发送到服务器同时携带一个标志 暂时用当前的时间戳 ,
//服务端生成临时文件,服务端接受一个文件结束的标志 ,然后将所有的文件进行合并成一个文件,清理临时文件。 返回结果(看情况)
function submitUpload() {
var chunkSize=2*1024*1024;//2m
var progressSpan = document.getElementById('progress').firstElementChild;
var file = document.getElementById('haoroomsFileinput').files[0];
var chunks=[],
token = (+ new Date()),
name =file.name,chunkCount=0,sendChunkCount=0;
progressSpan.style.width='0';
progressSpan.classList.remove('green');
if(!file){
alert('请选择文件');
return;
}
//拆分文件
if(file.size>chunkSize){
//拆分文件
var start=0,end=0;
while (true) {
end+=chunkSize;
var blob = file.slice(start,end);
console.log()
start+=chunkSize;
if(!blob.size){
//拆分结束
break;
}
chunks.push(blob);
}
}else{
chunks.push(file.slice(0));
}
console.log(chunks);
chunkCount=chunks.length;
//没有做并发限制,较大文件导致并发过多,tcp 链接被占光 ,需要做下并发控制,比如只有4个在请求在发送
for(var i=0;i< chunkCount;i++){
var haoroomsfd = new FormData(); //构造FormData对象
haoroomsfd.append('token', token);
haoroomsfd.append('haoroomsFileinput', chunks[i]);
haoroomsfd.append('index', i);
xhrSend(haoroomsfd,function () {
sendChunkCount+=1;
if(sendChunkCount===chunkCount){
console.log('上传完成,发送合并请求');
var formD = new FormData();
formD.append('type','merge');
formD.append('token',token);
formD.append('chunkCount',chunkCount);
formD.append('filename',name);
xhrSend(formD);
}
});
}
}
function xhrSend(haoroomsfd,cb) {
var xhr = new XMLHttpRequest(); //创建对象
xhr.open('POST', 'http://haorooms.com:8100/fileupload', true);
xhr.onreadystatechange = function () {
console.log('state change', xhr.readyState);
if (xhr.readyState == 4) {
console.log(xhr.responseText);
cb && cb();
}
}
function updateProgress(event) {
console.log(event);
if (event.lengthComputable) {
var completedPercent = (event.loaded / event.total * 100).toFixed(2);
progressSpan.style.width = completedPercent + '%';
progressSpan.innerHTML = completedPercent + '%';
if (completedPercent > 90) {//进度条变色
progressSpan.classList.add('green');
}
console.log('已上传', completedPercent);
}
}
//注意 send 一定要写在最下面,否则 onprogress 只会执行最后一次 也就是100%的时候
xhr.send(haoroomsfd);//发送时 Content-Type默认就是: multipart/form-data;
}
//绑定提交事件
document.getElementById('btn-submit').addEventListener('click',submitUpload);
服务端代码,对上面做了一些改进
//二次处理文件,修改名称
app.use((ctx) => {
console.log(ctx.request.files);
var body = ctx.request.body;
var files = ctx.request.files ? ctx.request.files.haoroomsFileinput:[];//得到上传文件的数组
var result=[];
var fileToken = ctx.request.body.token;// 文件标识
var fileIndex=ctx.request.body.index;//文件顺序
if(files && !Array.isArray(files)){//单文件上传容错
files=[files];
}
files && files.forEach(item=>{
var path = item.path.replace(/\\/g, '/');
var fname = item.name;//原文件名称
var nextPath = path.slice(0, path.lastIndexOf('/') + 1) + fileIndex + '-' + fileToken;
if (item.size > 0 && path) {
//得到扩展名
var extArr = fname.split('.');
var ext = extArr[extArr.length - 1];
//var nextPath = path + '.' + ext;
//重命名文件
fs.renameSync(path, nextPath);
result.push(uploadHost+ nextPath.slice(nextPath.lastIndexOf('/') + 1));
}
});
ctx.body = `{
"fileUrl":${JSON.stringify(result)}
}`;
if(body.type==='merge'){
//合并文件
var filename = body.filename,
chunkCount = body.chunkCount,
folder = path.resolve(__dirname, '../static/uploads')+'/';
var writeStream = fs.createWriteStream(`${folder}${filename}`);
var cindex=0;
//合并文件
function fnMergeFile(){
var fname = `${folder}${cindex}-${fileToken}`;
var readStream = fs.createReadStream(fname);// 运用了createReadStream
readStream.pipe(writeStream, { end: false });
readStream.on("end", function () {
fs.unlink(fname, function (err) {
if (err) {
throw err;
}
});
if (cindex+1 < chunkCount){
cindex += 1;
fnMergeFile();
}
});
}
fnMergeFile();
ctx.body='merge ok 200';
}
});
三、大文件断点续传
大文件分片上传我们已经实现了,那么断点续传,就是在断网的情况下,继续上传。和大文件分片上传相比,断网下次上传的时候,我们仅仅需要知道哪些上传了,哪些没有上传就可以了, 对于已经上传的文件,我们可以提供2中方式,一种是每个文件生成一个hash,存在本地,另一种是这个hash存在服务端,通过接口请求获取。 为了不出问题,我们可以存放到服务端。 简单起见,我们先讲下如何存在本地。通过获取本地文件hash的方式来续传。
代码如下( 对上面分片上传做了改造):
var saveChunkKey = 'haoroomschunkuploadedObj';//定义 key
//获得本地缓存的数据
function getUploadedFromStorage(){ // 服务端存储更安全,可以通过调用接口的方式获取文件key,这里可以写获取接口的方法getUploadedFromServer(fileHash)
return JSON.parse( localStorage.getItem(saveChunkKey) || "{}");
}
//写入缓存
function setUploadedToStorage(index) {
var obj = getUploadedFromStorage();
obj[index]=true;
localStorage.setItem(saveChunkKey, JSON.stringify(obj) );
}
//分段对比
var uploadedInfo = getUploadedFromStorage();//获得已上传的分段信息
for(var i=0;i< chunkCount;i++){ // 参考上文 大文件分片上传的chunkCount
console.log('index',i, uploadedInfo[i]?'已上传过':'未上传');
if(uploadedInfo[i]){//对比分段
sendChunkCount=i+1;//记录已上传的索引
continue;//如果已上传则跳过
}
var haoroomsfd = new FormData(); //构造FormData对象
haoroomsfd.append('token', token);
haoroomsfd.append('haoroomsFileinput', chunks[i]);
haoroomsfd.append('index', i);
(function (index) {
xhrSend(haoroomsfd, function () {
sendChunkCount += 1;
//将成功信息保存到本地
setUploadedToStorage(index);
if (sendChunkCount === chunkCount) {
console.log('上传完成,发送合并请求');
var formD = new FormData();
formD.append('type', 'merge');
formD.append('token', token);
formD.append('chunkCount', chunkCount);
formD.append('filename', name);
xhrSend(formD);
}
});
})(i);
}
服务端代码基本不变。
写到这里,基本把断点续传和大文件上传都写了。