【原生js】前端文件上传解决方案总结(自查用)

目录

axios的二次封装

基于FormData进行单一文件上传

1.限制上传文件的格式

2.限制文件的上传大小

3.修改input标签的默认样式

4.文件上传服务器

基于base64文件上传

1.发送base64图像数据

2.客户端实现文件浏览

3.文件hash名字的编译处理

文件上传进度条显示

多文件上传

1.获取上传文件信息

2.显示待上传文件/删除待上传文件

3.多文件上传服务器

文件拖拽上传 

大文件切片上传和断点续传

1.获取文件已经上传的切片信息

2.文件的切片处理

3.上传切片

4.通知合并切片


axios的二次封装

在项目中我们通常使用的文件上传的格式为FormData,我们可以通过创建FormData类的实例进行,再通过实例的append方法将和后端开发者约束的字段名进行添加,如下:

let fm = new FormData();
fm.append('file','');

在传递FormData格式的数据时,需要在请求时添加请求头Content-Type:multipart/form-data告知服务器要传递的数据格式,但为了避免每一次请求都要手动添加这个请求头,所以我们要对一些公共的消息进行提取,对axios进行二次封装;

我们需要使用axios.create创建一个axios实例,这样就可以避免和其它的axios模块发生冲突,随后统一设置请求头;

上传文件多使用post请求,如果需要对post请求的请求体的内容做转换,转化成想要的指定的urlencoded格式,可以使用transformRequest,配合指定的插件Qs进行转化。例如:请求体内容为{file:'xxx',filename:'xxx'} 它会转化为 file=xxx&filename=xxx的形式;

对于axios响应的信息,除了data属性是服务器返回的数据外,还有其它的信息,而我们项目中只需要响应主题信息,那么可以设置响应拦截器,直接获取响应的信息主体信息data,这样就不需要在获取响应数据时都去response.data了。

let axiosInstance = axios.create();
axiosInstance.defaults.baseURL = 'http://127.0.0.1:8000'  // 路径配置
axiosInstance.defaults.headers['Content-Type'] = 'multipart/form-data';  // 请求头设置
axiosInstance.defaults.transformRequest = (data,headers) => {
    const contentType = headers['Content-Type'];
    if(contentType === 'application/x-www-form-urlencoded') return Qs.stringify(data);
    return data;
}
axiosInstance.interceptors.response.use(response => {
    return response.data;
},reason => {
    return new Promise.reject(reason)
});

对于少量的接口需要使用其它请求头的,可以单独进行配置,代码如下:

axios.post('xxxx',fm,{
    headers:{
        'Content-Type':'x-www-form-urlencoded'
    }
});

基于FormData进行单一文件上传

文件上传组件使用原生input标签,type为file类型,上传文件会触发change事件,

<input type="file" class="upload_inp">
<script>
    ;(() => {
        let upload_inp = document.querySelector('.upload_inp');
        upload_inp.addEventListener('change',function () {
            console.log(upload_inp.files);
        })

    })()
</script>

我们获取该元素后,可以拿到该元素的files属性,它是一个类数组集合,输出内容如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 可以通过upload_inp.files[0]拿到这个上传的文件的对象信息,其中:

+ name:文件名

+ size:文件大小,单位是字节

+ type:文件的MINE类型

1.限制上传文件的格式

方法1:使用正则

;(() => {
    let upload_inp = document.querySelector('.upload_inp');
    upload_inp.addEventListener('change',function () {
        let file = upload_inp.files[0];
        if(!file) return;
        // 限制上传文件的格式
        if(!/(PNG|JPG|JPEG)/i.test(file.type)) {
            alert('上传的文件只能是PNG|JPG|JPEG格式的');
            return;
        }
    })
})()

方法2:使用accept属性

<input type="file" class="upload_inp" accept=".png,.jpg,.jpeg">

这样选择文件时,就只会显示这三种类型的文件了

2.限制文件的上传大小

通过文件对象的file.size获取选择文件的大小,单位为字节B

// 限制文件的上传大小
if(file.size > 2 * 1024 * 1024) {
    alert('上传的文件不能超过2MB');
    return;
}

3.修改input标签的默认样式

input标签默认样式如果不是我们需要的效果,可以将其隐藏display:none,随后我们再创建一个按钮,在点击这个按钮时触发input的click方法;

<input type="file" class="upload_inp" accept=".png,.jpg,.jpeg">
<button class="select-file">选择文件</button>

...... 

select_file.addEventListener('click',function () {
    upload_inp.click();
});

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_18,color_FFFFFF,t_70,g_se,x_16

4.文件上传服务器

<button class="upload-to-server">上传至服务器</button>
......

upload_to_server = document.querySelector('.upload-to-server');

// 上传文件
upload_to_server.addEventListener('click',function () {
    if(!_file) {
        alert('请选择上传的文件');
        return;
    }
    let formData = new FormData();
    formData.append('file',_file);
    formData.append('filename',_file.name);
    axiosInstance.post('/xxx',formData  ).then( data => {
        if(data.code === 0) {
            alert('上传文件成功');
        }
        return new Promise.reject(data.codeText);  // 如果服务器返回错误信息,则依旧走catch
    }).catch( err => {
        alert('上传文件失败,请重试')
    }).finally(() => {
        clearFileList();
    }) 
})

基于base64文件上传

使用FileReader实例可以将图片读取转化为base64格式,注意读取转换的过程是异步的,一般使用Promise进行封装

function changeBase64 (file) {
    return new Promise((resolve) => {
        let fileReader = new FileReader();
        fileReader.readAsDataURL(file);
        fileReader.onload = function (event) {
            resolve(event.target.result)
        }
    }) 
}

转化成功触发FileReader实例的onload事件的异步回调,在事件对象event.target.result中获取base64编码转换结果;

1.发送base64图像数据

发送数据,发送数据的格式不再是formDate格式,而是application/x-www-form-urlencoded格式的数据;

在向服务器传输数据过程中为了防止base64格式的数据出现乱码,我们使用API encodeURIComponent 将其中的一些特殊字符编码,该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。

例如:字符串 "nihao?*/你好" 经过编码转换后是 nihao%3F*%2F%E4%BD%A0%E5%A5%BD

try {
    let data = await axiosInstance.post('/xxx',{
        file:encodeURIComponent(BASE64),
        filename:file.name
    },{
        headers:{
            'Content-Type':'application/x-www-form-urlencoded'
        }
    });

    if(data.code === 0) {
        alert('文件上传成功');
        return;
    }
    throw date.codeText;  // 如果还是存在异常则则抛出异常,代码会走catch语句  
} catch(e) {
    alert('文件上传失败')
} finally {
    chanegDisable(false);
}

注意点 tip1:两种处理服务器失败结果的方式 

.then方式

这种方式对于返回的Promise失败的结果会自动执行catch的回调,如果是Promise返回的是成功的状态,但是返回的结果中的字段告诉你是失败的,那么我们可以在then回调中返回一个失败的Promise让代码去执行catch回调,即return new Promise.reject(xxx),无论成功或失败都要执行的代码放在finally回调中进行。

async..await方式

如上代码所示,await只能获取Promise成功的结果,对于失败的结果,我们需要使用try...catch对目标代码进行捕获,这样返回失败的结果代码会自动执行catch中的语句,如果返回的是成功的Promise但服务器返回的字段内容告诉你结果失败,那么我们可以在try语句中throw一个异常,这样代码就会自动执行catch中的部分,同样,无论成功或者失败都要执行的代码我们可以放在finally代码块中进行。

2.客户端实现文件浏览

所谓文件缩略图浏览就是将文件转换为base64,复制给图片的src属性即可;

<div class="upload_abbre">
    <img src="" alt="">
</div>

......

img = upload_abbre.querySelector('img');

function changeBase64 (file) {
    return new Promise( resolve => {
        let fileReader = new FileReader();
        fileReader.readAsDataURL(file);
        fileReader.onload = function (event) {
            resolve(event.target.result);
        }
    })
}

upload_inp.addEventListener('change',async function () {
    let file = this.files[0];
    if(!file) return;
    
    ......


    let BASE64 = await changeBase64(file);
    upload_abbre.style.display = 'block';
    img.src = BASE64;
});

5b55538d3b634cd59ff25b9e7f9cc6f3.gif

3.文件hash名字的编译处理

上传的图片文件可能存在同名的情况,如果此时服务器不进行文件名的hash处理,为了保证服务器存放的图片不重复,那么相同文件名的不同图片就无法存放到服务器上。

我们使用sparkMD5插件进行文件编译,这个插件需要获取的是文件的buffer格式数据,对这个数据进行识别处理,最终根据文件内容的不同生成不同的文件hash名称。

首先需要将文件转换为buffer格式的数据,使用fileReader实例的readAsArrayBuffer方法,并在load事件回调中获取:

let fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = function (event) {
    let buffer = event.target.result;
}

转换后的buffer格式数据如下: 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_15,color_FFFFFF,t_70,g_se,x_16

 随后使用插件SparkMD5,生成文件的hash,代码如下

let buffer = event.target.result;  // 文件的buffer格式数据
let spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
let HASH = spark.end();

生成的文件hash如下,相同的文件生成的hash都是相同的 

6ca0e3c5b7c440c5846c1192ff99d8c0.png

还需获取文件的后缀名,获取的是正则exec方法的第一个分区内容,和hash值进行拼接返回最终生成的文件名

let suffix = /\.([a-zA-z0-9]+)$/.exec(file.name)[1];

最后我们将文件的hash,buffer,文件名,后缀名放在一个对象中返回,输出如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

我们将以上流程全部封装在changeBuffer 方法中,由于文件的buffer转换是异步的,所以该方法返回一个Promise实例,完整代码如下:

function changeBuffer (file) {
    return new Promise( resolve => {
        let fileReader = new FileReader();
        fileReader.readAsArrayBuffer(file);
        fileReader.onload = function (event) {
            let buffer = event.target.result;
            let spark = new SparkMD5.ArrayBuffer();
            
            spark.append(buffer);
            let HASH = spark.end();
            let suffix = /\.([a-zA-z0-9]+)$/.exec(file.name)[1];  // 获取后缀名
            resolve({
                buffer,
                HASH,
                suffix,
                filename:`${HASH}.${suffix}`
            });
        }
    })
}

最后将生成的文件名作为参数和文件数据一同上传至服务器: 

upload_inp.addEventListener('change',async function () {
    let file = this.files[0];
    if(!file) return;

    if(file.size > 2 * 1024 * 1024) {
        alert('图片不能超过2M');
        return;
    }

    select_file.classList.add('disable')

    let BASE64 = await changeBase64(file);  // 获取图片的base64编码,用于显示缩略图
    let { filename } = await changeBuffer(file); // 获取图片的hash名

    let formData = new FormData();
    formData.append('file',file);
    formData.append('filename',filename);
    
    axiosInstance.post('/xxx',formData).then(data => {
        if(+data.code === 0) {
            alert('文件上传成功');
            return;
        }

        return new Promise.reject(data.codeText);
    }).catch(err => {
        alert('文件上传失败');
    }).finally(() => {
        img.src = '';
        upload_abbre.style.display = 'none';
    });

    upload_abbre.style.display = 'block';
    img.src = BASE64;
    select_file.classList.remove('disable')
});

文件上传进度条显示

监听文件上传进度使用axios的onUploadProgress回调函数,可以在该回调函数的事件对象的中拿到当前文件上传的进度信息,该回调函数的底层监听的是原生XMLHttpRequest实例的upload属性中的onprogress事件;

在onUploadProgress回调函数的事件对象中,属性loaded记录着当前文件上传的进度(字节),属性total记录着需要上传文件的总进度即文件大小,而onUploadProgress回调在上传过程中随着进度的变化会持续触发,再利用这两者之间的比值动态修改进度条的width属性即可;

<div class="upload-progress">
    <div class="value"></div>
</div>

......

try {
    let formData = new FormData();
    formData.append('file', file);
    formData.append('filename', file.name);
    let data = await axiosInstance.post('/consumer/updateConsumerPic?id=5',formData,{
        onUploadProgress(event) {
            let { loaded,total } = event;
            upload_progress.style.display = 'block';
            value.style.width = `${loaded / total * 100}%`  // 修改进度条的长度
        }
    });
    if(+data.code === 1) {
        value.style.width = `100%`
        alert('文件上传成功');
        return;
    }

    throw data.codeText;

} catch {
    alert('文件上传失败');
} finally {
    select_file.classList.remove('hidden');
    loading3.classList.remove('show');
    upload_progress.style.display = 'none';
    value.style.width = `0%`
}

但是会存在如下效果,在进度条没有走到底时就会跳出成功的提示框,这是由于在进度条的css样式中添加了transition:width .3s的过渡效果,但此时使用了alert弹出框,它会阻塞页面渲染,所以在执行alert代码之前需要手动添加一个延时,让css过渡效果过渡完毕后再弹出,延时时长可以是css过度效果的时间即300ms ;

 6340175e300c4bfe8e1f3572103de0ca.gif

添加延时我们定义一个delay方法,它内部创建定时器并返回一个Promise,使用await调用delay方法,会阻塞代码执行300ms,让进度条的过渡效果显示完毕后再调用alert语句

function delay (interval) {
    typeof interval !== 'number' ? interval = 1000 : null;
    return new Promise(resolve => {
        setTimeout(() => {
            resolve()
        },interval);
    })
}

try {
    ......
    let data = await axiosInstance.post('/consumer/updateConsumerPic?id=5',formData,{
        onUploadProgress(event) {
           ......
        }
    });
    if(+data.code === 1) {
        value.style.width = `100%`
        await delay(300);
        alert('文件上传成功');
        return;
    }

    throw data.codeText;

} catch {
    alert('文件上传失败');
} finally {
    ......
}

这样进度条就可以完全显示到最后了 

fd9434d6129e4afeadd717db4996f7bf.gif

多文件上传

上传多文件需要给input标签添加multiple属性,这样选择文件时才能选择多个文件

<input type="file" class="upload_inp4" multiple>

1.获取上传文件信息

上传文件的信息同样是在input的dom元素的files属性中,它是一个类数组,不同之前的是,这个类数组的每一个元素就是上传的一个文件信息

upload_inp.addEventListener('change', async function () {
    _files = Array.from(this.files);
    if (_files.length === 0) return;
});

上传六个文件,_files打印的就是这六个文件对象组成的类数组 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

2.显示待上传文件/删除待上传文件

首先需要使用Array.from将files这个类数组转为数组,随后遍历这个数组,依次生成对对应的页面标签,将这些标签添加到upload_list的innerHTML中去;

删除待上传的文件分为两个层面,一个是要删除页面上的节点,第二是要将file数组中对应的文件对象给删除掉。我们使用的思想是事件的委托,给文件列表共同的父元素upload_list绑定单击事件,通过事件对象的tagName判断当前点击的元素是不是删除标签em,随后删除指定的元素;其次是删除文件对象,为了标识我们当前点击的标签和文件对象的映射关系,我们在遍历生成页面节点时需要给文件列表(每一个li)添加自定义属性key,用于关联页面节点和文件对象,从而在点击删除时通过getAttribute拿到对应的key值,使用filter将对应的key值的files对象删除掉。

// 删除上传列表
upload_list.addEventListener('click',function (event) {
    if(event.target.tagName === 'EM') {
        let currentLI = event.target.parentNode.parentNode;
        // 移除需要删除的列表元素
        upload_list.removeChild(currentLI);
        let currentKey = currentLI.getAttribute('key');
        // 过滤掉删除的文件
        _files = _files.filter(item => {
            return item.id !== currentKey
        });

        if(_files.length === 0) {
            upload_list.innerHTML = '<div class="tip4">只能上传PNG|JPG|JPEG格式的图片,且大小不超过2MB</div>'
        }

        console.log(_files);
    }
    
})
upload_inp.addEventListener('change', async function () {
    _files = Array.from(this.files);
    if (_files.length === 0) return;

    _files = _files.map(item => {
        return {
            file:item,
            filename:item.name,
            id:createRandom()
        }
    })

    let str = '';

    _files.forEach((item,index) => {
        console.log(item);
        str += `
            <li key="${item.id}">
                <span class="upload_filename4">文件${index + 1}:${item.filename}</span>
                <span><em class="remove4">移除</em></span>    
            </li>
        `
    });
    
    upload_list.innerHTML = str;
})

3.多文件上传服务器

多个文件上传效果如下:

726e49093fc34269a94e4108c94b1932.gif

多个文件是无法同时上传服务器的,需要做的就是使用map遍历_files文件数组,将要上传的文件一一上传,map的返回值由多个Promise组成的数组,使用Promise.all确保所有的文件成功上传

多文件上传使用了上传进度百分比的功能,效果如上,依旧是使用axios提供的onUploadProgress回调,随后根据自定义属性拿到对应的span标签,将当前上传进度放入对应的标签内

// 上传文件
upload_to_server.addEventListener('click',function () {
    console.log(_files)
    if(_files.length === 0){
        alert('请选择文件');
        return;
    }
    upload_to_server.classList.add('hidden');
    select_file.classList.add('disable');
    loading4.classList.add('show');

    let upload_list_Arr = Array.from(upload_list.querySelectorAll('li'));

    let filesResult = _files.map( file => {
        let fm = new FormData();
        fm.append('file',file.file);
        fm.append('filename',file.filename);

        // 查找目标span元素
        let currentLI = upload_list_Arr.find(item => item.getAttribute('key') === file.id);
        let currentSpan = currentLI ? currentLI.querySelector('span:nth-last-child(1)') : null;
        
        return axiosInstance.post('/consumer/updateConsumerPic?id=5',fm,{
            onUploadProgress(event) {
                // 结果保留两位小数
                currentSpan.innerHTML = `${(event.loaded / event.total * 100).toFixed(2)}%`;
            }
        }).then(data => {
            if(+data.code === 1) {
                if(currentSpan) {
                    currentSpan.innerHTML = '100%';
                }
                return;
            }
            return Promise.reject();
        });
    });

    Promise.all(filesResult).then(async res => {
        await delay(500);
        alert('文件上传成功');
    }).catch(() => {
        alert('文件上传失败');
    }).finally(() => {
        _files = null;
        upload_list.innerHTML = '<div class="tip4">只能上传PNG|JPG|JPEG格式的图片,且大小不超过2MB</div>';
        select_file.classList.remove('disable');
        upload_to_server.classList.remove('hidden')
        loading4.classList.remove('show');
    })
})

文件拖拽上传 

关于js的拖拽事件

dragenter:拖拽进入指定的容器时触发;

dragleave:拖拽离开指定容器时候触发;

dragover:在指定容器内拖拽移动时触发;

drop:松开鼠标,将内容放置到指定容器时触发;

注意:无论将指定文件拖拽到页面的哪个容器中,只要在浏览器内松开鼠标,就会将该文件以新窗口的形式打开,这是浏览器的默认行为

当我们同时使用了dragover事件和drop事件时,我们会发现drop事件并没有触发。根据MDN的文档我们必须要阻止某一DOM元素对dragover的默认行为,才能使drop事件在其上正确执行

我们需要在dragover,drop事件中阻止浏览器的默认行为;

可以通过drop的事件对象拿到拖拽文件的信息,它存放在事件对象event.dataTransfer.files[0]

<section class="drag_upload">
    <input type="file" class="upload_inp5">
    <div class="upload-wrapper">
        <div class="iconfont icon-upload"></div>
        <div class="upload_tip">将文件拖拽到此处或<em class="upload_em">点击上传</em></div>
    </div>
    <div class="mask">
        <div class="mask_tip">正在上传中,请稍后...</div>
    <div>
</section>

let drag_upload = document.querySelector('.drag_upload');

drag_upload.addEventListener('dragover',function (event) {
    event.preventDefault();
});

drag_upload.addEventListener('drop',function (event) {
    event.preventDefault();
    let file = event.dataTransfer.files[0];
    if(!file) return;
})

drop事件的事件对象打印如下: 

 watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

 获取的拖拽文件信息输出如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

获取到了上传的文件对象,之后的上传操作就和之前的差不多了,我们可以封装一个方法uploadFile用于上传文件,当触发拖拽的drop事件和上传按钮的点击事件时调用这个方法;

// 上传文件
async function uploadFile (file) {
    mask.style.display = 'flex';

    try {
        let formData = new FormData();
        formData.append('file',file);
        formData.append('filename',file.name);

        console.log(formData);

        let data = await axiosInstance.post('/consumer/updateConsumerPic?id=5',formData);

        if(+data.code === 1) {
            alert('文件上传成功');
            return;
        }

        throw data.msg;

    } catch(e) {
        alert('文件上传失败',e);
    } finally {
        mask.style.display = 'none';
    }
}

// 拖拽上传
drag_upload.addEventListener('dragover',function (event) {
    event.preventDefault();
    console.log('dragover');
});

drag_upload.addEventListener('drop',function (event) {
    event.preventDefault();
    console.log('drop');
    let file = event.dataTransfer.files[0];
    console.log(event.dataTransfer.files[0]);
    if(!file) return;

    uploadFile(file);
})

// 点击按钮上传
upload_em.addEventListener('click',function () {
    upload_inp5.click();

})

upload_inp5.addEventListener('change',function () {
    let file = upload_inp5.files[0];
    if(!file) return;
    uploadFile(file);
})

 最终的效果如下:

fe5c664902cd486aa67f1aa1df93cc55.gif

大文件切片上传和断点续传

对于文件体积大的文件,同样可以使用上述方式,但是如果文件上传失败,则需要从头开始再次上传,会消耗大量的资源和时间。解决大文件上传常用的方式有切片上传和断点续传。

切片上传需要在客户端将上传的大文件切分成若干部分,将这些部分分开传递,当所有切片都传递成功后,客户端再次发送一个请求,服务器就将之前上传的这些切片合并成一个文件存放;切片上传一般可以配合断点续传,如果上传文件失败,那么服务器可以返回一些信息,告诉客户端当前已经上传的切片数,那么客户端只需要上传之后的切片即可,断点续传的实现方案有很多。

1.获取文件已经上传的切片信息

前后端都使用文件的hash名来标识一个文件,文件hash名如何生成上面已经介绍了,在每次上传文件之前,我们需要获取该文件的hash值,将其发送给服务器,获取该文件已经上传的切片信息,没有上传already数组则为空。

let already = [],  // 存放已上传切片的信息
    data = null,
    { HASH,suffix } = await changeBuffer(file); // 获取文件唯一hash值

// 获取已经上传的切片信息
try {
    data = await axiosInstance.get('/upload_already',{
        params:{ HASH }
    });

    if(data.code === 0) {
        already = data.fileList;
    }

} catch(err) {}

2.文件的切片处理

文件切片有两种方式,即固定数量/固定大小,为了使用一种折中的方案,我们先使用固定切片的大小,如果获得的切片数量过多,那么再使用固定数量的方式

let max = 100 * 1024, // 1KB 切片的固定大小
    count = Math.ceil(file.size / max), // 一共要有多少切片
    index = 0, // 切割的索引
    chunks = [];  // 存放切割的文件

// 若固定大小导致切面数量过多,则使用固定数量
if(count > 100) {
    max = file.size / 100;
    count = 100;
}

文件切片使用slice方法,它位于文件对象file的原型链上 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

使用file.slice('开始切片的字节','结束切片的字节')切片,将切完的文件放入chunks数组中保存

// 切片
while(index < count) {
    chunks.push({
        file:file.slice(index * max,(index + 1) * max),
        filename:`${HASH}_${index + 1}.${suffix}`
    });
    index++;
}

切片完成后的chunks数组

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA546W5Ly0Xw==,size_20,color_FFFFFF,t_70,g_se,x_16

3.上传切片

上传切片需要执行以下几步操作,首选遍历之前生成的切片文件数组chunks,将这写切片一一上传服务器,但在上传之前需要根据之前获得的already数组判读当前的切片是否已经上传,否则不用上传;

// 把每个切片上传服务器
chunks.forEach(chunk => {
    // 已经上传的切片无需再传
    if(already.length && already.includes(chunk.filename)) {
        complete();
        return;
    }

    let fm = new FormData();
    fm.append('file'.chunk.file);
    fm.append('filename',chunk.filename);
    axiosInstance.post('/upload_chunk',fm).then(data => {
        if(+data.code === 1) {
            complete();
            return;
        }
        return Promise.reject();
    }).catch(() => {
        alert('当前切片上传失败');
        clear();
    })
})

4.通知合并切片

每一个切片上传成功都会调用complete方法,修改当前上传进度条,使用全局index变量记录上传进度,当所有切片都成功上传,即 index = count ,再向服务器发送合并切片请求,服务器就将之前上传的所有切片合并成一个文件,文件上传成功。

index = 0;
function clear () {
    select_file.classList.remove('hidden');
    loading3.classList.remove('show');
    upload_progress.style.width = '0%';
}

async function complete () {
    // 进度条管控
    index++;
    upload_progress.style.width = `${ index / count * 100 }%`
    // 全部上传成功,合并切片
    if(index < count) return;
    upload_progress.style.width = '100%';

    try{
        data = await axiosInstance.post('/upload_merge',{
            HASH,count
        },{
            headers:{
                'Content-Type':'application/x-www-form-urlencoded'
            }
        });

        if(+data.code === 0) {
            alert('文件上传成功');
            clear()
            return;
        }

        throw data.codeText;
    } catch (err) {
        alert('合并切片失败');
        clear();
    }
} 

这样客户端切片上传的功能就实现了。如果当前文件之前上传中断过,当再次上传,就会发现进度条开始会有一个突进的过程,这就是由于开始的这部分切片之前已经成功上传了,便从之前未上传的切片位置继续上传,效果如下:

bb56ccc5c7714feda716e4552b1cc0bd.gif

 切片完整代码:

// 大文件上传
;(() => {
    let upload_inp = document.querySelector('.upload_inp6'),
        select_file = document.querySelector('.select-file6'),
        upload_list = document.querySelector('.upload_list3'),
        loading3 = document.querySelector('.loading6'),
        upload_progress = document.querySelector('.upload-progress6'),
        value = upload_progress.querySelector('.value6');

    // 生成文件唯一hash名
    function changeBuffer(file) {
        return new Promise(resolve => {
            let fileReader = new FileReader();
            fileReader.readAsArrayBuffer(file);
            fileReader.onload = function (event) {
                let buffer = event.target.result;
                let spark = new SparkMD5.ArrayBuffer()

                spark.append(buffer);
                let HASH = spark.end();
                let suffix = /\.([a-zA-z0-9]+)$/.exec(file.name)[1]; // 获取后缀名
                resolve({
                    buffer,
                    HASH,
                    suffix,
                    filename: `${HASH}.${suffix}`
                });
            }
        })
    }

    select_file.addEventListener('click', function () {
        if (select_file.classList.contains('disable')) return;
        upload_inp.click();
    });

    upload_inp.addEventListener('change', async function () {
        let file = this.files[0];
        console.log(file);
        if (!file) return;

        select_file.classList.add('hidden');
        loading3.classList.add('show');

        let already = [],
            data = null,
            { HASH,suffix } = await changeBuffer(file); // 获取文件唯一hash值

        // 获取已经上传的切片信息
        try {
            data = await axiosInstance.get('/upload_already',{
                params:{ HASH }
            });

            if(data.code === 0) {
                already = data.fileList;
            }

        } catch(err) {}

        // 文件的切片处理 固定数量/固定大小
        let max = 100 * 1024, // 1KB 切片的固定大小
            count = Math.ceil(file.size / max), // 一共要有多少切片
            index = 0, // 切割的索引
            chunks = [];  // 存放切割的文件

        // 若固定大小导致切面数量过多,则使用固定数量
        if(count > 100) {
            max = file.size / 100;
            count = 100;
        }
        // 切片
        while(index < count) {
            chunks.push({
                file:file.slice(index * max,(index + 1) * max),
                filename:`${HASH}_${index + 1}.${suffix}`
            });
            index++;
        }

        // 上传后的处理
        index = 0;
        function clear () {
            select_file.classList.remove('hidden');
            loading3.classList.remove('show');
            upload_progress.style.width = '0%';
        }

        async function complete () {
            // 进度条管控
            index++;
            upload_progress.style.width = `${ index / count * 100 }%`
            // 全部上传成功,合并切片
            if(index < count) return;
            upload_progress.style.width = '100%';

            try{
                data = await axiosInstance.post('/upload_merge',{
                    HASH,count
                },{
                    headers:{
                        'Content-Type':'application/x-www-form-urlencoded'
                    }
                });

                if(+data.code === 0) {
                    alert('文件上传成功');
                    clear()
                    return;
                }

                throw data.codeText;
            } catch (err) {
                alert('合并切片失败');
                clear();
            }
        } 

        // 把每个切片上传服务器
        chunks.forEach(chunk => {
            // 已经上传的切片无需再传
            if(already.length && already.includes(chunk.filename)) {
                complete();
                return;
            }

            let fm = new FormData();
            fm.append('file'.chunk.file);
            fm.append('filename',chunk.filename);
            axiosInstance.post('/upload_chunk',fm).then(data => {
                if(+data.code === 1) {
                    complete();
                    return;
                }
                return Promise.reject();
            }).catch(() => {
                alert('当前切片上传失败');
                clear();
            })
        })
    })
})()

猜你喜欢

转载自blog.csdn.net/weixin_43655896/article/details/124186241