前端如何分片上传大文件

时间:2021-1-8 作者:admin

最近做了一个需求,让用户本地上传一个最大 300M 的视频文件,下面是前端部分的记录。

需求分析点

  • 校验用户上传的文件类型,以及内存大小,还有视频的播放时长检查
  • 视频过大需要分片上传,记录本次是第几片数据,重新上传则可以实现接着上次的片段继续上传而非重头上传
  • 同个视频需要标记,不可重复上传,减小服务器的内存压力

交互样式

接下来首先如何让用户上传文件,input 标签中type=file可以让用户上传文件,此处我是在 Vue 项目里。

<input ref="videoInput" type="file" title="选择视频文件" @change="handleUpload" />

此时已经可以让用户在本地上传文件了,但是这里有个问题,在该用户已经有上传过的视频文件时,如果再次上传需要首先展示提示信息,重复上传会覆盖原有视频。但是用户点了该 input 按钮,就已经触发选择文件的弹窗了,并不能拦截,加之原生的 input 样式巨丑还自带选择的文件名,这不是我想要的,跟我们的 ui 样式不符,所以我的想法是,隐藏这个 input, 取而代之的是自己重新写一个按钮,给这个按钮加上点击事件,在该点击事件里处理额外逻辑,然后用 js 代码主动触发该 input 的点击事件。

this.$messageBox.confirm("重新上传视频会替换原有视频,是否继续?").then(() => {
  this.$refs.videoInput.click();
});

标记视频

接下来就是如何标记视频,以免重复上传相同的视频。方案是获取视频文件的哈希 md5 值。因为每个文件的 md5 是唯一的,我们在做文件上传的时候,就只要在前端先获取要上传的文件 md5,并把文件 md5 传到服务器,对比之前文件的 md5,如果存在相同的 md5,我们只要把文件的名字传到服务器关联之前的文件即可,并不需要再次去上传相同的文件,再去耗费存储资源、上传的时间、网络带宽。

let file = this.$refs.videoInput.files[0];
console.log(file);

此处可以拿到文件 file,它自身可以获取文件的类型type,以及文件的内存大小size,单位是字节。同时也有slice方法用于截取,分片获得子片段数据 file。
但是直接通过它去获取 md5 值发现跟后端(Java)获取不一致。接下来怎么办?

首先要引入的是spark-md5依赖包

SparkMD5 是 MD5 算法的快速实现。此脚本基于 JKM md5 库,这最适合浏览器使用。

它的原理:(化简后的核心代码)

import SparkMD5 from 'spark-md5';

const fileReader = new FileReader();
fileReader.onload = function (e) {
  let spark = new SparkMD5.ArrayBuffer();
  spark.append(e.target.result); // Append array buffer
  console.info("computed hash", spark.end()); // Compute hash
};
fileReader.readAsArrayBuffer(file);

关于FileReader请移步FileReader

使用browser-md5-file获取文件 md5 值, 它是进一步的封装
import BMF from "browser-md5-file";
const bmf = new BMF();
bmf.md5(
  file,
  (err, md5) => {
    if (err) {
      console.log(err);
    }
    console.log(md5); // md5值
  },
  (process) => {
    //计算进度
  }
);

上传文件

上传文件的数据格式{"content-type": "multipart/form-data"},html5 提供了 api

FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过 XMLHttpRequest.send() 方法发送出去,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 “multipart/form-data”,它会使用和表单一样的格式。

let formData = new FormData();
formData.append("file", item);
axios({
  url: "xxx",
  method: "post",
  headers: { "content-type": "multipart/form-data" },
  data: formData,
});

下面是项目中关于上传的代码逻辑,给大家一个参考吧!

/**
  * id
  * fileList 文件分片后的列表
  * hashList 文件分片后对应的每片数据的md5值列表
  * startIndex 在上一次上传的起点 继续上传 默认第一段
  * processFn 上传进度
  */
function async startMediaPartUpload(id, fileList, hashList, startIndex = 1, processFn) {
  let point = Math.floor(((startIndex - 1) / fileList.length) * 100);
  processFn && processFn(point)
  // 上传
  for (let i = startIndex - 1; i < fileList.length; i++) {
    const item = fileList[i];
    let formData = new FormData();
    formData.append("id", id);
    formData.append("option", "slice");
    formData.append("hash", hashList[i]);
    formData.append("part", i + 1);
    formData.append("file", item);

    let { code, memo, content } = await mediaPartUpload(formData);
    let point = Math.floor(((i + 1) / fileList.length) * 100);
    processFn && processFn(point)

    if (code == "000000") {
      // this.$message.success(`上传ok ${content.part}`);
    } else {
      this.$message.error(memo);

      return Promise.reject("上传失败了");
    }
  }
}

本文使用 mdnice 排版

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。