前言
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。经过上篇 文件上传【×】面向对象编程【√】 - 掘金 (juejin.cn) ,打算写一个通用的
file-upload上传组件,具备多文件上传、上传文件撤销的功能,想着挺简单,但是在实际开发过程中还是踩了一些很蛋疼的坑((((ToT)†~~~,特此撰文将开发过程中的心得和经验分享给大家,不足之处,欢迎指正!
需求分析
主要需求就三个,如下:
-
文件拖拽上传
-
不仅能单文件上传,多文件也可以同时上传
-
显示上传列表,能够对已上传文件进行撤销操作
效果演示
代码实现
template
<div class="upload-file">
<el-upload
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
show-file-list
drag
multiple
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
:on-success="handleUploadSuccess"
:on-preview="handleUploadedPreview"
:before-remove="beforeDelete"
:on-remove="handleDelete"
class="uploader"
ref="upload"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或 <em>选取文件</em> 上传</div>
<!-- 上传按钮 -->
<!--<el-button size="mini" type="primary">选取文件</el-button>-->
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip">
<template v-if="fileSize"> 请上传大小不超过 <b style="color: #f56c6c"> {{ fileSize }} KB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> 的文件</template>
</div>
</el-upload>
</div>
script
props 自定义属性,接收来自父组件的数据
props: {
// 上传文件数量限制
limit: {
type: Number,
default: 5
},
// 单个上传文件大小限制
fileSize: {
type: Number,
default: 500
},
// 允许上传的文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf", 'png', 'jpg', 'jpeg']
},
// 是否显示文件上传提示
isShowTip: {
type: Boolean,
default: true
}
}
父组件通过 v-bind 绑定 props 自定义的属性,可以个性化设置上传文件限制数量、单个上传文件限制大小、上传文件允许类型以及是否显示上传提示。
data 数据定义
data() {
return {
// 上传的图片请求地址
uploadFileUrl: "http://localhost:8088/file/upload",
fileList: [],
notifyPromise: Promise.resolve()
};
}
notifyPromise: Promise.resolve() 解决组件高度坍塌问题
当多文件上传前文件格式校验不通过时,弹出警告消息,但 Element 一下子同时调用了多次 this.$notify 方法,导致通知消息框高度坍塌,重叠在一起了 ↓
面向 Baidu 编程后,找到了一种采用 Promise 的回调方法可以解决 Element Notification 组件高度塌陷问题,具体造成和解决实现请耐心往后看 o( ̄︶ ̄)o
computed 计算属性
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
}
}
当 isShowTip = false 或者 fileType 和 fileSize 未定义时不显示上传提示。
methods 方法
各方法解析
-
handleBeforeUpload()上传文件之前的钩子,参数为上传的文件,若返回false或者返回Promise且被reject,则停止上传。 -
handleExceed()文件超出个数限制时执行弹出警告通知框。 -
handleUploadError()文件上传失败时执行弹出警告通知框,同时关闭上传加载。 -
handleUploadSuccess()单个文件上传成功就执行。 -
beforeDelete()删除文件之前的钩子,参数为上传的文件和文件列表,若返回false或者返回Promise且被reject,则停止删除。 -
handleDelete()文件列表移除文件时执行,调用删除文件接口,去删除指定的上传文件。 -
uploadFileDelete() 传入指定文件
url和该文件在fileList中的索引,后端根据文件文件路径删除已上传的文件,然后移除fileList中索引值位置上的file。 -
warningNotify() 接收一个参数,即警告信息,用来弹出警告框的,有 2s 的延迟消失时间。
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 校检文件类型
if (this.fileType) {
let fileExtension = "";
if (file.name.lastIndexOf(".") > -1) {
fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
}
const isTypeOk = this.fileType.some((type) => {
return fileExtension && fileExtension.indexOf(type) > -1;
});
if (!isTypeOk) {
this.warningNotify(`文件格式不正确,请上传${this.fileType.join("/")}格式文件!`);
return false;
}
}
// 校检文件大小
if (this.fileSize) {
// KB
const fileSize = file.size / 1024;
const isLt = fileSize < this.fileSize;
if (!isLt) {
this.warningNotify(`上传文件大小不能超过 ${this.fileSize} KB!`);
return false;
}
}
// 开始上传
this.loading = this.$loading({
lock: true,
text: "上传中...",
background: "rgba(0, 0, 0, 0.7)",
});
return true;
},
// 文件个数超出限制
handleExceed() {
this.$message.warning(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传失败
handleUploadError(err) {
this.$message.error(`上传失败[${err}], 请重试`);
this.loading.close();
},
// 上传成功回调
handleUploadSuccess(res, file, fileList) {
if (res.resultCode === 200) {
file['url'] = res.data.path;
//this.fileList.push(file); 报错 Cannot set properties of null (setting 'status')
this.$message.success("上传成功");
this.loading.close();
} else {
this.handleUploadError(res.message);
}
},
// 删除上传文件前
beforeDelete(file, fileList) {
this.fileList = fileList;
if (file.status === 'success') {
return this.$confirm(`确定删除文件【${file.name}】`);
}
},
// 删除上传文件
handleDelete(file, fileList) {
if (file.status === 'success') {
let filePath = file.url;
let fileIndex;
this.fileList.forEach((it, index) => {
if (it.url === filePath) {
fileIndex = index;
}
});
// 删除已上传的文件
this.uploadFileDelete(filePath, fileIndex);
}
},
uploadFileDelete(filePath, fileIndex) {
let _this = this;
if (fileIndex >= 0) {
this.axios({
method: 'DELETE',
url: '/file/upload/delete',
headers: {'content-type': 'application/json'},
data: filePath
}).then((response) => {
let data = response.data;
if (data.resultCode === 200) {
this.$message({
type: 'success',
message: data.message
});
_this.fileList.splice(fileIndex, 1);
} else {
this.$message.error(data.message);
}
}).catch(error => {
this.$message.error(error);
});
} else {
this.$message.error("未找到上传文件,无法删除");
}
}
}
$notify
$notify计算通知的间距时,会拿当前元素的高度,但是因为vue的异步更新队列存在缓冲机制,第一次方法调用时,并没有更新dom,导致拿到的高度为0,所有第二个通知框只是上移了默认的offset 16px。
warningNotify(msg) {
let _this = this;
this.notifyPromise = this.notifyPromise.then(_this.$nextTick).then(() => {
_this.$notify({
type: 'warning',
title: '警告',
message: msg,
duration: 2000
});
});
}
使用
vue提供的nextTick方法,保证第一次通知的dom更新之后,再执行第二次通知的代码,此时通知框的高度就会加上第一个通知框的高度,得到正确的计算高度,这时框重叠问题就解决了。
多文件上传
W( ̄_ ̄)W BUG
原因分析
主要是因为我在上传文件成功回调函数中向实例 fileList 中 push 当前上传的 file ,
我原本单纯认为上传成功后就可以添加到上传文件列表之中,但是实际上是不需要我们手动添加的,我这波操作简直是脱裤子放屁 o(╥﹏╥)o 还是开裆裤的那种!
从文件开始上传就已经全在文件上传列表里了,不必再次 push,否则会在异步多文件上传过程中干扰原来的 fileList ,导致上传文件的 status 状态为 null,从而导致报错!!
BUG 解决
方法一: 将 ...push(file) 注释,然后在删除文件前的回调函数中对实例中的 fileList 赋值就好了
方法二: 再定义一个文件上传列表 uploadList 用来存储上传成功的 files
示例测试
<template>
<upload :limit="10" :file-size="100" :is-show-tip="false"/>
</template>
<script>
import Upload from "../file/Upload";
export default {
name: 'Example',
components: {Upload},
data() {
return {
}
},
methods: {
}
}
</script>
(o゜▽゜)o☆[BINGO!]
结尾
撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。