目录
大文件切片上传和断点续传
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属性,它是一个类数组集合,输出内容如下:
可以通过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();
});
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;
});
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格式数据如下:
随后使用插件SparkMD5,生成文件的hash,代码如下
let buffer = event.target.result; // 文件的buffer格式数据
let spark = new SparkMD5.ArrayBuffer();
spark.append(buffer);
let HASH = spark.end();
生成的文件hash如下,相同的文件生成的hash都是相同的
还需获取文件的后缀名,获取的是正则exec方法的第一个分区内容,和hash值进行拼接返回最终生成的文件名
let suffix = /\.([a-zA-z0-9]+)$/.exec(file.name)[1];
最后我们将文件的hash,buffer,文件名,后缀名放在一个对象中返回,输出如下:
我们将以上流程全部封装在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 ;
添加延时我们定义一个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 {
......
}
这样进度条就可以完全显示到最后了
多文件上传
上传多文件需要给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打印的就是这六个文件对象组成的类数组
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.多文件上传服务器
多个文件上传效果如下:
多个文件是无法同时上传服务器的,需要做的就是使用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事件的事件对象打印如下:
获取的拖拽文件信息输出如下:
获取到了上传的文件对象,之后的上传操作就和之前的差不多了,我们可以封装一个方法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);
})
最终的效果如下:
大文件切片上传和断点续传
对于文件体积大的文件,同样可以使用上述方式,但是如果文件上传失败,则需要从头开始再次上传,会消耗大量的资源和时间。解决大文件上传常用的方式有切片上传和断点续传。
切片上传需要在客户端将上传的大文件切分成若干部分,将这些部分分开传递,当所有切片都传递成功后,客户端再次发送一个请求,服务器就将之前上传的这些切片合并成一个文件存放;切片上传一般可以配合断点续传,如果上传文件失败,那么服务器可以返回一些信息,告诉客户端当前已经上传的切片数,那么客户端只需要上传之后的切片即可,断点续传的实现方案有很多。
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的原型链上
使用file.slice('开始切片的字节','结束切片的字节')切片,将切完的文件放入chunks数组中保存
// 切片
while(index < count) {
chunks.push({
file:file.slice(index * max,(index + 1) * max),
filename:`${HASH}_${index + 1}.${suffix}`
});
index++;
}
切片完成后的chunks数组
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();
}
}
这样客户端切片上传的功能就实现了。如果当前文件之前上传中断过,当再次上传,就会发现进度条开始会有一个突进的过程,这就是由于开始的这部分切片之前已经成功上传了,便从之前未上传的切片位置继续上传,效果如下:
切片完整代码:
// 大文件上传
;(() => {
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();
})
})
})
})()