前端上传文件时如果文件很大,上传时会出现各种问题,比如连接超时了,网断了,都会导致上传失败。
为了避免上传大文件时上传超时,就需要用到切片上传,工作原理是:我们将大文件切割为小文件,然后将切割的若干小文件上传到服务器端,服务器端接收到被切割的小文件,然后按照一定的顺序将小文件拼接合并成一个大文件。
下面的实例就是如何一步步实现大文件切片上传。实例中运用到的技术包括:H5(前端使用)和nodejs(后端使用)。这个实例为了演示简便,我们使用大的图片上传来演示。
首先,我们来看一下上传表单的演示效果和代码,效果如下:
html结构如下:
因为这里使用的是ajax上传,所以没有使用form元素,直接使用一个上传文件的input来获取上传图片的数据。
获取图片数据用到了input元素的一个属性:flies属性,通过document.getElementById("file").files[0] 来获取图片数据。执行如下代码:
我们将其结果打印出来,如图所示:
打印的结果包含着图片的信息,这个信息是一个blob对象,这个对象被浏览器读取到了内存中,我们可以通过chrome://blob-internals/ 这个地址来查看浏览器读取到的blob的信息,如图所示:
读取了图片的数据之后,就将数据切片,然后将每次切割的小片文件上传到服务器,切片运用到了silce方法,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>upload</title>
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="./uuid.js"></script>
</head>
<body>
<input type="file" name="file" id="file">
<button id="upload">上传</button>
<script type="text/javascript">
var bytesPerPiece = 1024 * 1024; // 每个文件切片大小定为1MB .
var totalPieces; //切片总数
//发送请求
$("#upload").click(upload)
function upload() {
var blob = document.getElementById("file").files[0];
// 文件唯一标识符号,防止多个用户一起上传文件时切片混乱
var uuidfolder = uuidv1();
// 开始切割的位置
var start = 0;
// 切割的结束位置
var end;
// 切片的索引
var index = 0;
// 回调计数器
var count = 0;
// 文件的大小
var filesize = blob.size;
// 文件的名称
var filename = blob.name;
//计算文件切片总数
totalPieces = Math.ceil(filesize / bytesPerPiece);
// 启动while循环对文件切片
while(start < filesize) {
// 设置切片的结束位置
end = start + bytesPerPiece;
// 对最后一片数据进行处理(可以省略)
if(end > filesize) {
end = filesize;
}
// 切割文件
var chunk = blob.slice(start,end);//切割文件
// 给每一片切片设置名字,名字的值为原始名称加索引,这样做是为了让后端可以按照索引顺序合并图片。
var sliceIndex= blob.name + index;
// 利用formData来传递数据
var formData = new FormData();
formData.append("file", chunk, sliceIndex);
formData.append("uuidfolder", uuidfolder);
formData.append("imgorder", index);
$.ajax({
url: '/upload3',
type: 'POST',
data: formData,
processData: false, // 不处理数据
contentType: false, // 不设置内容类型
}).done(function(res){
count++;
if(count==totalPieces){
console.log("上传结束,请求拼接接口,将切片信息拼接完整,返回图片url");
$.post('/merge',{id:uuidfolder},function(data){
console.log(data);
})
}
}).fail(function(res) {
console.log("上传失败")
});
start = end;
index++;
}
}
</script>
</body>
</html>
代码解析见注释。核心代码就是这一段:
while(start < filesize) {
// 设置切片的结束位置
end = start + bytesPerPiece;
// 对最后一片数据进行处理(可以省略)
if(end > filesize) {
end = filesize;
}
// 切割文件
var chunk = blob.slice(start,end);//切割文件
// 给每一片切片设置名字,名字的值为原始名称加索引,这样做是为了让后端可以按照索引顺序合并图片。
var sliceIndex= blob.name + index;
// 利用formData来传递数据
var formData = new FormData();
formData.append("file", chunk, sliceIndex);
formData.append("uuidfolder", uuidfolder);
formData.append("imgorder", index);
$.ajax({
url: '/upload3',
type: 'POST',
data: formData,
processData: false, // 不处理数据
contentType: false, // 不设置内容类型
}).done(function(res){
count++;
if(count==totalPieces){
console.log("上传结束,请求拼接接口,将切片信息拼接完整,返回图片url");
$.post('/merge',{id:uuidfolder},function(data){
console.log(data);
})
}
}).fail(function(res) {
console.log("上传失败")
});
start = end;
index++;
}
上面的代码启动了一个while循环,在这个循环中,每次截取固定大小的切片,然后用ajax上传到后端服务器,并且会附加一些比较重要的信息,这些信息主要包括:图片的唯一标识符(这里用到了uuid.js来生成唯一的id),切片的索引(为了后端按照切片顺序将切片合并),ajax每次上传完成后都要检查所有切片是否上传完成,全部上传完成后,请求合并接口,这个接口返回合并后的图片的url。
前端将切片信息传递到后端,后端用过nodejs接受切片,然后按照索引将切片拼接成完整的文件,这里用到了两个工具包multer和concat-files,前一个是负责接收切片信息,后一个负责合并切片。
这里一般的做法是设置两个接口,一个接口负责接收图片的切片信息,将其保存,另外一个接口负责拼接切片信息。这样做的原因是,如果用一个接口来操作的话,每张切片接收完成后都要去检查所有切片是否都接收完成,而只有当所有切片完成才能将切片合并,这样比较耗费服务端的性能。
接口处理代码如下:
// 接收切片信息接口
router.post('/upload3', upload.single('file'), function (req, res, next) {
console.log(req.body)
// 接受图片唯一标识符号
let imgname = req.body.uuidfolder;
// 接受切片索引
let imgorder = req.body.imgorder;
// 建立图片存储目录
let imgpath = path.join(__dirname,'..','public/mult',imgname);
// 判断目录是否存在,存在的话直接使用并存储切片,不存在的话就新建。
if (fs.existsSync(imgpath)) {
fs.readFile(req.file.path, function (err, data) {
fs.writeFile(path.join(imgpath, imgorder), data, (err) => {
if (!err) {
res.send("写入后面的文件")
}
})
})
} else {
fs.mkdirSync(imgpath);
fs.readFile(req.file.path, function (err, data) {
fs.writeFile(path.join(imgpath, imgorder), data, (err) => {
if (!err) {
res.send("第一次写入并新建文件夹")
}
})
})
}
})
// 合并图片接口:
router.post('/merge',function(req,res){
let id = req.body.id;
let folderpath = path.join(__dirname,"..",'public/mult',id);
let destinpath = path.join(__dirname,"..",'public/img',id+'.jpg');
let dist = '/img/'+id+'.jpg'
fs.readdir(folderpath,function(err,arr){
let arr2 = arr.map(e=>path.join(folderpath,e));
concat(arr2, destinpath, function(err) {
if (err) throw err
res.send(dist);
});
})
})
以上便是大文件切片上传的原理解析。
相较于单独上传一个文件而言,大文件上传在前端层面,多了一步切割的步骤,后端多了一步合并的步骤,只有前后端配合才能完成大文件切片上传。
文件源码地址:https://github.com/clm1100/slicefile
项目中不仅有javascript原生语法实现大文件切片上传,还有webuploader切片上传的实例,以供大家参考。
欢迎关注公众号,有疑问可以留言给我!