这是系列文章:

express实现文件分片上传(一)JS原生文件分片及axios合并请求

express实现文件分片上传(二)后端API的实现

一、需求

在使用 express 做后端的时候,想要实现一个分片上传的功能。

分片上传不使用插件,通过 HTML5 的 File API,同时上传的时候使用 axios 进行 HTTP 请求。

二、原理

分片主要是通过 File APIslice() 进行实现切片,然后通过构建 FormData 发送数据。

1、设定每个 chunk 的大小:

const eachSize = 2 * 1024 ; // 每个chunks的大小

2、获取到能够分片的总数:

const blockCount = Math.ceil(file.size / eachSize); // 分片总数

3、获取到文件的一些基本信息:

这些信息是后端需要拿到的数据,比如 ext 文件后缀,还有 hash

hash 的主要作用是为了前后端文件分片的标识,这里只是用了 Date.now() 和 随机数 和 File.lastModified 的拼接而已,实际应用中,为了保证真正的唯一性可以借助uuid结合用户id这种操作。

// 获取文件后缀名
let ext = file.name.split('.');
ext = ext[ext.length-1]; 
// 通过hash标识文件
let random = Math.random().toString();
random = random.split('.');
random = random[random.length-1];
let hash = Date.now()+random+file.lastModified; 

4、循环每个分片,进行操作

文件上传通过 FormData() 构建表单,默认是 multipart/form-data,这样可以后端可以像处理正常的文件上传一下处理分片上传。

下面的文件上传方法我使用了多个回调函数参数,可以方便的处理 progress 事件、success 事件 、 error 事件。

同时,在 for 循环处理的时候,没有直接发送,而是将 axios.post() 全部写入到一个数组中,最终通过 axios.all() 当所有的 Promise 处理结束后,再请求分片合并。

 /**
     * @name fileSliceUpload
     * @description 文件分片上传操作
     * @param file 文件File
     * @param handleXhrProgressCallback // progress事件回调
     * @param handleXhrSuccessCallback // 请求成功事件回调
     * @param handleXhrErrorCallback // 请求失败事件
     */
    function fileSliceUpload(file,handleXhrProgressCallback,handleXhrSuccessCallback,handleXhrErrorCallback){
        const eachSize = 2 * 1024 ; // 每个chunks的大小
        const blockCount = Math.ceil(file.size / eachSize); // 分片总数
        const axiosArray = []; // axiosPromise数组
        let ext = file.name.split('.');
        ext = ext[ext.length-1]; // 获取文件后缀名
        // 通过hash标识文件
        let random = Math.random().toString();
        random = random.split('.');
        random = random[random.length-1];
        let hash = Date.now()+random+file.lastModified; // 文件 hash 实际应用时,hash需要更加复杂,确保唯一性,可以使用uuid
        // 处理每个分片的上传操作
        for(let i=0; i<blockCount; i++){
            let start = i * eachSize,
                end = Math.min(file.size,start+eachSize);
            // 构建表单
            const form = new FormData();
            form.append('file',file.slice(start, end));
            form.append('name',file.name);
            form.append('total',blockCount);
            form.append('ext',ext);
            form.append('index',i);
            form.append('size',file.size);
            form.append('hash',hash);
            // ajax提交 分片,此时 content-type 为 multipart/form-data
            const axiosOptions = {
                onUploadProgress:(e)=>{
                    handleXhrProgressCallback(blockCount,i,e)
                }
            };
            // 加入到 Promise 数组中
            axiosArray.push(axios.post('/upload_chunks',form,axiosOptions));
        }
        // 所有分片上传后,请求合并分片文件
        axios.all(axiosArray).then(()=>{
            // 合并chunks
            const data = {
                name:file.name,
                total:blockCount,
                ext,
                hash
            };
            // 请求分片合并
            axios.post('/merge_chunks',data).then((res)=>{
                handleXhrSuccessCallback(res.data);
                clearFileInput();
            }).catch((err)=>{
                handleXhrErrorCallback(err);
                clearFileInput();
            })
        });
    }

5、一些其他的操作函数

有一些其他的操作方法,与分片上传没有直接的关系,只是辅助。

    // 一些全局变量
    const fileInput = document.getElementById("fileInput");
    const progressListContainer = document.getElementById('progressListContainer');

    // 触发文件表单选择文件
    function handleActiveFileInput(){
        fileInput.click();
    }
    // 清除file input 的value内容
    function clearFileInput(){
        fileInput.setAttribute('value','');
    }
    // 文件选择变化处理
    function handleFileInputChange(){
        const file = fileInput.files[0];
        if(!file) return false;
        progressListContainer.innerHTML = '';
        fileSliceUpload(file,handleXhrProgressCallback,handleXhrSuccessCallback);
    }
    /**
     * @name handleXhrProgressCallback
     * @description 上传chunk的 progress 事件处理
     * @param form // 上传的表单数据
     * @param e // 事件处理
     */
    function handleXhrProgressCallback(total,index,e){
        const liHtm = document.createElement('li');
        liHtm.innerText = `当前上传第 ${index + 1} 个chunk,共计 ${total}`;
        progressListContainer.appendChild(liHtm);
    }
    // 上传成功处理
    function handleXhrSuccessCallback(data){
        const imgShowContainer = document.getElementById("imgShowContainer");
        imgShowContainer.innerHTML = ''; // 清空显示的内容
        if(data.code === -1){
            return alert('上传失败');
        }
        const img = document.createElement('img');
        img.style.width = '300px';
        img.src = `uploads/${data.data.path}`;
        img.onload = ()=>{
          const linkElem  = document.getElementById('imgUrlLink');
          linkElem.setAttribute('href',img.src);
          linkElem.innerText = img.src;
          imgShowContainer.appendChild(img);
        };
    }
    // 上传失败处理
    function handleXhrErrorCallback(err){
        console.log(err);
    }