跳到主要内容

分片上传

大致流程如下:

  • 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
  • 初始化一个分片上传任务,返回本次分片上传唯一标识;
  • 并发传多个小的文件切片,可以大大减少上传时间
  • 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件

为什么需要分片上传?​

  • 1、增加上传效率: 当需要上传大文件时,将整个文件作为一个单独的请求传输可能会导致较长的传输时间和较高的网络负载,通过将文件切分成较小的分片,可以将文件上传任务划分为多个独立的请求,减少单个请求的传输时间和网络负载
  • 2、稳定性和可靠性,防止上传失败:如果在上传过程中出现网络故障,只需要重新上传失败的分片,避免了整个文件上传失败的情况。

何时合并切片,即切片什么时候传输完成?​

当所有切片都已成功上传到服务器时,就可以进行切片合并了,

实现方式

  • 当最后一个切片上传完成后,客户端向服务器发送一个请求,告诉服务器所有切片都已上传完成,可以进行切片合并了

    发一个合并请求,主动通知服务端进行切片的合并

如果某一个切片出错,或者某一个切片的网络异常,如何解决?​

有两种方式

  • 1、重新发送这个切片请求
  • 2、也可以直接抛错,然后中止运行程序,通过throw抛错,然后promise.all的时候的catch捕捉之后就"提醒网络出错,请重新发送"
js
// 发送切片
await Promise.all(reqList).then(res => {
}).catch(err => {
//接受到错误,在这里终止断js的进程程序,不再继续往下走了
message.error('系统错误,请重发');
throw '系统错误,请重发'
});

代码实现​

react例子

js
// 文件上传 【文件切片】
import { useMemo, useState } from 'react'
import { createChunk, request, mergeRequest, createProgressHandler } from './utils'

type FileType = {
chunk: Blob
hash: string // 严格来说,应该要根据文件内容生成 hash
name: string
percentage: number // 进度百分比
}

const FileUpload = () => {
// 存储文件对象
const [fileData, setFileData] = useState<File>()
// 文件切片
const [fileSlice, setFileSlice] = useState<FileType[]>([])

/**
* 总的上传进度
* 将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度
*/
const uploadPercentage = useMemo(() => {
if (!fileData?.name || !fileSlice.length) return 0
const totalPercentage = fileSlice
.map((item) => item.chunk.size * item.percentage)
.reduce((acc, cur) => acc + cur)
return parseInt((totalPercentage / fileData.size).toFixed(2))
}, [fileSlice])

const handleFileChange = (e) => {
const [file] = e.target.files // 拿到文件对象
if (file) {
setFileData(file)
}
}

// 文件上传
const handleUpload = async () => {
if (fileData) {
const fileChunkList = createChunk(fileData)
const fileSlice = fileChunkList.map((item, index) => {
return {
chunk: item.file,
hash: fileData.name + '-' + index,
name: fileData.name,
percentage: 0,
}
})
const formList = fileSlice.map((item) => {
const { chunk, hash, name } = item
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('hash', hash)
formData.append('filename', name)
return formData
})

const requestList = formList.map((formData, index) => {
return request({
url: 'http://localhost:3000',
data: formData,
onProgress: createProgressHandler(fileSlice[index]),
})
})
try {
// 并发请求处理文件切片块
await Promise.all(requestList)
// 当所有切片都已成功上传到服务器时,就可以进行切片合并了
await mergeRequest({ fileName: fileData.name })
} catch (err) {
console.log(err, 'err')
}
// 文件切片,保存切片块、上传进度
setFileSlice(fileSlice)
}
}

return (
<div>
file-upload
<input type='file' id='upload-file' onChange={handleFileChange} />
<button onClick={handleUpload}>上传</button>
</div>
)
}

export default FileUpload

utils.ts

js
type RequestParams = {
url: string
method?: string
data: XMLHttpRequestBodyInit
headers?: HeadersInit
onProgress?: (event: ProgressEvent) => void // 监听上传进度回调
}
export function request({ url, method = 'post', data, headers = {}, onProgress }: RequestParams) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// 监听上传进度
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key])
})
xhr.open(method, url)
xhr.send(data)
xhr.upload.onprogress = onProgress
// 捕获错误
xhr.onerror = (e) => {
reject('发生错误,请重试')
return
}
// 监听请求,接受响应
xhr.onreadystatechange = () => {
// readySates == 4 表示 请求已完成,且响应已就绪
// xhr.status == 200 表示请求成功
// 304 协商缓存
if ((xhr.readyState == 4 && xhr.status == 200) || xhr.status == 304) {
resolve(JSON.parse(xhr.responseText))
} else {
reject('发生错误,请重试')
}
}
})
}

// 文件上传进度
export function createProgressHandler(fileItem) {
return (event: ProgressEvent) => {
// 如果文件切片总字节数未知,则 event.lengthComputable 属性会返回 false
if (event.lengthComputable) {
// event.loaded 表示已上传的字节数 event.total 表示文件切片总字节数
const percentComplete = (event.loaded / event.total) * 100
console.log(`Uploaded ${percentComplete}%`)
fileItem.percentage = percentComplete
}
}
}

/**
* 创建切片
* @param file 文件对象
* @param size 切片大小
* @returns
*/
export function createChunk(file: File, size = 2 * 1024 * 1024) {
//两个形参:file是大文件,size是切片的大小
const chunkList = []
let cur = 0
while (cur < file.size) {
chunkList.push({
// //使用slice()进行切片,每份以size进行切片
file: file.slice(cur, cur + size),
})
cur += size
}
return chunkList
}

// 合并请求
export async function mergeRequest({ fileName }: { fileName: string }) {
await request({
url: 'http://localhost:3000/merge',
headers: {
'content-type': 'application/json',
},
data: fileName,
})
}