关于前端:知道事件捕获-🤪-但不会用来实现批量拖拽上传

前言

共事 A】:你晓得 Element UIUpload 组件怎么实现 批量拖拽上传 吗?现有的二次封装的 Upload 组件只能做到 批量点击上传

】:Show me your code !!!

共事 A】:代码地址是 https://git.i-have-no-idea.com/what-the-hell

】:就冲这个地址,就算是天天 CRUD 的我,也高下给你整一篇水文 ~~

共事 A 接到的大抵需要是这样的:

  • 当用户同时抉择单个文件时,该文件会通过辨认后须要生成一个对应 待填写的信息项,并将辨认的内容进行主动填充和供用户手动编辑
  • 当用户同时抉择多个文件时,这多个文件会被作为同一个 待填写的信息项 的内容进行显示

接口设计 的内容大抵如下:

  • 前端每调用一次该辨认接口,就会返回一个新的 待填写的信息项 数据,其中会蕴含本次上传的 单个或多个 文件信息
  • 意味着生成 待填写的信息项 应该只调用一次上传接口,屡次调用则会失去多个 待填写的信息项

上面就基于 Element UIUpload 组件进行二次封装,一边封装一边复现上述问题,而后在解决问题,次要内容蕴含 批量点击上传批量拖拽上传 两种形式。

自定义上传形式

该我的项目波及核心技术为:vue@2.6.10 + vue-property-decorator@8.3.0 + element-ui@2.15.1,上面省略创立测试项目的过程 ~ ~

默认上传形式

相熟 Element UI 的都晓得 Upload 组件默认的上传形式是为每一个独自的文件发送独自的申请,大抵如下:

这显然和上述的需要不统一,因而自定义上传形式是必然的。

自定义上传

Upload 组件提供给了对应的 auto-upload 选项便于使用者可能自定义上传机会,同时为了可能更好的管制上传逻辑,还得应用 http-request 选项来笼罩其默认的上传行为,这样便于咱们自定义上传的实现。

当然除这些之外,在 vue 中个别基于二次封装的组件,能够间接通过 $attrs 的形式来接管内部传入的在 父组件 中不作为 prop 被辨认的 attribute 绑定,除了 class 和 style 之外,也就是间接应用 属性继承

于是就失去了一个最根本的二次封装后 <EasyUpload /> 组件的大抵内容:

<template>
  <div class="easy-upload">
    <el-upload :ref="refName" :name="aliasName" v-bind="$attrs" :auto-upload="false" :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';

// 定义一个自增的 id ,防止在同一个组件中屡次应用 EasyUpload 造成 ref 和 name 反复
let _uploadId_ = 0;

@Component({})
export default class EasyUpload extends Vue {
  refName = '_upload_ref_';
  aliasName = '_upload_name_';

  created() {
    this.initConfig();
  }

  // 初始化组件数据
  initConfig() {
    if (this.$attrs.name) this.aliasName = this.$attrs.name;

    // 保障 refName 和 <input> 的 name 属性值在父组件中惟一
    this.refName += _uploadId_;
    this.aliasName += _uploadId_;
    _uploadId_++;
  }

  formatParams(file) {
    const formData = new FormData();

    // 文件相干参数
    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(options.file);
    const res = await post(this.$attrs.action, formData, true);
    // do something
    (this.$refs[this.refName] as ElementUI.Upload).clearFiles();
  }

  onChange(file, fileList) {
    // 触发上传逻辑
    (this.$refs[this.refName] as ElementUI.Upload).submit();
  }
}
</script>

<style scoped lang="less">
@import './index.less';
</style>

批量点击上传

显然,以上的实现基本满足不了 批量点击上传,因为咱们在 onChange 间接调用了 submit 来实现和间接应用 el-upload 差不多的上传形式,既然如此咱们只有在 onChange 只调用一次 submit 办法即可,判断办法很简略:

  • 首先要获取用户以后一共选中了多少文件数量,即 总数量
  • 每次触发 onChange 时,把以后的 file 对象保留到 uploadedFiles 中,直到 uploadedFiles 中的文件数量和总数量统一时,在手动触发 submit 办法

怎么获取以后用户获取文件的总数量?

别着急,咱们先审查元素看看 el-upload 到底渲染的是个啥?置信你大概率曾经猜到了,其实就是 type="file"<input /> 元素,它自身也反对多选(即设置multiple="multiple"),具体如下:

更重要的是 input 元素的 onchange 中能够获取到对应的文件列表对象 files,即通过 event.target.files 来获取,而这就是用户抉择文件的总数量

通过 onChange 管制 submit

通过改写的 onChange 如下所示,没有什么太大的难点,间接上代码:

为什么不必 onChange 中的 fileList 参数呢?因为多选的状况下每次 onChange 被执行,其中的 fileList 就只会减少一条数据,而不是所有的数据,因而没方法依据其值来进行判断.

<script lang="ts">
import Vue from 'vue';
import { Component } from 'vue-property-decorator';
import { post } from '@utils/request';
import ElementUI from 'element-ui';

// 定义一个自增的 id ,防止在同一个组件中屡次应用 EasyUpload 造成 ref 和 name 反复
let _uploadId_ = 0;

@Component({})
export default class EasyUpload extends Vue {
  inputFiles: File[] = [];
  uploadedFiles: File[] = [];
  ...

  created() {
    this.initConfig();
  }

  // 初始化组件数据
  initConfig() {
   ...
  }

  formatParams(file) {
    ....
  }

  async httpRequest(options: any) {
    const formData = this.formatParams(options.file);
    const res = await post(this.$attrs.action, formData, true);
    // do something
    (this.$refs[this.refName] as ElementUI.Upload).clearFiles();
  }

  onChange(file, fileList) {
    // 将以后文件保留到 uploadedFiles 中
    if (file.status == 'ready') {
      this.uploadedFiles.push(file.raw);
    }

    // 只赋值一次,因为 input 元素上的 files 就是本次用户选中的所有文件
    if (this.inputFiles.length === 0) {
      this.inputFiles = Array.from((<HTMLInputElement>document.getElementsByName(this.aliasName)[0]).files || []);
    }

    console.log(' ================ onChange trigger ================ ');
    console.log('inputFiles.length = ', this.inputFiles.length, 'uploadedFiles.length = ', this.uploadedFiles.length);

    // 触发上传逻辑
    if (this.inputFiles.length === this.uploadedFiles.length) {
      (this.$refs[this.refName] as ElementUI.Upload).submit();
    }
  }
}
</script>

来测试一下看看成果吧!

能够看到 onChange 事件中的判断没有问题,既获取到了总文件数量,也保留了以后的文件,最终的判断条件也是没问题的,然而为什么还是调用了屡次的接口呢?

这个不扯别的,间接上源码,因为内容太简略了,文件门路:element-ui\packages\upload\src\upload.vue

起因超级简略,下面咱们管制的是 submit 的执行次数,然而源码中是间接从已上传的文件列表中通过遍历的形式来顺次调用 upload 办法,其中是否调用上传办法是依据 beforeUpload 的返回值来决定的(对于这一点在文档中也有阐明):

通过 beforeUpload 管制上传

beforeUpload 是上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject 会进行上传,于是能够有如下的实现:

<template>
  <div class="easy-upload">
    <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';
import { lang } from 'moment';

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.$refs[this.refName] as ElementUI.Upload).clearFiles();
  }

  beforeUpload() {
    // 是否须要调用上传接口
    return this.uploadedFiles.length === this.inputFiles.length;
  }

  onChange(file, fileList) {
    if (file.status === 'ready') {
      this.uploadedFiles.push(file.raw);
    }

    // 只赋值一次,因为 input 元素上的 files 就是本次用户选中的所有文件
    if (this.inputFiles.length === 0) {
      this.inputFiles = Array.from((<HTMLInputElement>document.getElementsByName(this.aliasName)[0]).files || []);
    }

    (this.$refs[this.refName] as ElementUI.Upload).submit();
  }
}
</script>

大抵成果如下:

批量拖拽上传

下面的形式能不能反对 批量拖拽上传 呢,间接来试试看:

上述咱们抉择了 3 个文件,也触发了 3beforeUpload,但在其中的 this.inputFiles 的长度却始终是 0,而 this.uploadedFiles 的长度在变动,导致最终的判断条件呈现了问题。

从源码查找起因

源码地位:element-ui\packages\upload\src\upload.vue

  • props.dragtrue 时,会渲染 <upload-dragger> 组件
  • props.dragfalse 时,会渲染内部指定的 默认插槽的内容

再去看看 <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 被屡次执行,意味着 微/宏工作队列 中会呈现多个没有必要被执行的工作

    • 比方用户抉择文件总数为 4onChange 执行 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 时,也只有最初一次会执行,并且最初一次曾经能够拿到咱们须要的所有数据。

最初

上述性能需要还是比较简单的,从源码角度来了解也并不艰难,通过上述的分析置信你对本人实现所谓的 点击上传拖拽上传 应该也有本人的了解,齐全能够本人去实现一个,并提供给它们对应的 批量上传 形式。

心愿本文对你所有帮忙!!!@【共事 A】

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理