目录
文章目录
前言
整体架构流程
技术名词解释
前端
大文件上传
断点续传
后端
大文件上传代码部分
js部分的逻辑,按照我们的上面的分析,我们可以写出如下的结构
将文件变成二进制,方便后续分片
将大文件进行分片
创建切片请求
将每一个切片 并行/串行 的方式发出
后端接口逻辑
小结
在Vue项目中,大图片和多数据Excel等大文件的上传是一个非常常见的需求。然而,由于文件大小较大,上传速度很慢,传输中断等问题也难以避免。因此,为了提高上传效率和成功率,我们需要使用切片上传的方式,实现文件秒传、断点续传、错误重试、控制并发等功能,并绘制进度条。
前端:使用vue3.0的版本实现大文件上传,在前端切片传递后端
后端:使用node接收切片,存储文件夹中,进行合并,返回前端
fs
multiparty
其实说到这里,如果你看懂并且理解了以上的思路,那么你已经学会了大文件上传+断点续传的 80%。下面的具体实现过程,对你来讲,就是小意思...
<el-uploadclass="upload-demo"dragactionmultiple:auto-upload="false":show-file-list="false":on-change="handleBeforeUpload"><el-icon class="el-icon--upload"><upload-filled /></el-icon><div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
// 上传前处理,返回false改为手动上传
const handleBeforeUpload = async (file) => {dialogVisible.value = false;if (!file) return;file = file.raw;let buffer = await fileParse(file, "buffer"), // 拿到二进制流spark = new SparkMD5.ArrayBuffer(), // 我们为了避免同一个文件(改名字)多次上传,我们引入了 spark-md5 ,根据具体文件内容,生成hash值hash,suffix;spark.append(buffer);hash = d();suffix = /.([0-9a-zA-Z]+)$/i.exec(file.name)[1];// 创建100个切片let partList = [],partsize = file.size / 100,cur = 0;for (let i = 0; i < 100; i++) {let item = {chunk: file.slice(cur, cur + partsize),filename: `${hash}_${i}.${suffix}`, // 每一个切片命名当时候,改成了 hash_1,hash_2 这种形式};cur += partsize;partList.push(item);}partLists.value = partList;hashs.value = hash;sendRequest(partList, hash);
};const sendRequest = async (partList, hash) => {// 根据100个切片创造100个请求(集合)let requestList = [];partList.forEach((item, index) => {// 每一个函数都是发送一个切片请求let fn = () => {// 我们发出去的数据采用的是FormData数据格式let formData = new FormData();formData.append("chunk", item.chunk);formData.append("filename", item.filename);return axios.post("localhost:3000/single3", formData, {headers: { "Content-Type": "multipart/form-data" },}).then((result) => {result = result.data;if (de == 0) {total.value += 1;// 传完的切片我们把它移除掉partList.splice(index, 1);}});};requestList.push(fn);});// 传递切片:并发/串发 并发就是一块发,串发就是一个一个发let i = 0;let complete = async () => {let result = ("localhost:3000/merge", {params: {hash: hash,},});result = result.data;if (de == 0) {video.value = result.path;}};let send = async () => {if (i >= requestList.length) {// 都传完了complete();return;}await requestList[i]();i++;send();};send();
};
js常见的二进制格式有 Blob,ArrayBuffer和Buffe,这里没有采用其他文章常用的Blob,而是采用了ArrayBuffer, 又因为我们解析过程比较久,所以我们采用 promise,异步处理的方式
export function fileParse(file, type = "base64") {return new Promise(resolve => {let fileRead = new FileReader();if (type === "base64") {adAsDataURL(file);} else if (type === "buffer") {adAsArrayBuffer(file);}load = (ev) => {// console.log(sult);resolve(sult);}})
}
在我们拿到具体的二进制流之后我们就可以进行分块了,就像操作数组一样方便。
当然了,我们在拆分切片大文件的时候,还要考虑大文件的合并,所以我们的拆分必须有规律,比如 1-1,1-2,1-3 ,1-5 这样的,到时候服务端拿到切片数据,当接收到合并信号当时候,就可以将这些切片排序合并了。
同时,我们为了避免同一个文件(改名字)多次上传,我们引入了 spark-md5 ,根据具体文件内容,生成hash值
let buffer = await fileParse(file, "buffer"), // 拿到二进制流spark = new SparkMD5.ArrayBuffer(), // 我们为了避免同一个文件(改名字)多次上传,我们引入了 spark-md5 ,根据具体文件内容,生成hash值hash,suffix;spark.append(buffer);hash = d();suffix = /.([0-9a-zA-Z]+)$/i.exec(file.name)[1];
而我们,为每一个切片命名当时候,也改成了 hash-1,hash-2 这种形式,
我们分割大文件的时候,可以采用 定切片数量,定切片大小,两种方式,我们这里采用了 定切片数量这个简单的方式做例子
// 创建100个切片let partList = [],partsize = file.size / 100,cur = 0;for (let i = 0; i < 100; i++) {let item = {chunk: file.slice(cur, cur + partsize),filename: `${hash}_${i}.${suffix}`, // 每一个切片命名当时候,改成了 hash_1,hash_2 这种形式};cur += partsize;partList.push(item);}
当我们采用定切片数量的方式,将我们大文件切割完成,并将切割后的数据存给一个数组变量,接下来,就可以封装的切片请求了
这里需要注意的就是,我们发出去的数据采用的是FormData数据格式,至于为什么大家可以找资料查询。
// 根据100个切片创造100个请求(集合)let requestList = [];partList.forEach((item, index) => {// 每一个函数都是发送一个切片请求let fn = () => {// 我们发出去的数据采用的是FormData数据格式let formData = new FormData();formData.append("chunk", item.chunk);formData.append("filename", item.filename);return axios.post("localhost:3000/single3", formData, {headers: { "Content-Type": "multipart/form-data" },}).then((result) => {result = result.data;if (de == 0) {total.value += 1;// 传完的切片我们把它移除掉partList.splice(index, 1);}});};requestList.push(fn);});
目前切片已经分好了,并且我们的请求也已经包装好了。 目前我们有两个方案 并行/串行 因为串行容易理解,这里拿串行举例子。
我们每成功的发出去一个请求,那么我们对应的下标就加一,证明我们的发送成功。当 i 下标和 我们的切片数相同的时候,我们默认发送成功,触发 合并(merge)请求
// 传递切片:并发/串发 并发就是一块发,串发就是一个一个发let i = 0;let complete = async () => {let result = ("localhost:3000/merge", {params: {hash: hash,},});result = result.data;if (de == 0) {video.value = result.path;}};let send = async () => {if (i >= requestList.length) {// 都传完了complete();return;}await requestList[i]();i++;send();};send();
当然了,并行发送的最大缺点就是没有串行快,但胜在代码简单,容易理解代码
首先引入fs模块和multiparty模块,进行存储文件夹
// 大文件断点续传
// 引入fs模块
const fs = require('fs')
const multiparty = require("multiparty"),uploadDir = `${__dirname}/upload`;
接下来是接收前端切片文件进行存储文件夹,判断有无此文件,如果有就实现秒传,如果没有就上传,上传不需要上传已经上传的切片,这就实现了断点续传问题,上面有注释,我就不多解释了
// 切片上传 && 合并
app.post('/single3', async (req, res) => {let { fields, files } = await handleMultiparty(req, res, true);let [chunk] = files.chunk,[filename] = fields.filename;let hash = /([0-9a-zA-Z]+)_d+/.exec(filename)[1],// suffix = /.([0-9a-zA-Z]+)&/.exec(files.name)[1],path = `${uploadDir}/${hash}`; //把所有切片放在这个文件夹中,会临时创建一个文件夹,然后把文件都存储到这个文件夹中!fs.existsSync(path) ? fs.mkdirSync(path) : null; //如果不存储创建一个path = `${path}/${filename}`; //拿到上传地址的名字// 判断是否存储fs.access(path, async err => {// 存在的则不再进行任何处理// 传过的不用在传了,就是断点续传,实现秒传if (!err) {res.send({code: 0,path: place(__dirname, `localhost:3000`)});return;}// 为了测试出效果,延迟1秒钟// await new Promise(resolve => {// setTimeout(_ => {// resolve();// }, 200);// })// 不存在的在创建// 通过fs文件流的操作,把不存在的存储到文件夹下let readStream = fs.createReadStream(chunk.path),writeStream = fs.createWriteStream(path);readStream.pipe(writeStream);('end', function () {fs.unlinkSync(chunk.path);res.send({code: 0,path: place(__dirname, `localhost:3000`)})})})})
而handleMultiparty是封装成了方法,里面是multiparty的一些配置,为了存储文件夹和判断文件大小问题
function handleMultiparty(req, res, temp) {return new Promise((resolve, reject) => {// multiparty的配置let options = {maxFieldsSize: 200 * 1024 * 1024 // 上传文件大小};!temp ? options.uploadDir = uploadDir : null;let form = new multiparty.Form(options);// multiparty解析form.parse(req, function (err, fields, files) {if (err) {res.send({code: 1,reason: err});reject(err);return;}// 成功resolve({fields,files})})})
}
最后进行合并切片,返回给前端
('/merge', async(req, res) => {let { hash,name } = req.query;// 拿到叫hash哈希值的所有的切片let path = `${uploadDir}/${hash}`,fileList = fs.readdirSync(path),suffix;// 进行排序fileList.sort((a, b) => {let reg = /_(d+)/;(a)[1] - (b)[1];}).forEach(item => {// 合并文件,通过fs中的appendFileSync全部合并到一起!suffix ? suffix = /.([0-9a-zA-Z]+)$/.exec(item)[1] : null;fs.appendFileSync(`${uploadDir}/${hash}.${suffix}`, fs.readFileSync(`${path}/${item}`));fs.unlinkSync(`${path}/${item}`);});// 把所以的切片文件全部删除掉fs.rmdirSync(path);// 把文件地址存储到数据库中let body = {path:`/${hash}.${suffix}`,name:name}ate(body)res.send({code: 0,path: `localhost:3000/upload/${hash}.${suffix}`})})
看到这里你已经会大文件上传、断点续传、秒传、多大文件上传了,这是个人的一些想法思路,如果有更好的方法可以分享,有问题可以留言评论
如果你感觉这篇文章对你有帮助,欢迎关注我的博客
本文发布于:2024-02-01 17:25:54,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170678078138261.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |