大文件上传

238 阅读2分钟

大文件上传一般有如下几种方法:直接上传、分片上传、并发上传、断点上传。

  1. 直接上传

这种方式在大文件上传时一般不推荐。首先,上传速度慢;其次,如果中间上传的时候出现问题,那么就需要重新上传。

  1. 分片上传

顾名思义,将大文件分成每个小 chunk 块,如此就可以每次传输的时候以块为单位,如果出问题的话就只需要从当前位置开始上传。

  1. 并发上传

利用浏览器的并发能力,把请求分批发送,每次并发 11 个,nodejs 同一个 IP 最多可以异步处理 11 个请求。

  1. 断点上传

每次将上传的节点存储到 localStorage 中,下次上传从 localStorage 中查找是否有这个文件的节点存在,如果有这个,从这个节点上传;如果没有,重新上传。

参考文章:www.jianshu.com/p/47d4f2be0…

import { useRef, useState } from 'react'
import './App.css'

function App() {
  const tboxRef = useRef(null)
  const fileRef = useRef<HTMLInputElement>(null)

  const [percent, setPercent] = useState(0)

  const postAjax = (url: string, fd: FormData) => {
    const xhr = new XMLHttpRequest()
    return new Promise((resolve) => {
      xhr.open('POST', url, true)
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          const res = JSON.parse(xhr.responseText)
          // 断点上传时需要加上以下代码
          if (res.hash) {
            window.localStorage.setItem('fileName', res.hash)
          }
          resolve(res)
        }
      }
      xhr.send(fd)
    })
  }
  const url = 'http://127.0.0.1:1000/file/uploading'
  const mergrUrl = "http://127.0.0.1:1000/file/mergrChunk"
  let fileName = ''
  const uploadFile = () => {
    const file = fileRef.current?.files?.[0]
    // 1. 直接上传
    blockUpload(file)

    // 2. 分片上传
    chunkedUpload(file)

    // 3 . 并发上传
    parallelUpload(file)

    // 4. 断点上传
    const pointHash = window.localStorage.getItem(fileName) || 0
    breakPointUpload(file, +pointHash)

  }
  const blockUpload = (file: File | undefined) => {
    if (!file) return
    const fd = new FormData()
    fd.append('file', file)
    fd.append('fileName', file.name)
    postAjax(url, fd)
  }

  const chunkedUpload = async (file: File | undefined) => {
    if (!file) return
    const chunkSize = 1024
    for (let start = 0; start <= file.size; start += chunkSize) {
      const chunk = file.slice(start, start + chunkSize)
      const fd = new FormData()
      fd.append('chunk', chunk)
      fd.append('hash', start + '')
      fd.append('fileName', file.name)
      // 上传 利用 async 实现同步请求
      let per = Math.floor(100 * start / file.size)

      if ((file.size - start) < chunkSize) {
        per = 100
      }

      await postAjax(url, fd)
      setPercent(per)
    }
  }

  const parallelUpload = async (file: File | undefined) => {
    if (!file) return
    const chunkSize = 1024
    let postQueue = []
    const parallelNum = 11

    for (let start = 0; start <= file.size; start += chunkSize) {
      const chunk = file.slice(start, start + chunkSize)
      const fd = new FormData()
      fd.append('chunk', chunk)
      fd.append('hash', start + '') // node.js 接收时做为文件名
      fd.append('fileName', file.name)
      // 上传 利用 async 实现同步请求
      let per = Math.floor(100 * start / file.size)

      if ((file.size - start) < chunkSize) {
        per = 100
      }

      await postAjax(url, fd)

      if (postQueue.length < parallelNum) {
        postQueue.push(postAjax(url, fd))
      }

      if (postQueue.length >= parallelNum || per === 100) {
        Promise.all(postQueue).then(() => {
          setPercent(per)
          postQueue = []
        })
      }
    }
  }

  const breakPointUpload = async (file: File | undefined, pointHash: number) => {
    if (!file) return
    const chunkSize = 1024 * 10;
    let postQueue: { post: Promise<any>, hash: number }[] = [];
    const parallelNum = 25; //谷歌最大线程数量 大于11后提效不明显,node.js在1s内最多异步处理11个请求
    for (let start = pointHash; start <= file.size; start += chunkSize) {
      const chunk = file.slice(start, start + chunkSize); // 分片 blob对象
      const fd = new FormData();
      fd.append("chunk", chunk);
      fd.append("hash", start + '');
      fd.append("fileName", file.name)
      window.localStorage.setItem('fileName', file.name);
      // 线程并发
      if (postQueue.length < parallelNum) {
        postQueue.push({ post: (postAjax(url, fd)), hash: start })
      }

      let per = Math.floor(100 * start / file.size);

      if ((file.size - start) < chunkSize) {
        per = 100;
      }
      if (postQueue.length >= parallelNum || per === 100) {
        const postApiQueues = postQueue.map(item => item.post)
        const res = await Promise.any(postApiQueues)
        let hash = res.hash
        const index = postQueue.findIndex(item => item.hash = hash)
        postQueue.splice(index, 1)

        setPercent(per)
        if (per >= 100) {
          await postAjax(mergrUrl, fd)
          let fileName = window.localStorage.getItem('fileName') || '';
          window.localStorage.removeItem(fileName);
          window.localStorage.removeItem('fileName');
        }
      }
    }
  }


  const handleUpload = () => {
    console.log('click')
  }

  return (
    <div className='wrapper'>
      <p className='file-title'>Big File BreakPoint Upload</p>
      <div ref={tboxRef}>
        <div style={{ width: percent + '%' }}>{percent}%</div>
      </div>
      <input ref={fileRef} type='file' onChange={uploadFile} />
      <input type='button' value='文件上传' className='btn btn-warning' onClick={handleUpload} />
    </div>
  )
}

export default App