文件切片上传和断点续传

1,388 阅读1分钟

上传文件是常用的操作,但是当在上传大文件时,如果碰到网络不稳定或者被刷新页面时,这个文件就得重新上传了,对用户很不友好。 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判断,如果上传过,上传到了第几片。通过服务器返回的已经上传的结果,我们可以通过分片结果获取到剩余的部分进行上传;如果没有上传过,便从第一块进行上传。如果是页面重新加载或者上传中断,只需要在重新上传之前查询在哪一片中断便可以继续上传。