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

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

CrazyPanda发表于:2023-12-05 19:58:20浏览:316次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是什么模式的前端框架
vue 中的 mvvm 架构将应用程序分为 model、view 和 viewmodel:model:包含数据和业务逻辑,独立于视图。view:显示 model 中的数据,使用模板语法进行数据绑定。viewmodel:model 和 view 之间的桥梁,包含与 view 交互的数据和方法,并更新 view。mvvm 在 vue 中的优势包括响应式数据绑定、代码可重用性、提高生产力、易于调试。Vue:MVVM 架构什么是 MVVM?MVVM(Model-View-ViewModel)是一种软件设
发表于:2024-04-28 浏览:301 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 浏览:235 TAG:
【Vue】vue使用后端提供的接口
在 vue 中使用后端接口可通过以下步骤实现:安装 axios 库并导入。使用 axios 对象创建 http 请求,如 get 或 post。使用 data 选项传递数据。处理响应,使用 data 属性访问后端返回的数据。使用 vuex 管理从后端获取的数据,通过组件访问。在 Vue 中使用后端接口在 Vue.js 应用中使用后端提供的接口可以让你与服务器通信,获取和更新数据。本文将介绍如何在 Vue 中使用后端接口。1. 安装 Axios首先,你需要安装 Axios 库,这是一个用于发起 H
发表于:2024-04-18 浏览:291 TAG:
【Vue】Antd Pro Vue的使用(二)—— 全局配置及登录
1. 默认语言设置&nbsp;Antd Pro Vue安装好之后,默认使用的是英文,我们需要把它设置为中文简体。找到/src/core/bootstrap.js文件,把最后一行 en-US 修改为 zh-CN,然后一定要清除浏览器缓存,修改才能生效修改后修改后的页面2. 请求服务端接口Antd Pro Vue封装好的有请求方法,在/src/api/文件夹,我们把自己的接口写到这里面就可以任意调用。Antd Pro Vue安装好之后,默认使用的是mock数据,我们要使用自己的接口,要把mock去掉
发表于:2024-04-25 浏览:460 TAG:
【Vue】vue2应用与vue3的不同之处
上一篇,我使用了vue2创建了一个应用,这次我使用vue3创建一个应用,看一下两者有什么不同。如下,是我用cue3创建的应用目录发现和vue2应用的目录一模一样,然后我用对比工具对比了两者的文件。1. 文件区别下面是package.json文件的区别,首先vue版本不同,对应的扩展组件也不同。下面是main.js的不同然后是APP.vue的不同2. 全局实例改变2.x 全局 API3.x 实例 API (app)Vue.configapp.configVue.config.productionT
发表于:2024-04-23 浏览:338 TAG:
【Vue】Andt Pro Vue的使用(六) —— 描述列表a-descriptions设置label和content的样式
1、&nbsp;a-descriptions设置label和content的样式在react组件中,可以直接设置labelStyle和contentStyle,来设置label和content的样式,但是在vue2的组件中,官方并没有给出响应的设置方法如下是我的订单详情页面label的宽度是自适应的,想要设置为固定的宽度,网上找了好多方法,都不生效直到遇到下面的方法https://blog.csdn.net/fifty_one/article/details/120219194 要使用/deep,
发表于:2024-05-09 浏览:381 TAG:
【Vue】Antd Pro Vue的使用(三)—— table列表的使用
用了几天ant design pro vue,发现vue2真的不是很好用,各种写法好麻烦。还有研究组件时,一定要看低版本的组件,高版本都是vue3的,并不适用。vue2版本组件位置:https://1x.antdv.com/components/alert-cn/ 作为后台管理端,用到最多的就是table列表,官网给的有预览但是自己上手的时候有事另外一回事了,首先就是接口请求的数据结果,官网并没有介绍接口应该返回什么样的数据结构,导致接口成功请求到数据,但table就是无法正常显示,最终参考de
发表于:2024-04-26 浏览:377 TAG:
【Vue】Antd Pro Vue的使用(十二) —— 菜单选中高亮显示问题
Antd Pro Vue2这套框架的路由菜单有两个问题,1、页面迁移子页面,父页面对应的菜单未能选中高亮显示2、登录后默认的菜单或页面刷新后原来的菜单未选中高亮显示网上查到的一些菜单配置都是新版本的,老版本并不支持这些方法,这里总结一下我的解决方法,如果大家有更好的解决方案,欢迎交流。一、解决第一个问题以我的菜单为例商品列表是菜单页面,添加编辑商品是隐藏菜单,我把他们做成了兄弟菜单,而不是子菜单/src/config/router.config.js配置如下然后再/src/layouts/Bas
发表于:2024-05-14 浏览:260 TAG:
【Vue】Antd Pro Vue的使用(十一) —— 富文本编辑器wangeditor的使用(避坑)
我承认,antd pro vue2是免费的,已经是老版本了,有多老呢,自带的wangeditor竟然是V3.1.1版本的,两年前无用wangeditor的时候已经是V5版本了,V3简直是上古的东西,官网都没有找到V3版本的资料。。。。,好在最后还是找到了一份V3版本的资料,要不然又要花时间去用新版本了。wangeditor V3参考资料:https://www.kancloud.cn/wangfupeng/wangeditor3/335782下面是框架组件的配置:问题出现的原因:做商品详情的时候
发表于:2024-05-10 浏览:273 TAG:
【Vue】Vue定义全局变量的方法
在Vue项目中我们需要使用许多的变量来维护数据的流向和状态,这些变量可以是本地变量、组件变量、父子组件变量等,但这些变量都是有局限性的。在一些场景中,可能需要在多个组件中共享某个变量,此时全局变量就派上了用场。定义全局变量的方法1. 使用Vue.prototype定义全局变量通过在 vue 的原型上定义属性,可以在所有组件中访问该属性。在main.js定义全局变量// main.jsVue.prototype.baseUrl = &quot;https://www.example.com/api
发表于:2024-04-22 浏览:351 TAG: