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

前提

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

<input type="file">

使用 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传递给<upload> */
        },
        ref: 'upload-inner'
      };
      
      const trigger = this.$slots.trigger || this.$slots.default;
      // 内部组件 <upload> 包裹
      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>
      );
    }
    
  2. 内部组件 <upload> 绑定事件

    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 type="file"> 选择本机文件
            <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();
      }
    }
    
  3. 通过 <input type="file"> 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);
      });
    }
    
  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);
    }
    

    所以,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);
      });
    }
    

    只有 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);
      }
    }
    

    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);
      }
    }
    

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

常见问题

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

    <el-form>
    	<el-form-item>
      	<el-upload></el-upload>
      </el-form-item>
    </el-form>
    

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

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

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

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

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

    <el-upload :file-list="fileList"></el-upload>
    

    内部实现:

    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;
          });
        }
      }
    }
    

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

    // on-change 事件
    handlerChange (file, filelist) {
      this.fileList.push(file) 
      /* 或者其他操作,无需通过 filelist 处理(组件内部对象引用)*/
    }
    
  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';
    }
    

    此时,处理方案有两种:① 修改 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)
      }
    }
    

    如果定了 file-list prop <el-upload :file-list="fileList"></el-upload>,则直接通过控制自己定义的 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)
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐