您的当前位置:首页>全部文章>文章详情

【Vue】vue3+node+Element-Ui+spark-md5实现大文件上传、断点续传、秒传、多大文件上传

CrazyPanda发表于:2023-12-05 19:58:20浏览:324次TAG:


前言

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 文件包含三种类型的顶级语言块 &lt;template&gt;、&lt;script&gt; 和 &lt;style&gt;,还允许添加可选的自定义块:
发表于:2024-03-29 浏览:280 TAG:
【Vue】vue中sync作用
vue 中的 sync 修饰符用于在父组件和子组件之间实现双向数据绑定。它通过生成一个 v-model 指令,将子组件的 prop 与父组件的 prop 绑定在一起,从而实现数据同步。用法如下:1. 在子组件中使用 v-bind:prop.sync=&quot;parentprop&quot;,其中 prop 是子组件的 prop 名称,parentprop 是父组件绑定的 prop 名称。Vue 中 sync 作用在 Vue 中,sync 修饰符是一种特殊的语法糖,它允许在父组件和子组件之间进
发表于:2024-05-16 浏览:241 TAG:
【Vue】前端框架 Vue3框架 使用总结(一) Vue框架的基础使用
目录一、Vue3框架基础1、创建项目2、项目结构3、Vue基础语法4、组件之间通信5、组合式api二、VueRouter的基础使用1、安装2、使用案例3、完整案例步骤4、调优-路由懒加载三、Vuex数据管理1、实现案例&nbsp;2、更改store状态,同步操作3、store中的计算属性4、redux里的异步操作Action5、模块化管理四、网络请求Vue3官方文档:Vue.js - 渐进式 JavaScript 框架 | Vue.js基础部分见官方文档一、Vue3框架基础1、创建项目安装yar
发表于:2023-12-05 浏览:806 TAG:
【Vue】Antd Pro Vue的使用(四)—— 打包发布到站点二级目录,生产环境请求接口配置
如题,Antd Pro Vue开发完成后,要打包发布到站点指定二级目录下,我这里服务端配置的是tp,在站点public文件夹新建一个system文件夹,前端打包后要放到个文件夹里面,需要配置2步1. 在根目录vue.config.js文件夹中配置publicPath&nbsp;publicPath:&nbsp;&#39;/system/&#39;,2. 在/src/router/index.js文件中,增加base配置,配置内容与publicPath保持一致router:&nbsp;{ &amp;nbs
发表于:2024-04-27 浏览:338 TAG:
【Vue】Antd Pro Vue的使用(五)—— 多文件上传回显问题
需求: 多文件上传 ,上传的时候绑定fileList回显问题: 上传成功了,也拿到了后台返回的数据,但是onchang监听的时候,file的状态一直是uploading原因:onchange 只触发了一次解决: 使用单文件上传时@change事件会至少触发两次,一次file.status=uploading,最后一次要么是done或者error,handleUpload1(info)&nbsp;{ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if&nbsp;(info
发表于:2024-05-06 浏览:349 TAG:
【Vue】vue通过class获取dom
其实就是操作 html 中的标签的一些能力  我们可以操作哪些内容  获取一个元素  移除一个元素  创建一个元素  向页面里面添加一个元素  给元素绑定一些事件  获取元素的属性给元素添加一些 css 样式  ...  DOM 的核心对象就是 docuemnt 对象  document 对象是浏览器内置的一个对象,里面存储着专门用来操作元素的各种方法  DOM: 页面中的标签,我们通过 js 获取到以后,就把这个对象叫做 DOM 对象获取一个元素通过 js 代码来获取页面中的标签获取到以后我们
发表于:2024-05-12 浏览:294 TAG:
【Vue】Vue中字符串数组和对象常用方法介绍
在Vue中,数组和字符串是我们最常处理的数据类型。Vue提供了响应式系统,可以自动跟踪数组和对象的变化,并响应式地更新DOM。在Vue中,您可以像在任何JavaScript应用程序中一样操作这些数据类型。下面时整理的Vue中字符串 数组 以及对象的常用操作方法。一.数组方法1.增删改: unshift、push、splice、shift、pop、splice、slice&nbsp;1.1.unshift:在数组的头部添加内容//&nbsp;数组.unshift(&quot;添加的值&quot;)
发表于:2024-05-08 浏览:287 TAG:
【Vue】Antd Pro Vue的使用(十二) —— 菜单选中高亮显示问题
Antd Pro Vue2这套框架的路由菜单有两个问题,1、页面迁移子页面,父页面对应的菜单未能选中高亮显示2、登录后默认的菜单或页面刷新后原来的菜单未选中高亮显示网上查到的一些菜单配置都是新版本的,老版本并不支持这些方法,这里总结一下我的解决方法,如果大家有更好的解决方案,欢迎交流。一、解决第一个问题以我的菜单为例商品列表是菜单页面,添加编辑商品是隐藏菜单,我把他们做成了兄弟菜单,而不是子菜单/src/config/router.config.js配置如下然后再/src/layouts/Bas
发表于:2024-05-14 浏览:275 TAG:
【Vue】Vue数据更新方法
前言平时做项目时会经常对数组和对象进行数据更新操作,而有时数据并没有及时更新,这时我们会用Vue.set(),this.$set()等方法让数据及时更新。
发表于:2024-03-30 浏览:324 TAG:
【Vue】yarn 运行vue3项目开发模式
要在YARN中以Vue.js 3的开发模式运行项目,可以按照以下步骤进行操作:确保已经安装了Node.js和YARN。如果没有安装,请先安装这两个工具。打开命令提示符或终端并导航到Vue.js 3项目所在的文件夹。使用以下命令来初始化新的YARN项目(如果还未创建):yarn&nbsp;init&nbsp;-y接下来,需要安装Vue CLI脚手架工具。运行以下命令来全局安装Vue CLI:npm&nbsp;install&nbsp;-g&nbsp;@vue/cli然后,通过以下命令将Vue CL
发表于:2024-02-08 浏览:297 TAG: