大文件上传一般有如下几种方法:直接上传、分片上传、并发上传、断点上传。
- 直接上传
这种方式在大文件上传时一般不推荐。首先,上传速度慢;其次,如果中间上传的时候出现问题,那么就需要重新上传。
- 分片上传
顾名思义,将大文件分成每个小 chunk 块,如此就可以每次传输的时候以块为单位,如果出问题的话就只需要从当前位置开始上传。
- 并发上传
利用浏览器的并发能力,把请求分批发送,每次并发 11 个,nodejs 同一个 IP 最多可以异步处理 11 个请求。
- 断点上传
每次将上传的节点存储到 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