最近用vue+typescript搭的一个框架写项目,UI框架使用的是ant design for vue的,由于其中还用到了“vue-property-decorator” 和 “babel-plugin-jsx-v-model”等依赖,用以支持TS和JSX的写法,所以不熟悉的可能看起来比较懵。当然,代码形式不一样,思想是一样的,基础JS代码怎么说也是看得懂的,那就足够了。
先总结一下分片的思路吧:
1.先根据一定大小计算要把文件分成几块,利用FileReader对象读取上传的文件并分段截取成字节流数组;
2.若需要进行断点续传,则在截取完毕后还要同时根据文件的完整字节流生成对应的MD5值,用于对分片上传的文件进行标记归类,和查询当前片是否已传给后台(断点续传:已上传的文件片无需重复上传);
3.循环执行(检查当前片是否已上传、上传当前片)这些操作,直到全部片上传完毕;
4.向后台发送合并请求接口,由后台完成合并操作。
后端人员的代码是参考 Spring Boot[五]:WebUploader分片断点上传 这个写的,据他说有两种方式,一种是这个,生成临时文件最后合并,一种是在实体中拼接文件流(记不清有没有在最后的参考文章中记录了)。
现在直接上代码吧
1.JSX中引入antd的upload组件
<a-upload
name="file"
accept={this.videoUploadData.acceptType}
multiple={false}
action={this.uploadUrl}
headers={this.headers}
data={this.uploadData}
fileList={this.fileList}
beforeUpload={this.handleBeforeUpload}
customRequest={this.handleUpload}
remove={this.handleRemove}
on-reject={this.handleReject}
on-preview={this.handlePreview}
on-change={this.handleChange}
>
{this.fileList.length >= this.videoUploadData.num ? (
''
) : (
<div>
<a-button style={this.style.btu}>
{' '}
<a-icon type="plus" style={this.style.icon} />
</a-button>
</div>
)}
</a-upload>
2.选择文件后,会先走beforeUpload方法,我们有初始化内容的可以写在这里
/**
* @description 上传前
* @author YXM
*/
handleBeforeUpload(file: any, fileList: any) {
this.successChunk = 0 // 重置当前已上传成功的片数
this.chunkList = [] // 清空文件流数组
// this.$AntMessage.loading({ duration: 0, content: '文件上传中...' })
}
3.用自写方法代替原组件的上传方法,
/**
* @description 手动请求上传服务
* @author YXM
*/
handleUpload(param: any) {
const file = param.file // 组件提供的文件
this.computeMD5(file).then((md5:any)=>{
if (this.chunkList.length > 0) { // 判断字节流数组长度
for(let i = 0,len = this.chunkList.length;i<len;i++) {
// 这里请求checkFile,发送MD5返回是否已上传该片段,只不过我没写
let formData = new FormData() // 按分片个数发送请求
formData.append('file', this.chunkList[i])
formData.append('md5File', md5)
formData.append('chunk', i.toString()) // 属于第几片
this.$Api
._postMultiData({
url: this.$Api.apiModulesList.videoUpload.upload.url,
method: 'post',
headers: {
token: Cookies.get('token'),
'Content-Type': 'multipart/form-data; boundary=ABCD',
},
data: formData,
})
.then((res: any) => {
if(res && res.status){
this.successChunk++ // 记录当前已上传成功的片数
} else {
// this.$AntMessage.warning('上传异常')
}
})
}
}
})
}
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
computeMD5(file: any) {
const _this = this
this.filename = file.name
return new Promise((resolve, reject)=>{
try {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice = File.prototype.slice
// let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
let currentChunk = 0; // 当前第几片
const chunkSize = 5 * 1024 * 1024; // 每片的大小,这里是5M
let chunks = Math.ceil(file.size / chunkSize); // 总片数
let spark = new SparkMD5.ArrayBuffer();
loadNext();
fileReader.onload = ((e: any) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
this.md5 = spark.end();
resolve(this.md5)
console.log(`MD5计算完毕:${file.name} \nMD5:${this.md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
});
fileReader.onerror = function () {
// this.error(`文件${file.name}读取出错,请检查该文件`)
// file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
_this.chunkList.push(blobSlice.call(file, start, end)) // 将每次分片的字节流放到数组里
}
}catch(e) {
reject(e)
}
})
}
4.监听 记录已成功的接口次数successChunk 数据,如果和字节流数组长度相同,则表明所有的接口都执行成功了,则发送合并请求。
@Watch('successChunk')
watchSuccessChunk(val: any) {
if (val == this.chunkList.length) {
this.sendMerge()
}
}
sendMerge(){
this.successChunk = 0 // 重置
this.$ModuleApis.videoUpload
.merge({
data: {
baseId: this.uploadData.baseId,
chunks: this.chunkList.length,
md5File: this.md5,
name: this.filename
},
})
.then((res: any) => {
if (res.code === window.CROSS_CODE) { // 返回200
this.$AntMessage.success('上传成功')
this.handleUploadSuccess()
}
})
}
/**
* @description 上传成功之后告诉父组件
* @author YXM
*/
@Emit('emitUploadSuccess')
handleUploadSuccess() {}
这样,整个前端的任务就完成了。
实际代码有删改(自己加了进度条等),主要讲思路,各位根据自身需要添加,这里不再赘述。下面上全部代码
import { Component, Vue, Prop, Emit, Watch } from 'vue-property-decorator'
import Cookies from 'js-cookie'
import SparkMD5 from 'spark-md5'
import './uploadVideo.scss' // css样式
@Component({
name: 'uploadFilesComponents',
})
export default class uploadFilesComponents extends Vue {
headers: any = {
token: Cookies.get('token'),
}
fileList: any = []
@Prop({ default: () => {} }) private videoUploadData!: any // 上传文件的众多参数
@Prop({ default: '' }) private uploadUrl!: String // 上传的路径
@Prop({ default: () => {} }) private uploadData?: any // 上传的额外参数
chunkList:any = [] // 存放字节流数组
filename:any = null // 文件名称
successChunk:any = 0 // 已上传成功片数
md5: any = null //加密值
mounted() {}
@Watch('successChunk')
watchSuccessChunk(val: any) {
if (val == this.chunkList.length) {
this.sendMerge()
}
}
render() {
return (
<div style={this.style.box}>
<a-upload
name="file"
accept={this.videoUploadData.acceptType}
multiple={false}
action={this.uploadUrl}
headers={this.headers}
data={this.uploadData}
fileList={this.fileList}
beforeUpload={this.handleBeforeUpload}
customRequest={this.handleUpload}
remove={this.handleRemove}
on-reject={this.handleReject}
on-preview={this.handlePreview}
on-change={this.handleChange}
>
{this.fileList.length >= this.videoUploadData.num ? (
''
) : (
<div>
<a-button style={this.style.btu}>
{' '}
<a-icon type="plus" style={this.style.icon} />
</a-button>
</div>
)}
</a-upload>
</div>
)
}
/**
* @description 上传前
* @author YXM
*/
handleBeforeUpload(file: any, fileList: any) {
this.successChunk = 0
this.chunkList = []
// this.$AntMessage.loading({ duration: 0, content: '文件上传中...' })
}
/**
* @description 手动请求上传服务
* @author YXM
*/
handleUpload(param: any) {
const file = param.file
this.computeMD5(file).then((md5:any)=>{
if (this.chunkList.length > 0) {
for(let i = 0,len = this.chunkList.length;i<len;i++) {
// 这里请求checkFile,发送MD5返回是否已上传该片段,只不过我没写
let formData = new FormData()
formData.append('file', this.chunkList[i])
formData.append('md5File', md5)
formData.append('chunk', i.toString())
this.$Api
._postMultiData({
url: this.$Api.apiModulesList.videoUpload.upload.url,
method: 'post',
headers: {
token: Cookies.get('token'),
'Content-Type': 'multipart/form-data; boundary=ABCD',
},
data: formData,
})
.then((res: any) => {
if(res && res.status){
this.successChunk++
} else {
this.$AntMessage.warning('上传异常')
}
})
}
}
})
}
sendMerge(){
this.successChunk = 0
this.$ModuleApis.videoUpload
.merge({
data: {
baseId: this.uploadData.baseId,
chunks: this.chunkList.length,
md5File: this.md5,
name: this.filename
},
})
.then((res: any) => {
if (res.code === window.CROSS_CODE) {
this.$AntMessage.success('上传成功')
this.handleUploadSuccess()
}
})
}
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
computeMD5(file: any) {
const _this = this
this.filename = file.name
return new Promise((resolve, reject)=>{
try {
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice = File.prototype.slice
let currentChunk = 0;
const chunkSize = 5 * 1024 * 1024;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
loadNext();
fileReader.onload = ((e: any) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
this.md5 = spark.end();
resolve(this.md5)
console.log(`MD5计算完毕:${file.name} \nMD5:${this.md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
});
fileReader.onerror = function () {
// this.error(`文件${file.name}读取出错,请检查该文件`)
// file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
_this.chunkList.push(blobSlice.call(file, start, end))
}
}catch(e) {
reject(e)
}
})
}
/**
* @description 删除
* @author YXM
*/
handleRemove(file: any, fileList: any) {
this.fileList.splice(this.fileList.indexOf(file), 1)
}
/**
* @description 每次上传时,都会触发这个方法
* @author YXM
*/
handleChange(info: any) {
this.$AntMessage.destroy()
if (info.file.response) {
this.$AntMessage.success('上传成功')
this.handleUploadSuccess()
}
}
/**
* @description 拖拽文件不符合 accept 类型时的回调
* @author YXM
*/
handleReject(fileList: any) {
this.$AntMessage.warning(`请选择 ${this.videoUploadData.acceptType} 格式的文件执行上传操作`)
}
/**
* @description 上传成功之后告诉父组件
* @author YXM
*/
@Emit('emitUploadSuccess')
handleUploadSuccess() {}
/**
* @description css样式代码
* @author YXM
*/
style: any = {
box: {
// 上传的盒子
// width: '100%',
// textAlign: 'center',
},
btu: {
padding: '30px 35px 70px 35px',
},
icon: {
fontSize: '40px',
},
modal: {
// modal对话框样式
modal: {
maxWidth: '30%',
maxHidth: '80%',
},
video: {
width: '100%',
},
},
}
}
注意:
1.分片的大小可能会对字节流上传有影响,表现为我设置分片大小为10M,上传了11M的文件,按分片分别得到10M、1M的字节流,上传过程中控制台显示接口并没有接收到10M的字节流,并无深究此问题,改成5M就能够上传了,因此在这里备注下;
2.有问题再补充吧。