超大视频文件分片上传

普通上传

在使用ajax文件上传,一般分3个步骤:

  1. HTML

HTML中有一个input用于选择文件:

<input type="file" id="selectFile" />

可以通过accept属性用于限制选择视频文件还是图片以及视频和图片具体是哪种类型.

<input type="file" id="selectFile" accept=".mp4"/>
或
<input type="file" id="selectFile" accept="image/*"/>
  1. js监听事件
$('#selectFile').on('change', function() {
    let file = this.files[0];
});
  1. 上传
const fd = new FormData();
fd.append('file', file);
const xhr = new XMLHttpRequest();

xhr.open('POST', api);

// 上传进度
xhr.upload.addEventListener('progress', function (ev) {
    var progress = ((ev.loaded / ev.total) * 100).toFixed(2) + '%';
    
});

// 上传结果
xhr.onreadystatechange = () => {
    if(xhr.readyState === 4 && xhr.status === 200){
        
    }
}

xhr.send(fd);

但是对于视频,如一部电影,直接用上面的方式上传,有可能遇到的问题:

  • 超时
  • 超出大小限制

这时候就需要分片上传文件.

分片上传

分片上传是建立在普通上传基础之上的,原理是将大文件分成N份,不断上传每一份,上传完后后台将这些块合并成一个文件.
因为要重复上传每一份分片,先定义一个函数:

function chunkUpload(video, loaded = 0, chunkName = '', target = '') {
        
}

第一个参数video为需要上传的数据,第二个参数loaded表示已经上传了多少了,第3,4个参数用于安全校验.文件如何分片呢?这里只需要用到slice函数就能实现分片:

let chunk = video.slice(loaded, chunkSize);
fd.append('file', chunk);

chunkSize是每份大小,单位是B,文件由分片替代。同样在onreadystatechange中检查是否上传完成,完成后再次调用chunkUpload上传下一个分片直到上传完成.

上传进度计算

上传进度不同于普通上传,使用已上传的数据除以总大小数据就可以得到一个进度.分片上传的进度是每片的进度,可以有两种方式展示进度:

(1). 当前已上传分片数/总分片数,相对简单
(2). (当前已上传大小 + progress事件中返回的大小)/文件总大小,进度计算稍微复杂一点

完整代码如下:

function chunkUpload(video, loaded = 0, chunkName = '', target = '') {
    const chunk = size_limit;

    const fd = new FormData();
    const nextChunk = loaded + chunk;
    const currentChunk = video.slice(loaded, nextChunk);
    const uploadChunk = loaded + currentChunk.size;
    const end = uploadChunk >= video.size ? 1 : 0;
    fd.append('file', currentChunk)
    fd.append('end', end)
    fd.append('chunk', chunk)
    fd.append('total', video.size)

    const xhr = new XMLHttpRequest();
    xhr.open('post', "api url"),
    xhr.upload.addEventListener('progress', (progress) => {
        const percent = ((loaded + progress.loaded) / video.size * 100).toFixed(2) + '%';
    })

    xhr.addEventListener('abort', (e) => {
        console.log('abort');
    })
    xhr.upload.addEventListener('abort', () => {
        console.log('abort upload');
    });
    xhr.setRequestHeader("file-index", Math.floor(nextChunk / chunk));
    xhr.setRequestHeader("chunk-name", chunkName);
    xhr.setRequestHeader("chunk-token", target);
    xhr.onreadystatechange = () => {
        if(xhr.status === 200 && xhr.readyState === 4) {
            try{
                const resp = JSON.parse(xhr.response);
            
                if(resp.error_code === 0 ) {
                    if(nextChunk <= video.size) {
                        chunkUpload(video, nextChunk, resp.data.code, resp.data.target);
                    }else {
                        // 上传完成后的逻辑处理
                    }
                }else {
                    // 错误处理
                }
            }catch(e) {
                // 错误处理
            }
        }
    }
    xhr.send(fd)
}

可以看到,和普通上传相比,我们多上传了3个参数,分别是end,chunk,total.end表示是否是最后一片,如果是最后一片了那么服务端应该在接收后将之前的所有分片都合并起来.chunk是每片的大小,total是文件总的大小.另外,为了防止出现意外,还在header头部,加入了3个参数:file-index,chunk-name,chunk-token.file-index是每片的序号,从1开始,以保证合成的文件是正确的.chunk-namechunk-token是安全校验的.在第一个分片上传的时候,服务端随机生成一串字符串(chunk-name),再MD5加密作为文件名.注意,这里chunk-name是未md5前的字符串,假如直接使用文件名,那别人传入../../xxx这类值会访问到你的其他路径.而chunk-token是分片保存的完整路径加密后字符串.