超详细【el-upload】二次封装心得以及踩坑分享

8,706 阅读3分钟

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。经过上篇 文件上传【×】面向对象编程【√】 - 掘金 (juejin.cn) ,打算写一个通用的 file-upload 上传组件,具备多文件上传上传文件撤销的功能,想着挺简单,但是在实际开发过程中还是踩了一些很蛋疼的坑 ((((ToT)†~~~ ,特此撰文将开发过程中的心得和经验分享给大家,不足之处,欢迎指正!

需求分析

主要需求就三个,如下:

  • 文件拖拽上传

  • 不仅能单文件上传,多文件也可以同时上传

  • 显示上传列表,能够对已上传文件进行撤销操作

效果演示

效果演示.gif

代码实现

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 方法,导致通知消息框高度坍塌,重叠在一起了 ↓

image.png

面向 Baidu 编程后,找到了一种采用 Promise 的回调方法可以解决 Element Notification 组件高度塌陷问题,具体造成和解决实现请耐心往后看 o( ̄︶ ̄)o

computed 计算属性

computed: {
    // 是否显示提示
    showTip() {
        return this.isShowTip && (this.fileType || this.fileSize);
    }
}

isShowTip = false 或者 fileTypefileSize 未定义时不显示上传提示。

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

多文件上传BUG.gif

image.png

原因分析

主要是因为我在上传文件成功回调函数中向实例 fileListpush 当前上传的 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!]

image.png

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。