批量拖拽上传
上面的方式能不能支持 批量拖拽上传 呢,直接来试试看:
上述我们选择了 3
个文件,也触发了 3
次 beforeUpload
,但在其中的 this.inputFiles
的长度却一直是 0
,而 this.uploadedFiles
的长度在变化,导致最终的判断条件出现了问题。
从源码查找原因
源码位置:element-ui\packages\upload\src\upload.vue
- 当
props.drag
为true
时,会渲染<upload-dragger>
组件 - 当
props.drag
为false
时,会渲染外部指定的 默认插槽的内容
再去看看 <upload-dragger>
组件 的具体内容,大致如下:
显然,当用户通过拖拽的方式实现上传时,是通过 HTML5
中的拖放事件来实现的,那么选择的文件自然不能通过 input.files
的方式获取到,这也就是文章开头提到的问题。
事件捕获 — 事件捕获都知道,那你倒是用起来啊!
通过查看源码之后发现拖拽时一定会触发 onDrop
,那么既然不能通过 input.files
的方式获取用户选中文件的总数量,那么我们就在父级的 onDrop
事件中再去获取用户选择的文件内容(可通过 event.dataTransfer.files
获取),即利用事件捕获的方式
DataTransfer
对象用于保存拖动并放下(drag and drop)过程中的数据,它可以保存一项或多项数据,这些数据项可以是 一种 或 多种 数据类型
<template> <div class="easy-upload" @drop.capture="onDrop"> <el-upload :ref="refName" :name="aliasName" v-bind="$attrs" :auto-upload="false" :before-upload="beforeUpload" :http-request="httpRequest" :on-change="onChange"> <slot></slot> </el-upload> </div> </template> <script lang="ts"> import Vue from 'vue'; import { Component } from 'vue-property-decorator'; import { post } from '@utils/request'; import ElementUI from 'element-ui'; let _uploadId_ = 0; @Component({}) export default class EasyUpload extends Vue { uploadedFiles: File[] = []; inputFiles: File[] = []; refName = '_upload_ref_'; aliasName = '_upload_name_'; created() { this.initConfig(); } // 初始化组件数据 initConfig() { if (this.$attrs.name) this.aliasName = this.$attrs.name; this.refName += _uploadId_; this.aliasName += _uploadId_; _uploadId_++; } formatParams() { const formData = new FormData(); // 文件相关参数 this.uploadedFiles.forEach((file) => { formData.append(this.$attrs.name || 'file', file); }); // 额外参数 const { data } = this.$attrs; if (data) { Object.keys(data).forEach((key) => { formData.append(key, data[key]); }); } return formData; } async httpRequest(options: any) { const formData = this.formatParams(); const res = await post(this.$attrs.action, formData, true); // 重置操作 this.resetUpload(); } beforeUpload() { // 是否需要调用上传接口 return this.uploadedFiles.length === this.inputFiles.length; } onChange(file, fileList) { if (file.status === 'ready') { this.uploadedFiles.push(file.raw); } // 由于开启了事件捕获,因此 ondrop 只要被触发,this.inputFiles 就会有值 // 如果 this.inputFiles 没有值,证明当前是点击上传的方式 if (this.inputFiles.length === 0) { this.inputFiles = Array.from((<HTMLInputElement>document.getElementsByName(this.aliasName)[0]).files || []); } (this.$refs[this.refName] as ElementUI.Upload).submit(); } onDrop(event) { // 事件捕获提前执行,为 inputFiles 赋值 this.inputFiles = Array.from(event.dataTransfer.files); } resetUpload() { this.uploadedFiles = []; this.inputFiles = []; (this.$refs[this.refName] as ElementUI.Upload).clearFiles(); } } </script> 复制代码
效果演示
批量点击上传 和 批量拖拽上传 效果如下:
事情的后续
同事 A
的另一种解决方案
【同事 A
】 看完这篇文章不仅没有 点赞 + 收藏,反而说他实现找到了一种更合适的方式,邀我一同欣赏他的操作,大致思路非常简单:
- 由于
Upload
组件的onChange
事件会被多次执行(即用户选择多少个文件,就会执行多少次) ,并且onChange(file, fileList)
的参数fileList
只有最后一次执行时才会拿到用户选择文件的总数 - 因此 【
同事 A
】 就在onChange
事件中使用了$nextTick
包裹整个onChange
的内容,大致如下:
onChange(file, fileList) { this.$nextTick(() => { file.status == 'ready' && this.uploadFiles.push(file.raw); let files: any = (<HTMLInputElement>document.getElementsByName(this.name)[0]).files; this.fileTotal = this.drag ? fileList.length : files.length; if (this.uploadFiles.length === this.fileTotal) { (this.$refs[this.refName] as any).submit(); } }); } 复制代码
@【同事 A】
等我分析完,总该给我【点赞 + 收藏】了吧!!!
合理分析
为什么可用?
显然,使用了 $nextTick
之后 onChange(file, fileList)
的参数 fileList
就一定是用户选择的文件总数,因为 $nextTick
包裹的内容是一个 微/宏任务,这意味着这段逻辑不会立马执行,而等到它执行时,由于 fileList
参数是对应源码中的 this.uploadFiles
,即等到 $nextTick
的回调函数被执行时,对应的 this.uploadFiles
已经是包含了用户选择的所有文件,因此 this.uploadFiles.length === this.fileTotal
这个判断是可以的。
源码位置:element-ui\packages\upload\src\index.vue
:
handleStart(rawFile) { rawFile.uid = Date.now() + this.tempIndex++; let file = { status: 'ready', name: rawFile.name, size: rawFile.size, percentage: 0, uid: rawFile.uid, raw: rawFile }; if (this.listType === 'picture-card' || this.listType === 'picture') { try { file.url = URL.createObjectURL(rawFile); } catch (err) { console.error('[Element Error][Upload]', err); return; } } this.uploadFiles.push(file); this.onChange(file, this.uploadFiles); }, 复制代码
能用就真的合适用吗?
虽然说上述方式确实能够实现对应的需求,但却并不一定合适:
- 由于
onChange
事件会被多次执行,导致$nextTick
被多次执行,意味着 微/宏任务队列 中会出现多个没有必要被执行的任务
- 比如用户选择文件总数为
4
,onChange
执行4
次,$nextTick
执行4
次,微/宏任务队列 中会被添加4
个任务,而这些任务都已经能够访问最终的fileList
总数,没有必要被多次推入任务队列中
- 相比来说,只需要执行一次即可 ``,比如:
hasChange = false; onChange(file, fileList) { // hasChange 的加持下,$nextTick 只会执行一次 !this.hasChange && this.$nextTick(() => { // 可以拿到用户选择的全部文件列表 this.uploadFiles = fileList; (this.$refs[this.refName] as any).submit(); }); this.hasChange = true; } 复制代码
扩展思路
有了上面的思路,仍然可以进行扩展,只要在 onChange
的最后一次执行时,保存 fileList
和进行 submit
提交,就可以实现最终的目的,比如用【防抖】来实现 onChange
,这样多次触发 onChange
时,也只有最后一次会执行,并且最后一次已经可以拿到我们需要的所有数据。
最后
【统一回复私信】
想交个朋友的可以添加 微信号:Mr10212021 ,也欢迎关注同名公众号《熊的猫》,文章会同步更新!
上述功能需求还是比较简单的,从源码角度来理解也并不困难,经过上述的剖析相信你对自己实现所谓的 点击上传 和 拖拽上传 应该也有自己的理解,完全可以自己去实现一个,并提供给它们对应的 批量上传 方式。
希望本文对你所有帮助!!!