使用该技术解决的需求场景:
大文件上传响应超时。
解决方案:
将大文件切片,并计算 md5 传给后台,后台根据 md5 合并文件。
注意:
options 参数中 testChunks: false, // 默认true, 是否开启服务器分片校验
默认为 true,因为只做分片,不做断点续传,该配置需要为 false
参考资料:
https://github.com/simple-uploader/vue-uploader vue-simple-uploader 的 github 地址
https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md vue-simple-uploader 的 github 地址(中文文档)
https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md simple-uploader.js 的 配置
https://www.cnblogs.com/xiahj/p/vue-simple-uploader.html 其实是参考了这篇的使用。这篇文章讲了断点续传,而我将该功能去掉了,只使用了分片
<template>
<div>
<el-button type="primary" @click="uploadFile" >导入</el-button>
<!-- 上传 -->
<global-uploader ref="uploader" :tenantId="tenantId" :target="BIG_UPLOAD_TARGET" ></global-uploader>
</div>
</template>
<script>
import globalUploader from '@/components/common/globalUploader.vue'
components: {
globalUploader
},
data() {
return {
BIG_UPLOAD_TARGET: `${process.env.BASE_URL}/school/teacher/face/insertTeacherFaceByZip`,
}
}
</script>
<!-- globalUploader.vue -->
<template>
<div class="uploader_box">
<div class="mask_box" v-show="panelShow"></div>
<div id="global-uploader">
<!-- 上传 -->
<uploader
ref="uploader"
:autoStart="false"
:options="options"
:file-status-text="statusText"
@file-added="onFileAdded"
@file-success="onFileSuccess"
@file-progress="onFileProgress"
@file-error="onFileError"
class="uploader-app">
<uploader-unsupport></uploader-unsupport>
<uploader-btn ref="uploadBtn" :single="true" v-show="false" :attrs="attrs" >选择文件</uploader-btn>
<uploader-list v-show="panelShow">
<div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
<div class="file-title">
<h2>文件列表</h2>
</div>
<ul class="file-list">
<li v-for="file in props.fileList" :key="file.id">
<uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file>
</li>
<div class="no-file" v-if="!props.fileList.length"><i class="iconfont icon-empty-file"></i> 暂无待上传文件</div>
</ul>
</div>
</uploader-list>
</uploader>
</div>
</div>
</template>
<script>
/**
* 全局上传插件
*/
import Vue from 'vue'
import uploader from 'vue-simple-uploader'
Vue.use(uploader)
import SparkMD5 from 'spark-md5';
const CHUNKSIZE = 1* 1024 * 1024;
export default {
props: {
tenantId: String,
target: String,
},
data() {
return {
options: {
target: this.target,
testChunks: false, // 默认true, 是否开启服务器分片校验, (为 true 时, 第一次请求该分片时不传 file 对象(此行为需后台配置处理,否则出错))
testMethod: 'POST',
// initialPaused: true, // 初始文件 paused 状态,默认 false。
chunkSize: CHUNKSIZE, // 分块时按照该值来分。最后一个上传块的大小是可能是大于等于1倍的这个值但是小于两倍的这个值大小,可见这个 Issue #51,默认 1*1024*1024。
forceChunkSize: true, // 是否强制所有的块都是小于等于 chunkSize 的值。默认是 false。
// fileParameterName: 'file',
maxChunkRetries: 3, // 最大自动失败重试上传次数,值可以是任意正整数,如果是 undefined 则代表无限次,默认 0。
// 服务器分片校验函数,秒传及断点续传基础
// checkChunkUploadedByResponse: function (chunk, message) { // 根据 XHR 响应内容检测每个块是否上传成功了
// console.log('checkChunkUploadedByResponse', chunk, message);
// let objMessage = JSON.parse(message);
// return objMessage.data
// },
// preprocess: function (chunk) { // 每个块在测试以及上传前会被调用,参数就是当前上传块实例 Uploader.Chunk,注意在这个函数中你需要调用当前上传块实例的 preprocessFinished 方法,默认 null
// console.log('preprocess', chunk);
// chunk.preprocessFinished()
// },
headers: {
Authorization: JSON.parse(localStorage.getItem("MemberInfo")).Authorization,
},
// query: {
// tenantId: this.tenantId
// }
// processParams: function (params) {}, // 处理请求参数,一般用于修改参数名字或者删除参数。0.5.2版本后,processParams 会有更多参数:(params, Uploader.File, Uploader.Chunk, isTest)。
},
statusText: {
success: '成功',
error: '出错了',
uploading: '上传中',
paused: '暂停中',
waiting: '等待中'
},
attrs: {
accept: ['.zip', '.rar']
},
panelShow: false, //选择文件后,展示上传panel
collapse: false,
}
},
mounted() {
},
computed: {
//Uploader实例
uploader() {
return this.$refs.uploader.uploader;
}
},
methods: {
openUploader(){
this.$refs.uploadBtn.$el.click()
},
onFileAdded(file) {
this.panelShow = true;
this.computeMD5(file);
},
onFileProgress(rootFile, file, chunk) {
console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
},
onFileSuccess(rootFile, file, response, chunk) {
console.log('onFileSuccess',rootFile, file, response, chunk);
if(!response) return // 成功时没有返回值
let res = JSON.parse(response);
console.log('code', res.code);
// 服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
if (res.code!=200) { // 失败
this.$message({ message: res.message, type: 'error' });
return
}else{
this.$message({ message: res.message, type: 'success' });
this.panelShow = false;
}
// 如果服务端返回需要合并
// if (res.needMerge) {
// 文件状态设为“合并中”
// this.statusSet(file.id, 'merging');
// api.mergeSimpleUpload({
// tempName: res.tempName,
// fileName: file.name,
// ...this.params,
// }).then(res => {
// // 文件合并成功
// }).catch(e => {});
// 不需要合并
// } else {
// Bus.$emit('fileSuccess');
// console.log('上传成功');
// }
},
onFileError(rootFile, file, response, chunk) {
console.log('onFileError', rootFile, file, response, chunk);
this.$message({
message: response,
type: 'error'
})
},
/**
* 计算md5,实现断点续传及秒传
* @param file
*/
computeMD5(file) {
console.log('file', file);
console.log('getRoot', file.getRoot());
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = CHUNKSIZE // 10 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize); // 总块数
let spark = new SparkMD5.ArrayBuffer();
loadNext();
fileReader.onload = (e => {
spark.append(e.target.result);
if (currentChunk < chunks) {
currentChunk++;
loadNext();
} else {
let md5 = spark.end();
this.computeMD5Success(md5, file);
console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
}
});
fileReader.onerror = function () {
this.$notify({
title: '错误',
message: `文件${file.name}读取出错,请检查该文件`,
type: 'error',
duration: 2000
})
file.cancel();
};
function loadNext() {
console.log('loadNext');
let start = currentChunk * chunkSize;
let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
}
},
computeMD5Success(md5, file) {
// 将自定义参数直接加载uploader实例的opts上
Object.assign(this.uploader.opts, {
query: {
...this.params,
tenantId: this.tenantId
}
})
file.uniqueIdentifier = md5;
file.resume();
},
},
destroyed() {
},
components: {}
}
</script>
<style lang="scss" scoped>
@import "@/scss/common.scss";
.mask_box{
position: fixed;
z-index: 19;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
}
#global-uploader {
position: fixed;
z-index: 20;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.uploader-app {
width: 520px;
}
.file-panel {
background-color: #fff;
border: 1px solid #e2e2e2;
border-radius: 7px 7px 0 0;
box-shadow: 0 0 10px rgba(0, 0, 0, .2);
.file-title {
display: flex;
height: 40px;
line-height: 40px;
padding: 0 15px;
border-bottom: 1px solid #ddd;
.operate {
flex: 1;
text-align: right;
}
}
.file-list {
position: relative;
height: 240px;
overflow-x: hidden;
overflow-y: auto;
background-color: #fff;
> li {
background-color: #fff;
}
}
&.collapse {
.file-title {
background-color: #E7ECF2;
}
}
}
.no-file {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
}
/deep/.uploader-file-icon {
&:before {
content: '' !important;
}
// &[icon=image] {
// background: url(./images/image-icon.png);
// }
// &[icon=video] {
// background: url(./images/video-icon.png);
// }
// &[icon=document] {
// background: url(./images/text-icon.png);
// }
}
/deep/.uploader-file-actions > span {
margin-right: 6px;
}
}
/* 隐藏上传按钮 */
#global-uploader-btn {
position: absolute;
clip: rect(0, 0, 0, 0);
}
</style>