大文件上传的正确打开方式

一、前言

想象两个场景:

场景一: 假设现在有一个需求,需要上传一个超过 10G 的高清视频,或者是一个很大的压缩包。如果直接上传,在上传过程中可能用户会由于网络问题导致中断。用户网络恢复的时候,又要从头开始上传。这会给用户带来很不好的使用体验。

解决方案: 对文件进行分片,每次上传文件的一小部分,当网络中断以后,文件之前上传的部分已经保存在服务器,只需要上传剩余部分即可,从而实现断点续传

场景二: 现在有100个用户,在不同的时间,需要上传同一个很大的高清视频,视频内容完全一致,视频的名字随机。服务器需要上传处理所有的上传吗?

解决方案: 秒传,第一个用户上传的同时,根据文件内容计算出文件的hash ,把hash一并发给服务器。以后每次上传,服务器只需要判断是否存在对应的hash,存在返回上传成功,复用同一个大文件。

什么是文件 hash?

是一个根据文件内容计算一串唯一的字符串的单向算法,也就是说,文件 ===> hash 是可行的,但是 hash ===> 文件 是不可行的。 我们只需要判断hash是否相同,而不必比较文件的每一个字节,就可以知道文件是否改变,或者是否是同一个文件。一般采用一些第三方库来实现。前端可以采用spark-md5 这个库。

二、大文件上传前端需要解决的两个核心问题

这个过程中,前端需要解决的两个核心问题:

1. 文件如何分片?

使用 <input type='file'> 收集到的files是一个文件数组,每一项都是一个文件对象,该文件对象继承于Blob 对象,而Blob 对象的原型上有一个slice 方法。该方法类似数组原型上的 slice, 可以用于对文件进行分片。

也就是说:

js 复制代码
<input type='file' name='bigFile'/>

<script>
    input.onChange = (e)=>{
        const file = e.target.files[0]
        console.log(file instanceOf Blob)
        console.log(Blob.prototype)
    }
</script>

直接开始分片。。。。

js 复制代码
// 文件分片算法  
const splitChunk = (file,perBlobSize)=>{  
const result = []  
let j = 0;  
for (let i = 0; i < file.size; i += perBlobSize) {  
    result.push(file.slice(i,i+perBlobSize))  
    j++;  
}  
    return result;  
}

没错就是这么简单,文件分片的结果就是一个由Blob对象构成的数组。

注意: 分片的时候,并非是把文件分割成真正的小文件,仅仅是获得每一个分片文件的基本信息和存储的位置,速度是很快的,并不会对主线程造成很大的负担,因此无需开辟额外的线程

2. 如何唯一标识每一个分片,唯一标识大文件的分片。

可以使用hash算法, spark-md5 这个库就能实现。

主要有两个问题需要解决:

  1. 计算hash值的过程是一个CPU 密集型任务 ,我们知道 js, 是一门单线程的语言,在js 执行期间,页面会被阻塞,具体表现为:动画卡顿、用户交互失效(其实在队列中等待执行)。因此需要单独开启一个线程来进行hash值得计算。 可以采用Web Worker 实现。
  2. 不能直接计算整个大文件的hash, 因为文件太大会导致内存溢出,因此需要依次计算每一个分片。在spark-md5 这个库中也考虑到了这点,提供了对应的api。

不废话,上代码:

子线程 计算hash:

js 复制代码
// getHashByChunks.js
// 子进程中不能使用import
importScripts('./spark-md5.js')

// 计算 hash 值,可以用
const getHashByChunks = (chunks)=>{
  return new Promise(resolve=>{
    const spark = new SparkMD5()
    function _read(index){ //递归计算hash
      if (index >= chunks.length){
        resolve(spark.end()) // 获取最终的结果
        return // 完成
      }
      const blob = chunks[index];
      const reader = new FileReader();
      reader.onload = e=>{
        const bytes = e.target.result // 读取到的字节数组
        spark.append(bytes)
        _read(index+1) // 递归
      }
      reader.readAsArrayBuffer(blob);
    }
    _read(0) // 开始计算
  })

}

// 和主线程通信,把计算结果返回
onmessage =  async (e)=>{
 // 接收主线程传过来的分片数组
  const {type,chunks} = e.data 
  if(type === 'getHashByChunks'){
    postMessage({
      type:'computedHashByChunks',
      //使用WebWork的时候Promise 不能被克隆,因此不能直接返回Promise
      hashPromise: await getHashByChunks(chunks)
    })

  }
}

主线程:也就是浏览器的渲染主线程:

js 复制代码
// 文件分片上传接口
const input = document.querySelector('input[type="file"]')
// 开辟新的线程用于计算 hash, 如果使用相对路径,这个路径是相对于运行时候的主线程的
const worker = new Worker('./getHashByChunks.js')
let fileHash
input.onchange =  async (e)=>{
  // 获得文件
  let file = input.files[0];
  // 分片
  const chunks = splitChunk(file,200)
  // 可以使用 web worker 单独开辟一个线程去计算 cpu 密集型任务
  worker.postMessage({ //线程通信
    type:'getHashByChunks',
    chunks
  })

}
// 计算结果在这
worker.onmessage =  async e=>{
  const {type,hash} = e.data
  if(type === 'computedHashByChunks'){
    fileHash =  hash;
    console.log(fileHash);
  }
}
// 文件分片算法
const splitChunk = (file,perBlobSize)=>{
  const result = []
  let j = 0;
  for (let i = 0; i < file.size; i += perBlobSize) {
        result.push(file.slice(i,i+perBlobSize))
    j++;
  }
  return result;
}

至此,webWork子线程计算hash, 文件分片已经完成!

==== 下次有空接着完善具体的上传 =====

留下几个我目前需要完善的问题:

1、如何设计接口,使用ajax 发起请求?

使用XMLHttpRequest

2、中途某一个分片上传失败(丢包,网络问题等原因)如何解决?

类似tcp 协议,每次上传后端的响应中,携带 expectIndex,代表期待的下一个分片(代表这个分片以前的分片后端都收到了)。

3、如何显示上传进度?

一种方案是根据已经发送的分片的数量和总分片数量,还有一种方案是根据expectIndex 和 总的分片数量。

4、后端怎么知道文件上传完成?

初步想法: 每次请求携带分片的总数量,文件hash,和当前上传的内容,当前上传的分片编号

有空接着写,睡觉~~~~

相关推荐
昨天;明天。今天。几秒前
案例-任务清单
前端·javascript·css
zqx_71 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己1 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称2 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色2 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
BigYe程普3 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H3 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍3 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发