【Vue】vue3+node+Element-Ui+spark-md5实现大文件上传、断点续传、秒传、多大文件上传
前言
在Vue项目中,大图片和多数据Excel等大文件的上传是一个非常常见的需求。然而,由于文件大小较大,上传速度很慢,传输中断等问题也难以避免。因此,为了提高上传效率和成功率,我们需要使用切片上传的方式,实现文件秒传、断点续传、错误重试、控制并发等功能,并绘制进度条。
整体架构流程
前端:使用vue3.0的版本实现大文件上传,在前端切片传递后端
后端:使用node接收切片,存储文件夹中,进行合并,返回前端
技术名词解释
vue
node
Element-Ui
axios
spark-md5
fs
multiparty
前端
大文件上传
将大文件转换成二进制流的格式
利用流可以切割的属性,将二进制流切割成多份
组装和分割块同等数量的请求块,并行或串行的形式发出请求
待我们监听到所有请求都成功发出去以后,再给服务端发出一个合并的信号
断点续传
为每一个文件切割块添加不同的标识
当上传成功的之后,记录上传成功的标识
当我们暂停或者发送失败后,可以重新发送没有上传成功的切割文件
后端
接收每一个切割文件,并在接收成功后,存到指定位置,并告诉前端接收成功
收到合并信号,将所有的切割文件排序,合并,生成最终的大文件,然后删除切割小文件,并告知前端大文件的地址
其实说到这里,如果你看懂并且理解了以上的思路,那么你已经学会了大文件上传+断点续传的 80%。下面的具体实现过程,对你来讲,就是小意思...
大文件上传代码部分
<el-upload class="upload-demo" drag action multiple :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>
js部分的逻辑,按照我们的上面的分析,我们可以写出如下的结构
// 上传前处理,返回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 = spark.end(); 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("http://localhost:3000/single3", formData, { headers: { "Content-Type": "multipart/form-data" }, }) .then((result) => { result = result.data; if (result.code == 0) { total.value += 1; // 传完的切片我们把它移除掉 partList.splice(index, 1); } }); }; requestList.push(fn); }); // 传递切片:并发/串发 并发就是一块发,串发就是一个一个发 let i = 0; let complete = async () => { let result = await axios.get("http://localhost:3000/merge", { params: { hash: hash, }, }); result = result.data; if (result.code == 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") { fileRead.readAsDataURL(file); } else if (type === "buffer") { fileRead.readAsArrayBuffer(file); } fileRead.onload = (ev) => { // console.log(ev.target.result); resolve(ev.target.result); } }) }
将大文件进行分片
在我们拿到具体的二进制流之后我们就可以进行分块了,就像操作数组一样方便。
当然了,我们在拆分切片大文件的时候,还要考虑大文件的合并,所以我们的拆分必须有规律,比如 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 = spark.end(); 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("http://localhost:3000/single3", formData, { headers: { "Content-Type": "multipart/form-data" }, }) .then((result) => { result = result.data; if (result.code == 0) { total.value += 1; // 传完的切片我们把它移除掉 partList.splice(index, 1); } }); }; requestList.push(fn); });
将每一个切片 并行/串行 的方式发出
目前切片已经分好了,并且我们的请求也已经包装好了。 目前我们有两个方案 并行/串行 因为串行容易理解,这里拿串行举例子。
我们每成功的发出去一个请求,那么我们对应的下标就加一,证明我们的发送成功。当 i 下标和 我们的切片数相同的时候,我们默认发送成功,触发 合并(merge)请求
// 传递切片:并发/串发 并发就是一块发,串发就是一个一个发 let i = 0; let complete = async () => { let result = await axios.get("http://localhost:3000/merge", { params: { hash: hash, }, }); result = result.data; if (result.code == 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: path.replace(__dirname, `http://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); readStream.on('end', function () { fs.unlinkSync(chunk.path); res.send({ code: 0, path: path.replace(__dirname, `http://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 }) }) }) }
最后进行合并切片,返回给前端
app.get('/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+)/; return reg.exec(a)[1] - reg.exec(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 } await fileModel.create(body) res.send({ code: 0, path: `http://localhost:3000/upload/${hash}.${suffix}` }) })
小结
看到这里你已经会大文件上传、断点续传、秒传、多大文件上传了,这是个人的一些想法思路,如果有更好的方法可以分享,有问题可以留言评论
如果你感觉这篇文章对你有帮助,欢迎关注我的博客
猜你喜欢
- 【Vue】Vue 单文件组件 (SFC) 规范
- 简介.vue 文件是一个自定义的文件类型,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 <template>、<script> 和 <style>,还允许添加可选的自定义块:
- 【Vue】vue中sync作用
- vue 中的 sync 修饰符用于在父组件和子组件之间实现双向数据绑定。它通过生成一个 v-model 指令,将子组件的 prop 与父组件的 prop 绑定在一起,从而实现数据同步。用法如下:1. 在子组件中使用 v-bind:prop.sync="parentprop",其中 prop 是子组件的 prop 名称,parentprop 是父组件绑定的 prop 名称。Vue 中 sync 作用在 Vue 中,sync 修饰符是一种特殊的语法糖,它允许在父组件和子组件之间进
- 【Vue】前端框架 Vue3框架 使用总结(一) Vue框架的基础使用
- 目录一、Vue3框架基础1、创建项目2、项目结构3、Vue基础语法4、组件之间通信5、组合式api二、VueRouter的基础使用1、安装2、使用案例3、完整案例步骤4、调优-路由懒加载三、Vuex数据管理1、实现案例 2、更改store状态,同步操作3、store中的计算属性4、redux里的异步操作Action5、模块化管理四、网络请求Vue3官方文档:Vue.js - 渐进式 JavaScript 框架 | Vue.js基础部分见官方文档一、Vue3框架基础1、创建项目安装yar
- 【Vue】Antd Pro Vue的使用(四)—— 打包发布到站点二级目录,生产环境请求接口配置
- 如题,Antd Pro Vue开发完成后,要打包发布到站点指定二级目录下,我这里服务端配置的是tp,在站点public文件夹新建一个system文件夹,前端打包后要放到个文件夹里面,需要配置2步1. 在根目录vue.config.js文件夹中配置publicPath publicPath: '/system/',2. 在/src/router/index.js文件中,增加base配置,配置内容与publicPath保持一致router: { &nbs
- 【Vue】Antd Pro Vue的使用(五)—— 多文件上传回显问题
- 需求: 多文件上传 ,上传的时候绑定fileList回显问题: 上传成功了,也拿到了后台返回的数据,但是onchang监听的时候,file的状态一直是uploading原因:onchange 只触发了一次解决: 使用单文件上传时@change事件会至少触发两次,一次file.status=uploading,最后一次要么是done或者error,handleUpload1(info) { if (info
- 【Vue】vue通过class获取dom
- 其实就是操作 html 中的标签的一些能力 我们可以操作哪些内容 获取一个元素 移除一个元素 创建一个元素 向页面里面添加一个元素 给元素绑定一些事件 获取元素的属性给元素添加一些 css 样式 ... DOM 的核心对象就是 docuemnt 对象 document 对象是浏览器内置的一个对象,里面存储着专门用来操作元素的各种方法 DOM: 页面中的标签,我们通过 js 获取到以后,就把这个对象叫做 DOM 对象获取一个元素通过 js 代码来获取页面中的标签获取到以后我们
- 【Vue】Vue中字符串数组和对象常用方法介绍
- 在Vue中,数组和字符串是我们最常处理的数据类型。Vue提供了响应式系统,可以自动跟踪数组和对象的变化,并响应式地更新DOM。在Vue中,您可以像在任何JavaScript应用程序中一样操作这些数据类型。下面时整理的Vue中字符串 数组 以及对象的常用操作方法。一.数组方法1.增删改: unshift、push、splice、shift、pop、splice、slice 1.1.unshift:在数组的头部添加内容// 数组.unshift("添加的值")