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

1 篇文章 0 订阅
1 篇文章 0 订阅

前言

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}`
  })

})

小结

看到这里你已经会大文件上传、断点续传、秒传、多大文件上传了,这是个人的一些想法思路,如果有更好的方法可以分享,有问题可以留言评论

如果你感觉这篇文章对你有帮助,欢迎关注我的博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值