像视频这种大文件上传有很多缺点:
1.后台可能设置了请求时长限制,太久会上传失败
2.NGINX可能设置了文件上传的最大限制导致失败
优点:
1.文件太大分片上传能加快上传速度,提高用户体验
2.能断点续传 如果上次上传失败或者中途离开的话下一次上传过的就不用重头开始了
3.已经上传过的文件根据HASH查询直接秒传
实现流程
这里只贴核心部分代码
数据结构:
const fileInfoRef = useRef<FileInfoType>({
HASH: '', // 生成的文件hash值
fileSuffix: '', // 文件后缀
alreadyUploadedHashes: [], // 服务器存在的切片hash
uploadedCount: 0, // 上传成功切片个数
url: '', // 上传的url
chunks: [], // 所有切片集合
file: null, // 当前上传的文件
totalSize: 0, // 总大小
currentChunk: null, // 当前上传的切片
})
1.前端用sparkmd5根据文件生成唯一标识HASH 先将文件转成ArrayBuffer再生成HASH
yarn add spark-md5
export const useFileToArrayBuffer =
() =>
(file: File): Promise<ArrayBuffer> =>
new Promise((resolve, reject) => {
try {
const fileReader = new FileReader()
fileReader.readAsArrayBuffer(file)
fileReader.onload = (ev: ProgressEvent<FileReader>) => {
resolve(ev.target!.result as ArrayBuffer)
}
} catch (error) {
reject(error)
}
})
// 将文件转arraybuffer
const arrayBuffer = await fileToArrayBuffer(file)
// 生成文件的HASH值
const spark = new SparkMD5.ArrayBuffer()
spark.append(arrayBuffer)
fileInfoRef.current.HASH = spark.end()
fileInfoRef.current.fileSuffix = /\.([a-zA-Z0-9]+)$/.exec(file.name)![1]
2.文件切片
文件切片有固定大小和固定数量 可以根据文件大小动态设置
使用Blob.prototype.slice方法对file进行切片
以HASH_chunkIndex.fileSuffix作为文件名
// 文件切片
function slice(file: File) {
fileInfoRef.current.file = file
let chunkIndex = 0,
singleSize = file.size > 1024 * 1024 * 5 ? 1024 * 1024 * 5 : file.size, // 默认一个切片,超过5M的话5M一个切片
splitCount = Math.ceil(file.size / singleSize), // 默认按照固定大小切 向上取整多切一个保证文件完整不丢字节
chunks = []
// 如果固定大小数量超出了最大切片数量 则按照固定数量切
if (splitCount > 100) {
splitCount = 100
singleSize = Math.ceil(file.size / 100)
}
while (chunkIndex < splitCount) {
// 切好和文件名存起来 后续请求上传的参数
chunks.push({
file: file.slice(chunkIndex * singleSize, ++chunkIndex * singleSize),
filename: `${fileInfoRef.current.HASH}_${chunkIndex}.${fileInfoRef.current.fileSuffix}`,
})
}
fileInfoRef.current.chunks = chunks
}
3.上传切片
遍历所有切片上传
async function uploadChunks() {
// 上传每一个切片
for (const chunk of fileInfoRef.current.chunks) {
fileInfoRef.current.currentChunk = chunk.file
// 服务器已经存在的切片不再上传
if (fileInfoRef.current.alreadyUploadedHashes.includes(chunk.filename)) {
// 更新上传进度
notifyUpdate()
continue
}
const formData = new FormData()
formData.append('file', chunk.file)
formData.append('filename', `${chunk.filename}`)
await axios.post<UploadSliceResp>(
'http://127.0.0.1:8888/uploadSlice',
formData,
{
onUploadProgress(progressEvent) {
// 更新进度
setUploadPercent(
Number(
((progressEvent.loaded + fileInfoRef.current.totalSize) /
fileInfoRef.current.file!.size) *
100
).toFixed(2)
)
},
}
)
await notifyUpdate()
}
}
4.边上传边更新切片上传进度
async function notifyUpdate() {
fileInfoRef.current.uploadedCount++ // 已上传+1
// 更新当前总上传大小
fileInfoRef.current.totalSize += fileInfoRef.current.currentChunk!.size
// 全部上传完成就合并
if (
fileInfoRef.current.uploadedCount === fileInfoRef.current.chunks.length
) {
fileInfoRef.current.url = await mergeChunks()
}
}
5.如果全部上传成功 则发送合并切片请求
async function mergeChunks() {
const response = await axios.post<UploadSliceResp>(
'http://127.0.0.1:8888/mergeSlice',
{
HASH: fileInfoRef.current.HASH,
count: fileInfoRef.current.chunks.length,
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
)
return response.data.servicePath
}