前端大文件上传

大文件上传是需要前端和后端同时参与才可以实现的。

思路

大文件上传整体思路:文件切片 和 断点续传

前端思路

  • 利用上传控件 input type="file" 绑定一个change事件,在回调中通过事件对象的e.target.files拿到这个文件对象,进行文件对象的slice方法,进行切片,一个大文件就转换成多个小文件了。
  • 借助 http 的可并发性,同时上传多个切片。这样从原本传一个大文件,同时传多个小的文件切片,可以减少上传时间
  • 断点续传:前端在上传切片时,给对应的切片添加一个唯一标识符,上传到后端,服务器需要记住已上传的切片,这样下次就可以跳过之前已经上传的部分

后端思路

  • 服务端负责接受前端传输的切片,并在接收到所有切片后合并所有切片。
  • 服务端保存已上传的切片 hash,上传相同文件要可以进行排除。

切片处理

  • 通过上传文件的控件 input type="file"以及他的change 事件拿到当前上传文件的文件对象e.target.files
  • 将文件对象 进行切片处理,利用slice方法。
  • createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    }

 注意:循环截取.slice方法不是数组的slice方法,而是文件对象原型上的slice方法,继承自 Blob的slice

 以小文件的形式 向后端发请求 Promise.all 

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <el-button @click="handleUpload">upload</el-button>
  </div>
</template>

<script>
import axios from "axios";
const SIZE = 10 * 1024 * 1024;
export default {
  name: "qqqUploadIndex11",
  data: () => ({
    container: {
      file: null
    }
  }),
  methods: {
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    },
    async handleUpload() {
      if (!this.container.file) return;
      const fileChunkList = this.createFileChunk(this.container.file);
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        hash: this.container.file.name + "-" + index
      }));
      await this.uploadChunks();
    },
    async uploadChunks() {
      const requestList = this.data
        .map(({ chunk, hash }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          return { formData };
        })
        .map(({ formData }) =>
          axios({ url: "http://localhost:3000", data: formData })
        );
      await Promise.all(requestList);
    },
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    }
  }
};
</script>
  • 后端接收到所有文件进行合并, 思路有两种
    • 等所有小文件上传成功, 前端这边 发一个请求 给后端合并请求
    • 切片总数据 前端告诉, 后端保存记录切片总数据 后端自己判断也可以

断点续传

断点续传要实现的就是 上传过的切片 不用重复上传,后端一查这个切片上传过了 就是当上传成功了。

断点续传的原理在于 服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分

  • 切片必须带一个可以标识切片的唯一字段,我们之前采用的是 file.name + "-" + index ===> 利用 spark-MD5 插件,生成和文件内容县官的 hash 值,只要内容不变,hash 值就不变。
  • 后端要记住已经上传过的切片, 当前端上传文件切片时 后端可以根据 hash 过滤
  • 全部上传后,后端会进行合并
  • 文件重新上传 只要文件内容相同,都不需要重新上传 直接秒传成功。

如何根据文件内容生成hash的过程

根据文件内容生成hash
spark-MD5 插件,可以根据文件内容生成hash值,这个值只和文件内容相关。

  • 通过const spark = new SparkMD5.ArrayBuffer()得到一个实例 spark。
  • spark实例有方法 append,可以追加一个buffer数据流, 知道读取文件内容结束以后,通过 spark的 end(), 生产对应的hash值。
  • FileReader是一个文件解析器 const reader = new FileReader()
  • readAsArrayBuffer()读取文件的内容,读取文件完成时,会触发onload事件,文件内容在e.target.result
  • 由于 读取文件时异步的,要等到整个文件读取完,才可以 生成对应的hash值,这个过程会阻塞主线程,所以你使用 web worker解决
// 导入脚本
// import script for encrypted computing
// self代表子线程自身,即子线程的全局对象。
Worker 内部如果要加载其他脚本,有一个专门的方法importScripts()。
self.importScripts("/spark-md5.min.js");

// 生成文件 hash
// create file hash
使用self.onmessage指定监听函数的参数是一个事件对象,它的data属性包含主线程发来的数据。
self.onmessage = e => {
  const { fileChunkList } = e.data;
  // 生成 spark 实例, SparkMD5.ArrayBuffer
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();// FileReader 文件解析器
    reader.readAsArrayBuffer(fileChunkList[index].file); 
    // reader.readAsArrayBuffer 可以读取文件的内容 
    // 当文件读取完成的时候 会触发 onload 把这个文件的buffer数据流
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()// 所有的内容读取完整之后 会返回一个hash
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        // self.postMessage()方法用来向主线程发送消息。
        self.postMessage({
          percentage
        });
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

webwork

难点

使用spark-MD5为切片文件添加唯一标识时,因为该插件会先读取文件内容,才能生成标识,而读取内容是异步操作,会造成线程阻塞,影响后面UI交互,后面的任务只能等待着

因此在项目中采用了webWorker来解决该问题,首先创建一个子线程与主线程进行交互,将读取文件内容生成hash标识的操作交给woker子线程,最终在将结果返回主线程,主线程就可以正常交互,不会被阻塞或拖慢。

具体实现

项目中封装了一个 calculateHash方法,这个方法内部创建一个 new worker开启一个子线程并传入了这个Worker 子线线程所要执行的任务

这个方法接收一个fileChunkList参数,代表切片的文件对象

通过返回一个promise对象,在promise内部去new Woker("hash.js")

通过worker.postMessage({fileChunkList})将通过文件对象.slice划分的切片文件传递给子线程

子线程内部通过self.onmessage去接收主线程传递的数据,最终这个切片文件对象会出现在self.onmessage回调函数的形参的e.data属性中,其中self代表子线程的全局对象,也就是子线程自身

因为要使用 spark-MD5 插件,根据内容生成hash标识,所以要先在子线程js文件中通过self.importScripts()加载我们需要的 spark-MD5 插件,这样当子线程接收到切片对象之后,就

可以 new self.SparkMD5.ArrayBuffer() 生成spark实例

然后在内部封装了一个loadNext方法,该方法内部会去new FileReader(),创建文件解析器

接着调用reader.readAsArrayBuffer(fileChunkList[index].file) index==>count

reader.readAsArrayBuffer 可以读取文件的内容 ,并转换成buffer数据流,当文件读取完成的时候,会触发 onload事件,刚刚读取文件的buffer数据流就存在e.target.result属性中

然后通过 spark.append 通过spark实例调用append(e.target.result)传入对应的buffer数据流,

接下来判断是否等于要读取文件的长度,如果等于,代表内容读取完毕

就直接通过self.postMessage({hash:spark.end()}) 所有的内容读取完整之后 通过 spark的 end()方法, 生产对应的hash值,并发送给主线程,并调用self.close()关闭worker节省系统资源

如果没有读取完毕,则继续调用loadNext(count)方法,传入count,这个count是自增之后的值,以次类推,直到读取完毕

最后主线程中定义了一个 worker.onmessage监听函数去监听子线程返回的值,也就是hash标识,同样也会出现在e.data属性中,最终通过resolve(hash)即可

hash.js

// 导入脚本
// import script for encrypted computing
self.importScripts("/spark-md5.min.js");

// 生成文件 hash
// create file hash
// new worker
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        loadNext(count);
      }
    };
  };
  loadNext(0);
};
// 生成文件 hash(web-worker)
// use web-worker to calculate hash
calculateHash(fileChunkList) {
  return new Promise(resolve => {
    this.container.worker = new Worker("/hash.js");
    this.container.worker.postMessage({ fileChunkList });
    this.container.worker.onmessage = e => {
      const { percentage, hash } = e.data;
      this.hashPercentage = percentage;
      if (hash) {
        resolve(hash);
      }
    };
  });
}

FileReader.readAsArrayBuffer() - Web API 接口参考 | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/FileReader/readAsArrayBuffer

Web Worker 使用教程 - 阮一峰的网络日志https://www.ruanyifeng.com/blog/2018/07/web-worker.html

FileReader - Web API 接口参考 | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/FileReader

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值