上传文件是常用的操作,但是当在上传大文件时,如果碰到网络不稳定或者被刷新页面时,这个文件就得重新上传了,对用户很不友好。
JS中可以通过onchange
事件监听<input type='file'/>
元素,上传文件时onchange
可以得到File对象。我们可以通过使用slice
来进行切片。注意:文件通过slice
截取的内容为Blob格式
function createChunks(file, chunkSize) {
const chunks = []
for(let i = 0; i < file.size; i+=chunkSize) {
chunks.push(file.slice(i, i + chunkSize))
}
return chunks
}
利用这个方法就可以自定义分片大小,然后对每个分片做ajax请求。 文件的分片上传功能就已经完成了
当我们使用这个方式进行分片时,会发现方法瞬间完成,这是因为结果中的File或者Blob只保存文件的size、name等一些文件信息,而不是保存文件的数据。如果我们要访问文件数据则需要通过创建一个FileReader()
这时我们还会想到另外一个问题:当我们正在执行分片上传时,如果用户刷新了页面,那我们重新上传文件时还需要上传之前已经上传过的切片吗? 当分片上传被中断时,我们需要告诉后端这个文件是否被上传过,还需要上传后续哪些分片。
那这个文件我们怎么去获取?通过文件名称?因为通过文件名等一些字段容易导致重复。所以采用hash算法将文件转化为特定的字符串,使用hash算法的好处还有就是当文件哪怕只改变了一个字节,整个结果都会完全不一样。这次采用的是hash算法中的md5
npm i spark-md5 // or
yarn add spark-md5
拿到文件后我们最好不要一次性计算整个文件的hash,如果是一个上百G的文件,一次性把数据读到内存中计算可能会撑爆内存,我们要使用增量算法(分块),先用一块数据计算出一个结果,计算过后这个数据就不需要要了,然后将这个结果和下一个数据算出的结果计算出一个新的结果。
步骤就是先读取第一个分块的结果,然后递归调用自身读取下一个分块的结果
import SparkMD5 from 'spark-md5'
function hash(chunks) {
new Promise((resolve) => {
const sparkMD5 = new SparkMD5()
function _read(i) {
if (i >= chunks.length) {
resolve(sparkMD5.end()) // 结果
return; // 读取完成
}
const blob = chunks[i]
const reader = new FileReader()
// 这步会消耗时间
reader.onload((e) => {
const bytes = e.target.result // 读取到的字节数组
})
sparkMD5.append(bytes)
_read(i+1)
reader.readAsArrayBuffer(blob)
}
_read(0)
})
}
input.onchange(async (e) => {
const file = input.files[0]
if (!file) return;
const chunks = createChunks(file, 10*1024*1024) // 10Mb分片
const result = await hash(chunks)
console.log(result)
})
通过得到这个result判断,如果上传过,上传到了第几片。通过服务器返回的已经上传的结果,我们可以通过分片结果获取到剩余的部分进行上传;如果没有上传过,便从第一块进行上传。如果是页面重新加载或者上传中断,只需要在重新上传之前查询在哪一片中断便可以继续上传。