用户上传文件的时候,如果文件过大,那么上传可能就会很耗时。而且一旦上传的过程中发生了网络中断,那上传就前功尽弃了。为了提高用户的体验,我们可以选择断点续传,也就是把文件切分成小块后,挨个上传。这样即使中间上传中断,但下次再上传时,只上传缺失的那些部分就可以了。可以看到,断点上传虽然在性能上,会造成网络请求变多的问题,但也极大地提高了用户上传的体验。
1、基于文件流(form-data) element-ui上传组件默认是基于文件流的
2、客户端把文件转化为BASE64,再传给后台
用element-ui提供基于文件流的上传方案:
<template>
<div id="app">
<!--
action:存放的是文件上传到服务器的接口地址
-->
<el-upload
drag
action="/single1"
:show-file-list="false"
:on-success="handleSuccess"
:before-upload="beforeUpload"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
</el-upload>
<!-- IMG -->
<div class="uploadImg" v-if="img">
<img :src="img" alt />
</div>
</div>
</template>
<script>
/*
* 默认上传
* 格式:multipart/form-data
* 数据格式:form-data
* file 文件流信息
* filename 文件名字
* 上传成功后获取服务器返回信息,通知on-success回调函数执行
* 内部封装了ajax
*/
export default {
name: "App",
data() {
return {
img: null,
};
},
methods: {
handleSuccess(result) {
if (result.code == 0) {
this.img = result.path;
}
},
beforeUpload(file) {
// 格式校验
let { type, size } = file;
if (!/(png|gif|jpeg|jpg)/i.test(type)) {
this.$message("文件合适不正确~~");
return false;
}
if (size > 200 * 1024 * 1024) {
this.$message("文件过大,请上传小于200MB的文件~~");
return false;
}
return true;
},
},
};
</script>
<template>
<div id="app">
<el-upload drag action :auto-upload="false" :show-file-list="false" :on-change="changeFile">
<i class="el-icon-upload"></i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传</em>
</div>
</el-upload>
<!-- IMG -->
<div class="uploadImg" v-show="img">
<img :src="img" alt />
</div>
</div>
</template>
<script>
import { fileParse } from "./assets/utils";
import axios from "axios";
import qs from "qs";
export default {
name: "App",
data() {
return {
img: null,
};
},
methods: {
async changeFile(file) {
if (!file) return;
file = file.raw;
// 继续做格式校验
/*
* 把上传的文件先进行解析(FileReader)
* 把其转换base64编码格式
* 自己基于axios把信息传递给服务器
* ...
*/
let result = await fileParse(file, "base64");
result = await axios.post(
"/single2",
qs.stringify({
chunk: encodeURIComponent(result),
filename: file.name,
}),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
result = result.data;
if (result.code == 0) {
this.img = result.path;
}
},
},
};
</script>
1、拿到文件,对文件进行切片,有两个方式,一种时固定数量,另一种时固定大小。
2、用SparkMD5库对每一个分片进行命名(服务器接口后,会对相同hash的文件进行合并)
3、发请求传文件,可以有串行和并行两种方式。这里使用串行,一个一个发,方便点击暂停上传的时候取消发送。
4、可以拿一个数组保存待发的文件,上传成功的文件可以从数组里面删除。这样,当再次点击继续发送的时候,就不需要重复发送了。
5、等全部文件发完了,再发一个请求告诉服务器文件发完了
dom部分:
<div id="app">
<el-upload drag action :auto-upload="false" :show-file-list="false" :on-change="changeFile">
<i class="el-icon-upload">i>
<div class="el-upload__text">
将文件拖到此处,或
<em>点击上传em>
div>
el-upload>
<div class="progress">
<span>上传进度:{{total|totalText}}%span>
<el-link type="primary" v-if="total>0 && total<100" @click="handleBtn">{{btn|btnText}}el-link>
div>
<div class="uploadImg" v-if="video">
<video :src="video" controls />
div>
div>
文件上传时调用的方法:
async changeFile(file) {
if (!file) return;
console.log('file', file)
file = file.raw;
// 解析成buffer数据
// 切片处理,把文件切成多个部分(固定数量/固定大小)
// 每一个切片都有自己的部分数据和自己的名字
let buffer = await fileParse(file, "buffer"),
spark = new SparkMD5.ArrayBuffer(),
suffix;
spark.append(buffer);
let hash = spark.end();
suffix = /\.([0-9a-zA-Z]+)$/i.exec(file.name)[1];
console.log('suffix', suffix)
// 创建100个切片
let partList = [],
partsize = file.size / 100,
cur = 0;
for(let i = 0; i < 100; i++) {
let item = {
chunk: file.slice(cur, cur + partsize),
filename: `${hash}_${i}.${suffix}`,
};
cur += partsize;
partList.push(item)
}
this.partList = partList;
this.hash = hash;
this.sendRequest();
},
发送请求的方法:
async sendRequest() {
// 根据100个切片创建100个请求(集合)
let requestList = [];
this.partList.forEach((item, index) => {
// 每一个函数都是发送一个切片的请求
let fn = () => {
let formData = new FormData();
formData.append("chunk", item.chunk);
formData.append("filename", item.filename);
return axios.post('/single3', formData, {
headers: { "Content-Type": "multipart/form-data" },
}).then(res => {
res = res.data;
if (res.code == 0) {
this.total += 1;
this.partList.splice(index, 1);
}
})
}
requestList.push(fn);
})
let complete = async () => {
let result = await axios.get("/merge", {
params: {
hash: this.hash,
},
});
result = result.data;
if (result.code == 0) {
this.video = result.path;
}
};
let i = 0;
let send = async() => {
// 都发完了
if (this.abort) return;
if (i >= requestList.length) {
complete();
return;
}
await requestList[i]();
i++;
send();
}
send();
},
处理切换按钮的逻辑:
handleBtn() {
if (this.btn) {
// 断点续传
this.btn = false;
this.abort = false;
this.sendRequest();
return;
}
// 暂停上传
this.btn = true;
this.abort = true;
}
虽然文件断点续传的功能要浪费额外的性能,造成网络请求变多的问题,但是这提高了用户体验。
视频资源
项目代码