知道事件捕获! 🤪 但不会用来实现批量拖拽上传?(一)

简介: 知道事件捕获! 🤪 但不会用来实现批量拖拽上传?

image.png


前言

同事 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 组件默认的上传方式是为每一个单独的文件发送单独的请求,大致如下:

image.png

这显然和上述的需求不一致,因此自定义上传方式是必然的。

自定义上传

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

当然除这些之外,在 vue 中一般基于二次封装的组件,可以直接通过 $attrs 的方式来接收外部传入的在 父组件 中不作为 prop 被识别的 attribute 绑定,除了 classstyle 之外,也就是直接使用 属性继承

于是就得到了一个最基本的二次封装后 <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"),具体如下:

image.png

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

image.png

通过 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>
复制代码

来测试一下看看效果吧!

image.png

可以看到 onChange 事件中的判断没有问题,既获取到了总文件数量,也保存了当前的文件,最终的判断条件也是没问题的,但是为什么还是调用了多次的接口呢?

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

image.png

原因超级简单,上面我们控制的是 submit 的执行次数,但是源码中是直接从已上传的文件列表中通过遍历的方式来依次调用 upload 方法,其中是否调用上传方法是根据 beforeUpload 的返回值来决定的(关于这一点在文档中也有说明):

image.png

通过 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>
复制代码

大致效果如下:

image.png


目录
相关文章
|
1月前
|
JavaScript 数据安全/隐私保护
2024了,你会使用原生js批量获取表单数据吗
2024了,你会使用原生js批量获取表单数据吗
47 4
|
6月前
|
搜索推荐
【sgUploadTray_v2】自定义组件:升级版上传托盘自定义组件,可实时查看上传列表进度,可以通过选项卡切换上传中、成功、失败的队列,支持翻页,解决了列表内容太多导致卡顿的情况。(一)
【sgUploadTray_v2】自定义组件:升级版上传托盘自定义组件,可实时查看上传列表进度,可以通过选项卡切换上传中、成功、失败的队列,支持翻页,解决了列表内容太多导致卡顿的情况。
【sgUploadTray_v2】自定义组件:升级版上传托盘自定义组件,可实时查看上传列表进度,可以通过选项卡切换上传中、成功、失败的队列,支持翻页,解决了列表内容太多导致卡顿的情况。(一)
|
6月前
|
前端开发
前端实现拖拽上传
前端实现拖拽上传
101 1
|
6月前
|
JavaScript
点击导出所选数据(原生js)
点击导出所选数据(原生js)
40 0
|
6月前
【sgUploadTray】自定义组件:上传托盘自定义组件,可实时查看上传列表进度。
【sgUploadTray】自定义组件:上传托盘自定义组件,可实时查看上传列表进度。
|
6月前
|
存储 前端开发 JavaScript
点击按钮时触发防抖
点击按钮时触发防抖
80 0
|
前端开发
添加按钮的两种方式
添加按钮的两种方式
87 0
Echarts链接操作弹出窗口防止重复触发点击事件的解决方案
Echarts链接操作弹出窗口防止重复触发点击事件的解决方案
129 0
|
移动开发 HTML5
知道事件捕获! 🤪 但不会用来实现批量拖拽上传?(二)
知道事件捕获! 🤪 但不会用来实现批量拖拽上传?
131 0
移动端touch拖动事件和click事件冲突问题解决
移动端touch拖动事件和click事件冲突问题解决
238 0