• 关于el-upload看这一篇就够了


    下述源码分析基于 Element v2.15.9 版本

    前提

    在解析源码之前,先阐述其重点使用的两个基础内容:

    使用 type=“file” 的 元素使得用户可以选择一个或多个元素以提交表单的方式上传到服务器上,或者通过 Javascript 的 File API 对文件进行操作。

    其支持附加属性:

    属性说明
    accept一个或多个 唯一文件类型说明符 描述允许的文件类型
    capture捕获图像或视频数据的源
    filesFileList 列出了已选择的文件
    multiple布尔值,如果出现,则表示用户可以选择多个文件

    XMLHttpRequest

    XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。

    其支持的关键属性/方法/事件:

    属性/方法/事件说明
    upload代可以通过对其绑定事件来追踪它的进度
    setRequestHeader()设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用
    open()初始化一个请求
    abort()如果请求已被发出,则立刻中止请求
    send()发送请求。如果请求是异步的(默认),那么该方法将在请求发送后立即返回
    load请求成功完成时触发
    error当 request 遭遇错误时触发

    el-upload 多数 prop 是借助上述两个原生形式实现的。

    el-upload 执行逻辑

    1. 定义 trigger slot 或使用默认 slot

      packages/upload/src/index.vue render()

      render(h) {
        let uploadList;
        
        if (this.showFileList) {
          uploadList = ( <UploadList ...>);
        }
      
        const uploadData = {
          props: {
            /* 注入的props传递给 */
          },
          ref: 'upload-inner'
        };
        
        const trigger = this.$slots.trigger || this.$slots.default;
        // 内部组件  包裹
        const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
      
        return (
          <div>
          	{ this.listType === 'picture-card' ? uploadList : ''}
          	{
          		this.$slots.trigger
          			? [uploadComponent, this.$slots.default]
        				: uploadComponent
      			}
            {this.$slots.tip}
            { this.listType !== 'picture-card' ? uploadList : ''}
          </div>
        );
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
    2. 内部组件 绑定事件

      packages/upload/src/upload.vue render()

      render(h) {
          let { handleClick, ... } = this;
          const data = {
            class: {
              'el-upload': true
            },
            on: {
              click: handleClick,
              keydown: handleKeydown
            }
          };
          data.class[`el-upload--${listType}`] = true;
          return (
            // 外层绑定了 click/keydown 事件
            <div {...data} tabindex="0" >
              {
                drag
                  ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
                  : this.$slots.default
              }
              //  选择本机文件
              <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
            </div>
          );
      }
      
      // 打开选择文件弹窗
      handleClick() {
        if (!this.disabled) {
          this.$refs.input.value = null;
          this.$refs.input.click();
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
    3. 通过 on-change 事件获取上传文件

    4. 判断文件是否超出 limit prop 限制,超出后调用 on-exceed

      这里需要注意,区分自动上传、手动上传

      handleChange(ev) {
        const files = ev.target.files;
      
        if (!files) return;
        this.uploadFiles(files);
      },
      
      uploadFiles(files) {
        if (this.limit && this.fileList.length + files.length > this.limit) {
          this.onExceed && this.onExceed(files, this.fileList);
          return;
        }
      
        let postFiles = Array.prototype.slice.call(files);
        if (!this.multiple) { postFiles = postFiles.slice(0, 1); }
      
        if (postFiles.length === 0) { return; }
      
        postFiles.forEach(rawFile => {
          // 手动、自动上传前都会触发
          this.onStart(rawFile);
          if (this.autoUpload) this.upload(rawFile);
        });
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
    5. onStart(rawFile),这里会调用 on-chagne

      handleStart(rawFile) {
        rawFile.uid = Date.now() + this.tempIndex++;
        let file = {
          status: 'ready',
          ...
        };
      
        if (this.listType === 'picture-card' || this.listType === 'picture') {
          try {
            file.url = URL.createObjectURL(rawFile);
          } catch (err) { ... }
        }
      
        this.uploadFiles.push(file);
        // 调用 on-change
        this.onChange(file, this.uploadFiles);
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17

      所以,on-change 的执行顺序早于 before-upload,且不区分是否自动

    6. 【手动上传】this.refs['upload'].submit

      手动上传,官方给出的方式是调用 el-upload 组件的 submit()

      submit() {
        this.uploadFiles
          .filter(file => file.status === 'ready')
          .forEach(file => {
          this.$refs['upload-inner'].upload(file.raw);
        });
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

      只有 ready 的才可以调用 upload

    7. this.upload(rawFile)

      upload(rawFile) {
        this.$refs.input.value = null;
      
        if (!this.beforeUpload) {
          return this.post(rawFile);
        }
      	// before-upload 在该阶段执行!
        const before = this.beforeUpload(rawFile);
        if (before && before.then) {
          before.then(processedFile => {
            // 忽略了逻辑分支判断
            this.post(rawFile); 
          }, () => {
            // ①
            this.onRemove(null, rawFile);
          });
        } else if (before !== false) {
          this.post(rawFile);
        } else {
          // ①
          this.onRemove(null, rawFile);
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

      before-upload 返回 false/Promise.reject() 会调用 on-remove

    8. this.post(rawFile) Ajax 提交文件

      post(rawFile) {
        options = { headers, withCredentials, action, filename, data, file }
        const req = this.httpRequest(options)
        this.reqs[uid] = req;
        if (req && req.then) {
          req.then(options.onSuccess, options.onError);
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8

      通过 XMLHttpRequest 封装,会调用 on-progresson-successon-error

    常见问题

    1. 可以作为form表单元素使用

      <el-form>
      	<el-form-item>
        	<el-upload>el-upload>
        el-form-item>
      el-form>
      
      • 1
      • 2
      • 3
      • 4
      • 5

      disabled 的状态,可以沿用 el-form 的 disabled 状态

      computed: {
        uploadDisabled() {
          // 这段代码存在逻辑漏洞,当 form 表单为 disabled,el-upload 为 fasle 不起作用
          return this.disabled || (this.elForm || {}).disabled;
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6

      注意:form 表单元素普遍存在上述问题:
      this.$options.propsData.hasOwnProperty('disabled') ? this.disabled : (this.elForm || {}).disabled;

      但,其不会触发 el.form.change 事件,即不会触发验证流程
      在这里插入图片描述

    2. 如何设置了 file-list prop,内部会监听其变化

      
      
      • 1

      内部实现:

      watch: {
        fileList: {
          immediate: true,
            handler(fileList) {
            this.uploadFiles = fileList.map(item => {
              item.uid = item.uid || (Date.now() + this.tempIndex++);
              item.status = item.status || 'success';
              return item;
            });
          }
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12

      这意味,一旦指定 file-list 后,自己业务中操作全部可以围绕此对象 fileList 展开即可,不要同其提供的 filelist 混淆使用。

      // on-change 事件
      handlerChange (file, filelist) {
        this.fileList.push(file) 
        /* 或者其他操作,无需通过 filelist 处理(组件内部对象引用)*/
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
    3. 非自动上传 before-upload 失效

      通过上述源码分析可知【第7步】,其是在 this.upload(rawFile) 确认提交环节才执行,对于非自动上传,调用 submit() 时才触发,并非不触发。

      这意味,在非自动上传场景下,验证文件基础信息(大小、类型、个数等),需要在 on-change 中处理!

    4. 非自动上传后端校验失败后,该文件不能再上传(对于携带formdata字段唯一性校验,很常见)

      通过上述源码分析可知【第6步】,非自动上传调用 submit() 方法,只针对 file 为 ready 状态文件调用上传方法;而一旦上传过,该文件状态会改变为 success

      handleProgress(ev, rawFile) {
        file.status = 'uploading';
      }
      handleSuccess(res, rawFile) {
        file.status = 'success';
      }
      handleError(err, rawFile) {
        file.status = 'fail';
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      此时,处理方案有两种:① 修改 file 状态为 ready;② 自定义上传 ajax 方法(不调用submit)!

    5. 限制只有一个文件,如果存在已上传文件,希望覆盖操作

      通过上述源码分析可知【第4步】,el-upload 提供了 limit 属性,如果将其设置为 1,会在选择文件时进行判断,如果超出不会做任何操作,此时达不到覆盖的效果。

      这意味,我们不能通过 limit 控制(不设置 limit),在 on-change 中修改 filelist!

      handleChange (file, fileList) {
        // 只保留一个文件
        if (fileList.length > 1) {
          // 这里直接改了引用值 组件内部 uploadFiles
          fileList.splice(0, 1)
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

      如果定了 file-list prop ,则直接通过控制自己定义的 filelist 即可(常见问题2中我们有提及,内部会watch该 filelist)

    总结

    el-upload 提供了诸多处理,为我们日常开发提供了便利性,同时也存在着一些边界没有处理。所以,这里建议如下:

    【关于校验】放到 on-change 中实现,而不是 before-upload

    1. 这样无需关心是否为自动上传执行问题(非自动掉用submit,才触发before-upload
    2. before-upload 返回 false,会执行 on-remove,整体比较混乱

    【关于是否自定义 file-list】

    1. 如果存在存量file,一定要使用file-list,便于初始化展示
    2. 对于文件列表有其他业务要求可自定义,否则不建议使用,避免引用之间的传递问题

    【非自动上传】auto-upload=false

    1. 如果存在其他【上传时附带的额外参数】后端校验问题,建议自定义上传 ajax(而非修改 file status = ready)
  • 相关阅读:
    ROS系统通过类定义实现数据处理并重新发布在另一话题
    Linux文件编程API的使用
    【STM32】入门(四):外部中断-按键通过中断动作
    翻译:Fully Convolutional Networksfor Semantic Segmentation
    IPv6通信实验
    数据结构复习笔记6.2:图的存储和遍历
    镜头之滤光片---关于日夜两用双通滤光片
    如何用python一键保存绝美***壁纸?
    VUE——验证码倒计时
    LABVIEW详细介绍:LABVIEW是什么软件?都可以干什么?
  • 原文地址:https://blog.csdn.net/ligang2585116/article/details/126147199