element ui upload 源码解析-逐行逐析

389 阅读17分钟

[TOC]

ajax封装

ajax代码

function getError(action, option, xhr) {
    // 获取错误信息
  let msg;
  if (xhr.response) {
    msg = `${xhr.response.error || xhr.response}`;
  } else if (xhr.responseText) {
    msg = `${xhr.responseText}`;
  } else {
    msg = `fail to post ${action} ${xhr.status}`;
  }

  const err = new Error(msg);
  err.status = xhr.status;
  err.method = 'post';
  err.url = action;
  return err;
}

function getBody(xhr) {
  const text = xhr.responseText || xhr.response;
  if (!text) {
    return text;
  }

  try {
    return JSON.parse(text);
    // 如果能够使用JSON.parse解析text 就直接解析,并返回解析后的数据
  } catch (e) {
    // 如果解析失败,则表明test不是json串格式,只是一个普通的字符串,直接返回即可
    return text;
  }
}

export default function upload(option) {
  if (typeof XMLHttpRequest === 'undefined') {
    // 如果 浏览器不支持 XMLHttpRequest 就终止
    return;
  }

  const xhr = new XMLHttpRequest();
  const action = option.action;

  if (xhr.upload) {
    xhr.upload.onprogress = function progress(e) {
        // 上传进度实时更新
      if (e.total > 0) {
        e.percent = e.loaded / e.total * 100;
      }
    //   调用父组件传入的onProgress 函数将 进度同步给父组件
      option.onProgress(e);
    };
  }

  const formData = new FormData();
// 创建FormData对象,并且将对象数据添加到formData中
  if (option.data) {
    // 添加上传时除了文件的额外数据
    Object.keys(option.data).forEach(key => {
      formData.append(key, option.data[key]);
    });
  }
    // 将文件数据添加到formData中
  formData.append(option.filename, option.file, option.file.name);

  xhr.onerror = function error(e) {
    // xhr的异常函数触发了,将异常信息,使用父组件传入的onError函数将异常信息抛出
    option.onError(e);//
  };

  xhr.onload = function onload() {
    if (xhr.status < 200 || xhr.status >= 300) {
        // 当状态码不等于200时抛出异常
      return option.onError(getError(action, option, xhr));
    }
    // 状态码200 表示访问正常 将XMLHttpRequest 返回的数据
    // 利用传入的回调函数抛出
    option.onSuccess(getBody(xhr));
  };

  xhr.open('post', action, true);

  if (option.withCredentials && 'withCredentials' in xhr) {
    xhr.withCredentials = true;//withCredentials 允许跨源请求
  }

  const headers = option.headers || {};
  for (let item in headers) {
    // for in 循环 headers 
    // 将对象里的key和value使用 setRequestHeader 将传入的请求头数据 添加到将XMLHttpRequest的header中
    if (headers.hasOwnProperty(item) && headers[item] !== null) {
      xhr.setRequestHeader(item, headers[item]);
    }
  }
  xhr.send(formData);//发送请求
  return xhr;
}

ajax封装的基础知识点和基本逻辑

XMLHttpRequest简介

  • 大家都知道XMLHttpRequest 是封装ajax的基础,但是实际上,axios也是基于它封装的,并且经过我对axios源码的探究,element-ui的代码也是基于它封装的

XMLHttpRequest 的基本使用步骤

  1. 创建 XMLHttpRequest
    const xhr = new XMLHttpRequest();
  1. 初始化
   xhr.open(method, URL, [async, user, password])
  1. 发送请求
    xhr.send([body])
  1. 监听事件
  • load 请求完成
    xhr.onload =()=>{
        console.log("已经加载完成")
    }
  • error 请求无法发出,例如网络中断或者无效的url
    xhr.onerror =()=>{
        console.og("networking error")
    }
  • progress 在下载响应期间定期触发,报告已经下载了多 xhr.onprogress =(event)=>{ console.log(已经下载了${event.loaded}) console.log(一共有${event.total}) }
  1. 超时时间 timeout element 源码里没有添加,建议添加
 xhr.timeout = 6*1000
  1. 响应类型
类型说明
"" (默认)响应格式为字符串
text响应格式为字符串
arraybuffer响应格式为 ArrayBuffer(二进制数组)
blob响应格式为 Blob (二进制数据)
document响应格式为 XML document(可以使用 XPath 和其他 XML 方法)或 HTML document(基于接收数据的 MIME 类型)
json响应格式为 JSON(自动解析)

7.readystate

    xhr.onreadystatechange = function() {
    if (xhr.readyState == 1) {
        // 初始状态
    }
    if (xhr.readyState == 2) {
        // open被调用
    }
    if (xhr.readyState == 3) {
        // 加载中
    }
    if (xhr.readyState == 4) {
        // 请求完成
    }
};
  1. abort 终止请求 它会触发 abort 事件,且 xhr.status 变为 0
    xhr.abort()
  1. HTTP-header
  • XMLHttpRequest 允许发送自定义 header,并且可以从响应中读取 header
  • setRequestHeader(name, value)
    xhr.setRequestHeader('Content-Type', 'application/json');
  • getResponseHeader 获取具有给定 name 的 header
  xhr.getResponseHeader('Content-Type')
  • getAllResponseHeaders 返回除 Set-Cookie 和 Set-Cookie2 外的所有 response header。
  • 获取header对象 header 之间的换行符始终为 "\r\n"(不依赖于操作系统),所以我们可以很容易地将其拆分为单独的 header。name 和 value 之间总是以冒号后跟一个空格 ": " 分隔。这是标准格式
    let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

  1. post FormData 文件上传
    const formData = new FormData();
    formData.append('formKey',"formvalue");
    const xhr = new XMLHttpRequest();
    xhr.open('POST','http://test.example')
    xhr.send(formData);
    xhr.onload=function(){
        console.log(xhr.response);
    }
  1. 跨源请求
  • withCredentials
    const xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    xhr.open('POST', 'http://test/request');
  1. status状态码
状态码含义
200OK,访问正常
301Moved Permanently,永久移动
302Moved temporarily,暂时移动
304Not Modified,未修改
307Temporary Redirect,暂时重定向
401Unauthorized,未授权
403Forbidden,禁止访问
404Not Found,未发现指定网址
500Internal Server Error,服务器发生错误[TOC]

ajax封装

ajax代码

function getError(action, option, xhr) {
    // 获取错误信息
  let msg;
  if (xhr.response) {
    msg = `${xhr.response.error || xhr.response}`;
  } else if (xhr.responseText) {
    msg = `${xhr.responseText}`;
  } else {
    msg = `fail to post ${action} ${xhr.status}`;
  }

  const err = new Error(msg);
  err.status = xhr.status;
  err.method = 'post';
  err.url = action;
  return err;
}

function getBody(xhr) {
  const text = xhr.responseText || xhr.response;
  if (!text) {
    return text;
  }

  try {
    return JSON.parse(text);
    // 如果能够使用JSON.parse解析text 就直接解析,并返回解析后的数据
  } catch (e) {
    // 如果解析失败,则表明test不是json串格式,只是一个普通的字符串,直接返回即可
    return text;
  }
}

export default function upload(option) {
  if (typeof XMLHttpRequest === 'undefined') {
    // 如果 浏览器不支持 XMLHttpRequest 就终止
    return;
  }

  const xhr = new XMLHttpRequest();
  const action = option.action;

  if (xhr.upload) {
    xhr.upload.onprogress = function progress(e) {
        // 上传进度实时更新
      if (e.total > 0) {
        e.percent = e.loaded / e.total * 100;
      }
    //   调用父组件传入的onProgress 函数将 进度同步给父组件
      option.onProgress(e);
    };
  }

  const formData = new FormData();
// 创建FormData对象,并且将对象数据添加到formData中
  if (option.data) {
    // 添加上传时除了文件的额外数据
    Object.keys(option.data).forEach(key => {
      formData.append(key, option.data[key]);
    });
  }
    // 将文件数据添加到formData中
  formData.append(option.filename, option.file, option.file.name);

  xhr.onerror = function error(e) {
    // xhr的异常函数触发了,将异常信息,使用父组件传入的onError函数将异常信息抛出
    option.onError(e);//
  };

  xhr.onload = function onload() {
    if (xhr.status < 200 || xhr.status >= 300) {
        // 当状态码不等于200时抛出异常
      return option.onError(getError(action, option, xhr));
    }
    // 状态码200 表示访问正常 将XMLHttpRequest 返回的数据
    // 利用传入的回调函数抛出
    option.onSuccess(getBody(xhr));
  };

  xhr.open('post', action, true);

  if (option.withCredentials && 'withCredentials' in xhr) {
    xhr.withCredentials = true;//withCredentials 允许跨源请求
  }

  const headers = option.headers || {};
  for (let item in headers) {
    // for in 循环 headers 
    // 将对象里的key和value使用 setRequestHeader 将传入的请求头数据 添加到将XMLHttpRequest的header中
    if (headers.hasOwnProperty(item) && headers[item] !== null) {
      xhr.setRequestHeader(item, headers[item]);
    }
  }
  xhr.send(formData);//发送请求
  return xhr;
}

ajax封装的基础知识点和基本逻辑

XMLHttpRequest简介

  • 大家都知道XMLHttpRequest 是封装ajax的基础,但是实际上,axios也是基于它封装的,并且经过我对axios源码的探究,element-ui的代码也是基于它封装的

XMLHttpRequest 的基本使用步骤

  1. 创建 XMLHttpRequest
    const xhr = new XMLHttpRequest();
  1. 初始化
   xhr.open(method, URL, [async, user, password])
  1. 发送请求
    xhr.send([body])
  1. 监听事件
  • load 请求完成
    xhr.onload =()=>{
        console.log("已经加载完成")
    }
  • error 请求无法发出,例如网络中断或者无效的url
    xhr.onerror =()=>{
        console.og("networking error")
    }
  • progress 在下载响应期间定期触发,报告已经下载了多 xhr.onprogress =(event)=>{ console.log(已经下载了${event.loaded}) console.log(一共有${event.total}) }
  1. 超时时间 timeout element 源码里没有添加,建议添加
 xhr.timeout = 6*1000
  1. 响应类型
类型说明
"" (默认)响应格式为字符串
text响应格式为字符串
arraybuffer响应格式为 ArrayBuffer(二进制数组)
blob响应格式为 Blob (二进制数据)
document响应格式为 XML document(可以使用 XPath 和其他 XML 方法)或 HTML document(基于接收数据的 MIME 类型)
json响应格式为 JSON(自动解析)

7.readystate

    xhr.onreadystatechange = function() {
    if (xhr.readyState == 1) {
        // 初始状态
    }
    if (xhr.readyState == 2) {
        // open被调用
    }
    if (xhr.readyState == 3) {
        // 加载中
    }
    if (xhr.readyState == 4) {
        // 请求完成
    }
};
  1. abort 终止请求 它会触发 abort 事件,且 xhr.status 变为 0
    xhr.abort()
  1. HTTP-header
  • XMLHttpRequest 允许发送自定义 header,并且可以从响应中读取 header
  • setRequestHeader(name, value)
    xhr.setRequestHeader('Content-Type', 'application/json');
  • getResponseHeader 获取具有给定 name 的 header
  xhr.getResponseHeader('Content-Type')
  • getAllResponseHeaders 返回除 Set-Cookie 和 Set-Cookie2 外的所有 response header。
  • 获取header对象 header 之间的换行符始终为 "\r\n"(不依赖于操作系统),所以我们可以很容易地将其拆分为单独的 header。name 和 value 之间总是以冒号后跟一个空格 ": " 分隔。这是标准格式
    let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

  1. post FormData 文件上传
    const formData = new FormData();
    formData.append('formKey',"formvalue");
    const xhr = new XMLHttpRequest();
    xhr.open('POST','http://test.example')
    xhr.send(formData);
    xhr.onload=function(){
        console.log(xhr.response);
    }
  1. 跨源请求
  • withCredentials
    const xhr = new XMLHttpRequest();
    xhr.withCredentials = true;
    xhr.open('POST', 'http://test/request');
  1. status状态码
状态码含义
200OK,访问正常
301Moved Permanently,永久移动
302Moved temporarily,暂时移动
304Not Modified,未修改
307Temporary Redirect,暂时重定向
401Unauthorized,未授权
403Forbidden,禁止访问
404Not Found,未发现指定网址
500Internal Server Error,服务器发生错误

upload 组件文件上传 组件封装

upload.vue 组件

代码

<script>
import ajax from './ajax';
// import UploadDragger from './upload-dragger.vue';

export default {
  inject: ['uploader'],
  components: {
    // UploadDragger
  },
  props: {
    type: String,//
    action: {//action请求路径
      type: String,
      required: true
    },
    name: {//上传时 ,文件流的 key(键) 名称
      type: String,
      default: 'file'
    },
    data: Object,//上传时附带的额外参数
    headers: Object,//设置上传的请求头部
    withCredentials: Boolean,//支持发送cookie 凭证信息
    multiple: Boolean,//是否支持多选文件
    accept: String,//接受上传的文件类型
    onStart: Function,//开始上传的函数
    onProgress: Function,//文件上传时的钩子 进度
    onSuccess: Function,//文件上传成功的钩子函数
    onError: Function,//错误回调函数
    beforeUpload: Function,//上传文件之前的钩子, 一般用于上传之前的拦截处理,如 文件类型,文件大小的拦截等
    drag: Boolean,//是否启用拖拽上传
    onPreview: {//点击已经上传了的文件时的狗子,可用于处理文件预览的逻辑
      type: Function,
      default: function() {}
    },
    onRemove: {//文件移除的钩子 
      type: Function,
      default: function() {}
    },
    fileList: Array,//上传的文件列表
    autoUpload: Boolean,//是否在选取文件后立即进行上传
    listType: String,//文件列表的类型  text/picture/picture-card 待优化里欸包yang
    httpRequest: {//覆盖默认的上传行为 可自定一上传的实现
      type: Function,
      default: ajax//默认时封装的上传行为
    },
    disabled: Boolean,//是否禁用 
    limit: Number,//文件上传的个数
    onExceed: Function//文件超出个数限制时的钩子	
  },

  data() {
    return {
      mouseover: false,
      reqs: {}//上传文件的请求对象
      //请求中  给请求对象 添加一个属性 键值 键值为当前文件的uid
    //   请求成功或者失败  删除reqs[当前属性ui]
    };
  },

  methods: {
    isImage(str) {
      return str.indexOf('image') !== -1;
    },
    handleChange(ev) {
      const files = ev.target.files;//获取到文件上传的files
      if (!files) return;//没有选择文件则拦截
      this.uploadFiles(files);
    },
    uploadFiles(files) {
      if (this.limit && this.fileList.length + files.length > this.limit) {
        /**
         * limit 是限制的最大文件数量
         * fileList 这次点击之前的已经上穿文件
         * files 这次点击传入的文件 是个数组 长度可为1到多
         * 
        */
        // 文件数量大于 限制的最大文件限制数量 则拦截并且 触发 个数超出的钩子函数
        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; }
    //   如果文件数组的长度为0 也就是没有要上传的文件,则拦截
      postFiles.forEach(rawFile => {
            //调用开始上传文件
        this.onStart(rawFile);
        if (this.autoUpload) this.upload(rawFile);
        // 如果 autoUpload 为true 也就是开启了自动上传模式 调用upload方法
      });
    },
    upload(rawFile) {
      this.$refs.input.value = null;

      if (!this.beforeUpload) {
        // beforeUpload 用于上传前拦截
        // beforeUpload 的返回值 有两种情况
        // 1 为boolean true或者false
        // 2 返回 promise 函数
        // 此处表示没有传入beforeUpload函数 也就是 没有作上传前的异常拦截处理 直接调用上传到的方法
        return this.post(rawFile);
      }

      const before = this.beforeUpload(rawFile);
      // 能走到这一步 表示组件在调用的时候传入了 beforeUpload 自定义上传拦截函数
      if (before && before.then) {
        // before (beforeUpload) 存在并且返回的是一个promise对象
        before.then(processedFile => {
          const fileType = Object.prototype.toString.call(processedFile);
        // Object.prototype.toString.call(A) 返回 '[object A]'
          if (fileType === '[object File]' || fileType === '[object Blob]') {
            if (fileType === '[object Blob]') {
              processedFile = new File([processedFile], rawFile.name, {
                type: rawFile.type
              });
            /**
             * new File(bits,name,[,options]) 构造器创建新的File对象
             *  参数 bits 一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容
             *      name 表示文件名称 或者 文件路径
             *      options 选项对象 包含文件的可选属性
             *      type: DOMString 表示要放到文件中的MIME类型  默认值为 ""
             *      
             * 
             * 
             *  */ 
            }
            for (const p in rawFile) {
              if (rawFile.hasOwnProperty(p)) {
                //hasOwnProperty 判断对象是否包含特定的自身(非继承)属性
                // 去除rawFile原型上的属性,将对象非原型上的属性的赋值给 processedFile
                processedFile[p] = rawFile[p];
              }
            }
            this.post(processedFile);
          } else {
            this.post(rawFile);
          }
        }, () => {
            //如果 beforeUpload 出现了异常 走到了catch 则移除临时文件
          this.onRemove(null, rawFile);
        });
      } else if (before !== false) {
        // 如果beforeUpload函数的返回值不是promise 是boolean值 并且值为true 则 
        // 上传前 文件类型 文件大小 符合 没有进行拦截
        this.post(rawFile);
      } else {
        //  如果beforeUpload函数的返回值不是promise 是boolean值 并且值为false  则
        // 上传前 文件类型 文件大小 不符合 进行拦截
        this.onRemove(null, rawFile);//将临时的文件移除
      }
    },
    abort(file) {
      const { reqs } = this;
      if (file) {
        let uid = file;
        if (file.uid) uid = file.uid;
        if (reqs[uid]) {
          reqs[uid].abort();
        }
      } else {
        Object.keys(reqs).forEach((uid) => {
          if (reqs[uid]) reqs[uid].abort();
          delete reqs[uid];
        });
      }
    },
    post(rawFile) {
      const { uid } = rawFile;// uid 文件的唯一id 用作标识
      const options = {
        headers: this.headers,//请求头
        withCredentials: this.withCredentials,//跨源请求,是否允许携带 cookie
        file: rawFile,//文件数据 上传的文件信息
        data: this.data,// 额外 数据 
        filename: this.name,//文件上传的key值
        action: this.action,//上传的服务器地址
        onProgress: e => {
            // 上传进度的回调函数
          this.onProgress(e, rawFile);
        },
        onSuccess: res => {
            // 上传成功的回调函数
          this.onSuccess(res, rawFile);
          delete this.reqs[uid];// 上
        },
        onError: err => {
            // 上传失败的回调函数
          this.onError(err, rawFile);
          delete this.reqs[uid];
        }
      };
      const req = this.httpRequest(options);
    //调用httpRequest 返回 r
      this.reqs[uid] = req;
      if (req && req.then) {
        //自定义上传文件函数 httpRequest 可能是 promise 类型
        //判断一个函数是否是 promise  判断其 then属性是否存在
        req.then(options.onSuccess, options.onError);
        // promise 有两种 使用方法
        // promise.the(res=>{}).catch(err=>{})
        // promise.then(res=>{},err=>{})
        // 将自定一上传promise函数返回的成功信息或者失败信息 通过回调函数返回给调用组件
      }
    },
    handleClick() {
      if (!this.disabled) {
        //如果禁用,则拦截
        this.$refs.input.value = null;
        // 先将input的value值 置为null 清空
        this.$refs.input.click();//再此触发input框的上船事件
      }  
    },
    handleKeydown(e) {
      if (e.target !== e.currentTarget) return;
    //  防止 事件冒泡 
    //   e.target 触发事件的点击元素
    // e.currentTarget 绑定事件的元素
      if (e.keyCode === 13 || e.keyCode === 32) {
        // 13 enter
        // 32 空格键
        this.handleClick();
      }
    }
  },

  render(h) {
    let {
      handleClick,
      drag,
      name,
      handleChange,
      multiple,
      accept,
      listType,
      uploadFiles,
      disabled,
      handleKeydown
    } = this;
    const data = {
      class: {
        'el-upload': true//render中使用的是 jsx语法,class[key]=true显示类名 class[false] 隐藏类名
      },
      on: {
        click: handleClick,//点击事件
        keydown: handleKeydown//键盘的事件
      }
    };
    data.class[`el-upload--${listType}`] = true;
    // el-upload--${文件类型} 
    return (
      <div {...data} tabindex="0" >
        {/* 将定义的data数据挂载到div上  */}
        {
          drag
            ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
            : this.$slots.default
            // 如果 drag 为true
            // 则 调用 upload-dragger组件,并且 将 调用该组件时传入的组件传入 upload-dragger组件
            //如果 drag 为false
            // 则 展示 调用该组件时传入的组件
        }
        <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
        {/* 
          这里是核心,所有的逻辑都是基于h5的input type = file的表单控件封装的
          type file 表示这个是文件上传类型的控件
          name 表示服务器 
        
        
         */}
      </div>
    );
  }
};
</script>

代码解析

render

    render(h) {
    let {
      handleClick,
      drag,
      name,
      handleChange,
      multiple,
      accept,
      listType,
      uploadFiles,
      disabled,
      handleKeydown
    } = this;
    const data = {
      class: {
        'el-upload': true
      },
      on: {
        click: handleClick,
        keydown: handleKeydown
      }
    };
    data.class[`el-upload--${listType}`] = true;
    return (
      <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>
    );
  }

render解析

div 属性
    const data = {
      class: {
        'el-upload': true
      },
      on: {
        click: handleClick,
        keydown: handleKeydown
      }
    };

     <div {...data} tabindex="0" >
     </div>
  • div的属性值 定义为一个对象,属性是是html支持的属性
  • 在标签上解构,即可使用,这种方法模板和js分离,很是友好
input 属性详解
属性名属性说明
typefile表示文件上传
ref用于后续获取dom
name表单中文件上传控件的名字
multiple表示是否支持多选 truefalse
accept文件类型 常见可选有 ['doc', 'docx', 'xlsx', 'xls', 'txt', 'pdf','jpg','jpeg','png','zip,'rar']
input事件
  • onchange 选择图片触发的事件

props参数

属性名属性说明
actionaction请求路径
name上传时 ,文件流的 key(键) 名称
data上传时附带的额外参数
headers设置上传的请求头
withCredentials支持发送cookie 凭证信息
multiple是否支持多选文件
accept接受上传的文件类型
onStart开始上传的函数
onProgress文件上传时的钩子 进度
onSuccess文件上传成功的钩子函数
onError错误回调函数
beforeUpload上传文件之前的钩子, 一般用于上传之前的拦截处理,如 文件类型,文件大小的拦截等
drag是否启用拖拽上传
onPreview点击已经上传了的文件时的狗子,可用于处理文件预览的逻辑
onRemove文件移除的钩子
fileList上传的文件列表
autoUpload是否在选取文件后立即进行上传
listType文件列表的类型 text/picture/picture-card 待优化里欸包yang
httpRequest覆盖默认的上传行为 可自定一上传的实现
disabled是否禁用
limi文件上传的个数
onExceed文件超出个数限制时的钩子

data

  1. reqs 上传文件过程中的请求对象
  • 请求中 给请求对象 添加一个属性 键值 键值为当前文件的uid
  • 请求成功或者失败 删除reqs[当前属性ui] delete this.reqs[uid]

mehods 方法

handleChange

代码

      handleChange(ev) {
      const files = ev.target.files;//获取到文件上传的files
      if (!files) return;//没有选择文件则拦截
      this.uploadFiles(files);
    },

执行逻辑

  • ev onchange 传入的事件对象
  • ev.target.files获取到上传的文件
  • if(!files) return 用户可能没有上传文件 终止代码即可
  • 如果files存在,就继续调用 uploadFiles

uploadFiles

代码

      uploadFiles(files) {
      if (this.limit && this.fileList.length + files.length > this.limit) {
        /**
         * limit 是限制的最大文件数量
         * fileList 这次点击之前的已经上穿文件
         * files 这次点击传入的文件 是个数组 长度可为1到多
         * 
        */
        // 文件数量大于 限制的最大文件限制数量 则拦截并且 触发 个数超出的钩子函数
        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; }
    //   如果文件数组的长度为0 也就是没有要上传的文件,则拦截
      postFiles.forEach(rawFile => {
            //调用开始上传文件
        this.onStart(rawFile);
        if (this.autoUpload) this.upload(rawFile);
        // 如果 autoUpload 为true 也就是开启了自动上传模式 调用upload方法
      });
    },

执行逻辑

  1. limit 上传文件数量限制参数传入了,则,需要判断是否超出了最大数量
  • 如果传入的数量与现有的文件数量之和大于limit的值,则调用onExceed函数(文件数量超过了限制的钩子)
  1. 如果没有传入multiple则表明是单选,截取第一个数据
  2. 如果文件列表长度为0 则终止代码执行
  3. postFiles数组循环 调用父组件传入的 onStart 函数
  4. 如是开启了自动上传,则调用upload

upload(rawFile)

代码

 upload(rawFile) {
      this.$refs.input.value = null;

      if (!this.beforeUpload) {
        // beforeUpload 用于上传前拦截
        // beforeUpload 的返回值 有两种情况
        // 1 为boolean true或者false
        // 2 返回 promise 函数
        // 此处表示没有传入beforeUpload函数 也就是 没有作上传前的异常拦截处理 直接调用上传到的方法
        return this.post(rawFile);
      }

      const before = this.beforeUpload(rawFile);
      // 能走到这一步 表示组件在调用的时候传入了 beforeUpload 自定义上传拦截函数
      if (before && before.then) {
        // before (beforeUpload) 存在并且返回的是一个promise对象
        before.then(processedFile => {
          const fileType = Object.prototype.toString.call(processedFile);
        // Object.prototype.toString.call(A) 返回 '[object A]'
          if (fileType === '[object File]' || fileType === '[object Blob]') {
            if (fileType === '[object Blob]') {
              processedFile = new File([processedFile], rawFile.name, {
                type: rawFile.type
              });
            /**
             * new File(bits,name,[,options]) 构造器创建新的File对象
             *  参数 bits 一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容
             *      name 表示文件名称 或者 文件路径
             *      options 选项对象 包含文件的可选属性
             *      type: DOMString 表示要放到文件中的MIME类型  默认值为 ""
             *      
             * 
             * 
             *  */ 
            }
            for (const p in rawFile) {
              if (rawFile.hasOwnProperty(p)) {
                //hasOwnProperty 判断对象是否包含特定的自身(非继承)属性
                // 去除rawFile原型上的属性,将对象非原型上的属性的赋值给 processedFile
                processedFile[p] = rawFile[p];
              }
            }
            this.post(processedFile);
          } else {
            this.post(rawFile);
          }
        }, () => {
            //如果 beforeUpload 出现了异常 走到了catch 则移除临时文件
          this.onRemove(null, rawFile);
        });
      } else if (before !== false) {
        // 如果beforeUpload函数的返回值不是promise 是boolean值 并且值为true 则 
        // 上传前 文件类型 文件大小 符合 没有进行拦截
        this.post(rawFile);
      } else {
        //  如果beforeUpload函数的返回值不是promise 是boolean值 并且值为false  则
        // 上传前 文件类型 文件大小 不符合 进行拦截
        this.onRemove(null, rawFile);//将临时的文件移除
      }
    },

执行逻辑

  1. 将input的值置空
  2. 如果!this.beforeUpload 值为true,则没有传入beforeUpload属性
  3. beforeUpload() 方法返回的是promise对象 before
  • 判断是否是promise对象判断是否有then方法
  1. 判断返回的文件类型
  • 如果是Object Blob 就调用new File 构建File对象
  • 将file对象传入poset方法,开始上传
  1. 如果返回的before是对象出现了异常,走到了catch的回调函数,则调用onRemove方法,移除刚选中的临时文件
  2. before 是true 则,文件信息校验通过(问价大小,类型正确)调用post上传方法
  3. 如果before的值传入了,并且既不是 promise对象,也不是等于true,则表明拦截失败,则调用onRemove方法,移除刚选中的临时文件

函数内部执行的逻辑图

 flowchart TD
    A[执行beforeUpload上传的之前的函数返回before]-->C[判断before的类型]
    C-->D{before是否为promise类型}
    C-->E[before的返回值是true]
    C-->F[before的返回值既不是promise也不是true,也就是false]
    G[调用post方法进行文件上传] 
    H[调用remove删除临时选择的文件]
    I{promise是否异常}
    D-->|否|H
    D-->|是|I
    E-->G
    F-->G
    I-->|是|H
    I-->|否|G

具体代码细节

判断是否是promise对象
  • 简单的判断方法是判断是否有then方法
  • Object.prototype.toString.call(obj) =='[object Promise]'
根据blob流创建file对象
  1. new File(bits,name,[,options]) 构造器创建新的File对象
  2. 参数 bits 一个包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 对象的 Array — 或者任何这些对象的组合。这是 UTF-8 编码的文件内容
  • name 表示文件名称 或者 文件路径
  • options 选项对象 包含文件的可选属性
  • type: DOMString 表示要放到文件中的MIME类型 默认值为 ""

hasOwnProperty

  • 用于判断属性是否是对象本身的属性,而非原型上的属性

abort(file)

abort(file) {
      const { reqs } = this;
      // 获取到上传文件的请求对象
      if (file) {
        // abort 取消文件上传,如果传入了指定文件,就取消指定文件的上传
        let uid = file;//ui初始化为file
        if (file.uid) uid = file.uid;//如果file(文件对象的)uid存在,则 赋值为uid
        if (reqs[uid]) {
          // 如果当前文件对象当前uid属性存在,当前文件对应的uid请求存在,则调用abort终止
          reqs[uid].abort()//abort是ajax提供的终止请求的函数
        }
      } else {
        // 如果没有传入指定的文件
        //就用Object.keys循环真个req对象,终止所有的请求,并删除
        Object.keys(reqs).forEach((uid) => {
          if (reqs[uid]) reqs[uid].abort();//终止请求
          delete reqs[uid];//删除对象对应的请求
        });
      }
    },

执行逻辑

  • 用于取消正在上传的文件
  • 如果传入了指定文件,就取消指定文件的上传
  • 如果没有传入指定的文件 就用Object.keys循环真个req对象,终止所有的请求,并删除

具体代码细节

Object.keys

  • 用于循环对象的键值

设计理念

  • 设置一个全局的请求对象
  • 每次发起请求都会给情求对象添加一个请求属性,终止后悔删除这个属性(用delete)

post(rawFile)

  post(rawFile) {
      const { uid } = rawFile;// uid 文件的唯一id 用作标识
      const options = {
        headers: this.headers,//请求头
        withCredentials: this.withCredentials,//跨源请求,是否允许携带 cookie
        file: rawFile,//文件数据 上传的文件信息
        data: this.data,// 额外 数据 
        filename: this.name,//文件上传的key值
        action: this.action,//上传的服务器地址
        onProgress: e => {
            // 上传进度的回调函数
          this.onProgress(e, rawFile);
        },
        onSuccess: res => {
            // 上传成功的回调函数
          this.onSuccess(res, rawFile);
          delete this.reqs[uid];// 上
        },
        onError: err => {
            // 上传失败的回调函数
          this.onError(err, rawFile);
          delete this.reqs[uid];
        }
      };
      const req = this.httpRequest(options);
    //调用httpRequest 返回 r
      this.reqs[uid] = req;
      if (req && req.then) {
        //自定义上传文件函数 httpRequest 可能是 promise 类型
        //判断一个函数是否是 promise  判断其 then属性是否存在
        req.then(options.onSuccess, options.onError);
        // promise 有两种 使用方法
        // promise.the(res=>{}).catch(err=>{})
        // promise.then(res=>{},err=>{})
        // 将自定一上传promise函数返回的成功信息或者失败信息 通过回调函数返回给调用组件
      }
    },

执行逻辑

这个函数是自动上传

  1. 组装好httpRequest需要的所有参数
  2. 调用httpRequest并返回一个promise ,
  3. 将httpRequest返回的promise调用then方法,并将成功的回调函数和失败的回调函数的返回值通过回调函数的方式将成功的信息或者失败的信息抛给父组件 具体代码细节
  • 这个组件使用了大量的回调函数
  • 我们可以将传入的回调函数执行并将参数作为返回值,返回给父组件定义回调函数的地方

点击事件的处理

      handleClick() {
      if (!this.disabled) {
        //如果禁用,则拦截
        this.$refs.input.value = null;
        // 先将input的value值 置为null 清空
        this.$refs.input.click();//再此触发input框的上船事件
      }  
    },
    handleKeydown(e) {
      if (e.target !== e.currentTarget) return;
    //  防止 事件冒泡 
    //   e.target 触发事件的点击元素
    // e.currentTarget 绑定事件的元素
      if (e.keyCode === 13 || e.keyCode === 32) {
        // 13 enter
        // 32 空格键
        this.handleClick();
      }
    }

具体代码细节

  • dom元素绑定了事件后,并不一定需要再界面上点击,也可以通过js触发click事件 if (e.target !== e.currentTarget) return 这段代码表明了,不是事件冒泡触发的事件,而是点击到了当前元素才触发的

render函数

render(h) {
    let {
      handleClick,
      drag,
      name,
      handleChange,
      multiple,
      accept,
      listType,
      uploadFiles,
      disabled,
      handleKeydown
    } = this;
    const data = {
      class: {
        'el-upload': true//render中使用的是 jsx语法,class[key]=true显示类名 class[false] 隐藏类名
      },
      on: {
        click: handleClick,//点击事件
        keydown: handleKeydown//键盘的事件
      }
    };
    data.class[`el-upload--${listType}`] = true;
    // el-upload--${文件类型} 
    return (
      <div {...data} tabindex="0" >
        {/* 将定义的data数据挂载到div上  */}
        {
          drag
            ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger>
            : this.$slots.default
            // 如果 drag 为true
            // 则 调用 upload-dragger组件,并且 将 调用该组件时传入的组件传入 upload-dragger组件
            //如果 drag 为false
            // 则 展示 调用该组件时传入的组件
        }
        <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input>
        {/* 
          这里是核心,所有的逻辑都是基于h5的input type = file的表单控件封装的
          type file 表示这个是文件上传类型的控件
          name 表示服务器 
        
        
         */}
      </div>
    );
  }

代码细节

  • jsx render 可以定义好属性data,然后通过 {...data}将属性数据挂载到div标签上
  • 如果 drag 为true,
  • 则 调用 upload-dragger组件,并且 将 调用该组件时传入的组件传入 upload-dragger组件
  • 如果 drag 为false
  • 则 展示 调用该组件时传入的组件

upload-list

<template>
  <transition-group
    tag="ul"
    :class="[
      'el-upload-list',
      'el-upload-list--' + listType,
      { 'is-disabled': disabled }
    ]"
    name="el-list"
  >
    <li
      v-for="file in files"
      :class="['el-upload-list__item', 'is-' + file.status, focusing ? 'focusing' : '']"
      :key="file.uid"
      tabindex="0"
      @keydown.delete="!disabled && $emit('remove', file)"
      @focus="focusing = true"
      @blur="focusing = false"
      @click="focusing = false"
    >
    <!-- 
      tabindex
      一个普通的div,如果加上tabindex属性,就可以执行focus 和 blur方法
      tabindex 全局属性,表示其是否可以聚焦,以及它是否/在何处参与顺序键盘导航 用tab键控制
      tabindex = 负值 表示元素是可聚焦的,但是不能通过键盘导航来访问到该元素
      tabindex = 0 表示元素是可聚焦的,并且可以通过键盘导航来聚焦到该元素,它的相对顺序是当前处于的dom结构来决定的
      tabindex = 正值 示元素是可聚焦的,并且可以通过键盘导航来访问到该元素,它的先后顺序是由tabindex和dom出现的先后顺序决定的,如果tabindex值一样,则先出现的dom 优先级更高

        @keydown.delete="!disabled && $emit('remove', file)"
        如果没有被禁用,则在点击delete按键的时候,触发 remove 删除方法
    
     -->



      <slot :file="file">
        <img
          class="el-upload-list__item-thumbnail"
          v-if="file.status !== 'uploading' && ['picture-card', 'picture'].indexOf(listType) > -1"
          :src="file.url" alt=""
        >
        <!-- 
          文件状态 不是上传中
          图片类型为图片卡片或者图片类型

        
        
        -->
        <a class="el-upload-list__item-name" @click="handleClick(file)">
          <!-- handleClick 点击的时候 触发预览的操作 -->
          <i class="el-icon-document"></i>{{file.name}}
        </a>
        <label class="el-upload-list__item-status-label">
          <i :class="{
            'el-icon-upload-success': true,
            'el-icon-circle-check': listType === 'text',
            'el-icon-check': ['picture-card', 'picture'].indexOf(listType) > -1
          }"></i>
        </label>
        <i class="el-icon-close" v-if="!disabled" @click="$emit('remove', file)"></i>
        <!-- 没有被禁用的时候才可以 触发删除的事件 -->
        <i class="el-icon-close-tip" v-if="!disabled">{{ t('el.upload.deleteTip') }}</i> <!--因为close按钮只在li:focus的时候 display, li blur后就不存在了,所以键盘导航时永远无法 focus到 close按钮上-->
        <el-progress
          v-if="file.status === 'uploading'"
          :type="listType === 'picture-card' ? 'circle' : 'line'"
          :stroke-width="listType === 'picture-card' ? 6 : 2"
          :percentage="parsePercentage(file.percentage)">
        </el-progress>
        <!-- 进度条的 控制
             文件的状态是 uploading 
             当文件的类型是 list-card 的时候,使用圆形进度条,否则使用 line 直线进度条
             stroke-width 进度条的线的宽度
             percentage 进度的百分比
        -->
        <span class="el-upload-list__item-actions" v-if="listType === 'picture-card'">
          <!-- 文件类型为卡片的时候 
            需要显示两个操作
              预览
              删除
          -->
          <span
            class="el-upload-list__item-preview"
            v-if="handlePreview && listType === 'picture-card'"
            @click="handlePreview(file)"
          >
            <i class="el-icon-zoom-in"></i>
          </span>
          <span
            v-if="!disabled"
            class="el-upload-list__item-delete"
            @click="$emit('remove', file)"
          >
            <i class="el-icon-delete"></i>
          </span>
        </span>
      </slot>
    </li>
  </transition-group>
</template>
<script>
  import Locale from 'element-ui/src/mixins/locale';
  import ElProgress from 'element-ui/packages/progress';

  export default {

    name: 'ElUploadList',

    mixins: [Locale],

    data() {
      return {
        focusing: false
      };
    },
    components: { ElProgress },

    props: {
      files: {// 文件列表
        type: Array,
        default() {
          return [];
        }
      },
      disabled: {//是否禁用 
        type: Boolean,
        default: false
      },
      handlePreview: Function,//预览函数
      listType: String//文件列表展示类型
    },
    methods: {
      parsePercentage(val) {
        return parseInt(val, 10);
        // parseInt 解析一个字符串 并返回指定基数 的十进制整数
      },
      handleClick(file) {
        this.handlePreview && this.handlePreview(file);
        // 如果传入的 handlePreview 文件预览函数存在,则调用 并传入 file 参数
      }
    }
  };
</script>

upload-list参数

属性名属性值
files文件列表
disabled是否禁用
handlePreview文件预览函数
listType文件列表展示类型

具体逻辑

  1. 文件列表展示
  2. 文件列表的预览

技术点

tabindex

一个普通的div,如果加上tabindex属性,就可以执行focus 和 blur方法 tabindex 全局属性,表示其是否可以聚焦,以及它是否/在何处参与顺序键盘导航 用tab键控制 tabindex = 负值 表示元素是可聚焦的,但是不能通过键盘导航来访问到该元素 tabindex = 0 表示元素是可聚焦的,并且可以通过键盘导航来聚焦到该元素,它的相对顺序是当前处于的do结构来决定的 tabindex = 正值 示元素是可聚焦的,并且可以通过键盘导航来访问到该元素,它的先后顺序是由tabinde和dom出现的先后顺序决定的,如果tabindex值一样,则先出现的dom 优先级更高

parseInt 两个参数

  • parseInt的常规用法是加一个参数,此处传入了两个参数
  • 第二个参数是转换的基数,可以是2 进制 10 进制 16进制等

upload-dragger

<template>
  <div
    class="el-upload-dragger"
    :class="{
      'is-dragover': dragover
    }"
    @drop.prevent="onDrop"
    @dragover.prevent="onDragover"
    @dragleave.prevent="dragover = false"
  >
    <!-- 
      ondrop 到指定元素 释放拖动时操作
      ondragover 拖动到此元素时,但是还没有释放
      ondragleave 当离开此元素时
     -->
    <slot></slot>
  </div>
</template>
<script>
  export default {
    name: 'ElUploadDrag',
    props: {
      disabled: Boolean
    },
    inject: {
      uploader: {
        default: ''
      }
    },
    data() {
      return {
        dragover: false//是否移入
      };
    },
    methods: {
      onDragover() {
        if (!this.disabled) {
          // 在没有禁用的情况下,拖动到此元素上 dragover设置为true
          this.dragover = true;
        }
      },
      onDrop(e) {
        if (this.disabled || !this.uploader) return;
        // 如果是 禁用了,或者 祖籍组件没有依赖注入 uploader (不是在上传组件内使用的)则 终止代码执行
        // this.uploader 是upload/index 组件的内部实例 ,能访问到 upload/index组件内的所有方法和属性等
        const accept = this.uploader.accept;
        // 限制的文件类型
        this.dragover = false;
        if (!accept) {
          // 没有传入限制的文件类型
          this.$emit('file', e.dataTransfer.files);
          // 直接返回文件
          // 并终止
          return;
        }
        this.$emit('file', [].slice.call(e.dataTransfer.files).filter(file => {
          const { type, name } = file;
          const extension = name.indexOf('.') > -1
            ? `.${ name.split('.').pop() }`
            : '';
            //获取文件的后缀名 
          const baseType = type.replace(/\/.*$/, '');
          return accept.split(',')
            .map(type => type.trim())
            .filter(type => type)
            .some(acceptedType => {
              if (/\..+$/.test(acceptedType)) {
                return extension === acceptedType;
              }
              if (/\/\*$/.test(acceptedType)) {
                return baseType === acceptedType.replace(/\/\*$/, '');
              }
              if (/^[^\/]+\/[^\/]+$/.test(acceptedType)) {
                return type === acceptedType;
              }
              return false;
            });
            // 这个一大串的正则处理,是为了将文件后缀名与传入的文件名类型做对比 返回值是 boolean
            // [].map.filter.some 是数组的链式调用 每次调用都会将结果作为返回值供下次调用
        }));
      }
    }
  };
</script>

upload-dragger 参数

属性名属性值
disabled是否禁用
inject 中的 uploader是父级组件或者祖籍组件传入的实例,此处是父组件传入的
dragover是否移入

技术点

div上的拖拽事件

  • ondrop 到指定元素 释放拖动时操作
  • ondragover 拖动到此元素时,但是还没有释放
  • ondragleave 当离开此元素时

index 上传文件的入口文件

<script>
import UploadList from './upload-list';
import Upload from './upload';
import ElProgress from 'element-ui/packages/progress';
import Migrating from 'element-ui/src/mixins/migrating';

function noop() { }

export default {
  name: 'ElUpload',

  mixins: [Migrating],

  components: {
    ElProgress,
    UploadList,
    Upload
  },

  provide() {
    return {
      uploader: this//将 自身 挂载到provide上 ,供子组件访问当前组件的实例
    };
  },

  inject: {
    // inject 和 provide 搭配 , 在父级父级或者祖级使用provide 提供一个变量,然后在子孙组件获取或者调用
    elForm: {
      default: ''//
    }
  },

  props: {
    action: {//请求路径传递给ajax的默认请求路径
      type: String,
      required: true
    },
    headers: {//请求头,传递给ajax的请求头参数
      type: Object,
      default() {
        return {};
      }
    },
    data: Object,//上传时附带的额外参数
    multiple: Boolean,//是否开启多选文件
    name: {//上传文件时的 文件名字段
      type: String,
      default: 'file'
    },
    drag: Boolean,//上传时是否开启拖拽上传
    dragger: Boolean,
    withCredentials: Boolean,//支持发送cookie 凭证信息
    showFileList: {//是否显示文件上传列表
      type: Boolean,
      default: true
    },
    accept: String,//接受上传的文件类型
    type: {//这个字段,没有用到
      type: String,
      default: 'select'
    },
    beforeUpload: Function,//上传文件之前的钩子, 一般用于上传之前的拦截处理,如 文件类型,文件大小的拦截等
    beforeRemove: Function,//删除文件之前的钩子函数
    onRemove: {//文件删除的钩子函数
      type: Function,
      default: noop
    },
    onChange: {//文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用
      type: Function,
      default: noop
    },
    onPreview: {//点击文件列表中已上传的文件时的钩子
      type: Function
    },
    onSuccess: {//上传成功时的狗子函数
      type: Function,
      default: noop
    },
    onProgress: {//文件上传的进度钩子函数
      type: Function,
      default: noop
    },
    onError: {//文件上传失败时的钩子函数
      type: Function,
      default: noop
    },
    fileList: {//上传的文件列表
      type: Array,
      default() {
        return [];
      }
    },
    autoUpload: {//是否开启自动上传
      type: Boolean,
      default: true
    },
    listType: {//文件列表的类型
      type: String,
      default: 'text' // text,picture,picture-card
    },
    httpRequest: Function,//覆盖默认的上传行为,可自定以上传的实现
    disabled: Boolean,//是否禁用
    limit: Number,//最大允许上传个数
    onExceed: {//文件超出个数限制时的钩子
      type: Function,
      default: noop
    }
  },

  data() {
    return {
      uploadFiles: [],//文件上传过程中暂存的文件列表,用于文件列表的显示和逻辑的处理
      dragOver: false,//没有用到
      draging: false,//没有用到
      tempIndex: 1
      // 定义一个临时的index字段 结合Date.now() 形成一个唯一的uid
      // 时间戳本身就是唯一的,但是为了防止同时并发 外 加一个自增的字段
    }
  },

  computed: {
    uploadDisabled() {
      return this.disabled || (this.elForm || {}).disabled;
      // 组件禁用 
      // 如果组件传入了 disabled 参数则禁用该上传组件
      // 或者 调用的表单组件设置了整体的disabled属性
      // (this.elForm || {}).disabled 为了防止报错 this.elForm 可能是null
    }
  },

  watch: {
    listType(type) {
      if (type === 'picture-card' || type === 'picture') {
        this.uploadFiles = this.uploadFiles.map(file => {
          if (!file.url && file.raw) {
            try {
              file.url = URL.createObjectURL(file.raw);
              // file.raw 是上传文件信息
              // URL.createObjectURL 
              // 参数 用于创建URL的file对象、blob对象或者MediaSource
              // 返回值 获取当前文件的一个内存URL 返回的是一段base64的字符串
              // 没有fileUrl 则 调用URL.createObjectURL 将file的raw属性 转换成 url
            } catch (err) {
              // 如果出现异常,则抛出异常
              console.error('[Element Error][Upload]', err);    
            }
          }
          return file;
        });
      }
    },
    fileList: {//文件列表
      immediate: true,//immediate 表示立即触发
      handler(fileList) {
        this.uploadFiles = fileList.map(item => {
          item.uid = item.uid || (Date.now() + this.tempIndex++);
          // 文件的唯一标识 id 使用时间戳 + 自增索引
          item.status = item.status || 'success';//由于是列表回显的 所以如果有status 就使用传入的 否则使用 默认的 success
          return item;
        });
      }
    }
  },

  methods: {
    handleStart(rawFile) {
      rawFile.uid = Date.now() + this.tempIndex++;
      // 文件的唯一标识 id 使用时间戳 + 自增索引  
      //  每次执行都需要调用,故写了多份代码,可封装成一个函数执行调用 返回唯一uid
   /*    const getUid = () => {
        return Date.noe() + Math.random() + this.tempIndex++
      } */
      let file = {
        status: 'ready',//文件状态,由于是开始上传故,直接写死状态值为 ready
        name: rawFile.name,//name 文件名称
        size: rawFile.size,//文件的大小 单位是B(比特) 1KB = 1024 * 1024 
        percentage: 0,
        uid: rawFile.uid,//文件的uid 唯一标识
        raw: rawFile//上传的文件
      };
      /**
       * uploadFiles 文件上传过程中暂存的文件列表
       *  status 
       *      ready 准备上传
       *      uploading 正在上传
       *      success 上传成功
       *      error 上传失败
       * 
       * 
        */
      if (this.listType === 'picture-card' || this.listType === 'picture') {
        // 如果  listType 的值为 picture-card(带卡片样式的图片)或者 picture (图片) 则需要显示图片 将文件信息中的 文件流 raw 调用 URL.createObjectURL 转换成 base64 浏览器可显示的
        try {
          file.url = URL.createObjectURL(rawFile);
        } catch (err) {
          console.error('[Element Error][Upload]', err);
          return;
        }
      }

      this.uploadFiles.push(file);//将组装好的file添加到 问价上传过程中的暂存列表
      this.onChange(file, this.uploadFiles);//并调用onChange 函数将当前上传的文件信息 file 和 文件上传过程中的暂存列表 uploadFiles 返回给 调用的父组件
    },
    handleProgress(ev, rawFile) {
      // ev 上传的事件对象 
      // rawFile 文件信息
      const file = this.getFile(rawFile);
      // 文件信息
      this.onProgress(ev, file, this.uploadFiles);
      // 调用onProgress 传入文件对象 临时的上传的文件列表
      file.status = 'uploading';
      // 文件的状态设置为 uploading
      file.percentage = ev.percent || 0;
      // 文件的进度,
    },
    handleSuccess(res, rawFile) {
      const file = this.getFile(rawFile);
      //开始上传的时候,会将文件信息添加到 临时的文件列表 
      //会将选择的文件添加到uploadFiles中,只是文件的status(状态不对) 由start(开始) 变为uploading(上传中) 改为succcess(成功)
      if (file) {
        file.status = 'success';
        file.response = res;

        this.onSuccess(res, file, this.uploadFiles);
        // 成功之后,将响应对象,当前文件信息,以及文件上传列表都返回给调用组件
        this.onChange(file, this.uploadFiles);
        // 由于文件的状态 由 uploading 改为了 success 故需要调用change函数更新文件的信息
      }
    },
    handleError(err, rawFile) {
      const file = this.getFile(rawFile);
      // 根据错误文件的uid,从临时的文件上传列表中获取到 完整的文件信息
      const fileList = this.uploadFiles;
      // 文件列表 
      file.status = 'fail';
      //将文件的状态改为 fail,文件上传失败
      fileList.splice(fileList.indexOf(file), 1);
      // 将文件删除 splice(文件索引,删除的数量)
      this.onError(err, file, this.uploadFiles);
      // 调用抛出异常的钩子函数
      // err 异常信息
      // file 删除的文件信息
      // this.uploadFiles 由于是引用数据类型,是删除文件后的文件列表
      this.onChange(file, this.uploadFiles);
      // 由于文件状态改变了,调用change函数
    },
    handleRemove(file, raw) {
      if (raw) {
        file = this.getFile(raw);
        // 获取到文件
      }
      let doRemove = () => {//定义一个局部的删除函数,方便重复调用
        this.abort(file);
        //删除文件之前,先abort终止文件的上传
        let fileList = this.uploadFiles;
        fileList.splice(fileList.indexOf(file), 1);
        // 将当前文件从文件列表中删除
        this.onRemove(file, fileList);
        // 并调用父组件传入的 删除钩子函数 onRemove
      };

      if (!this.beforeRemove) {
        // 判断是否有传入删除前的钩子拦截函数 
        // 如果没有,则直接调用删除的 doRemove函数
        doRemove();
      } else if (typeof this.beforeRemove === 'function') {
        //如果传入了 beforeRemove参数,并且是函数类型,则 调用beforeRemove 函数 并传入 file(当前文件) 和 上传的文件列表uploadFiles
        const before = this.beforeRemove(file, this.uploadFiles);
        if (before && before.then) {
          // 如果 beforeRemove的返回值存在,并且有then方法
          // 则 表明是一个Promise函数
          before.then(() => {
            // 调用then方法 
            // 并执行doRemove函数
            doRemove();
          }, noop);
          // catch方法是一个空的函数,不需要执行
        } else if (before !== false) {
          doRemove();
          // ruguo  beforeRemove的返回值不是promise函数,并且值为true
          // 则 可以直接删除
        }
      }
    },
    getFile(rawFile) {
      let fileList = this.uploadFiles;
      let target;
      fileList.every(item => {
        target = rawFile.uid === item.uid ? item : null;
        // 如果循环中的uid等于传入的数据项的uid 则 将数据项赋值给target
        // return !target; 这段代码没有用  fileList.every 函数没有被接收,故 return !target 这段代码是没有意义的
        // 合理的操作应该使用find
      });
      return target;//将查到的数据返回
    },
    abort(file) {
      this.$refs['upload-inner'].abort(file);
      // 使用 $refs['upload-inner'].abort 获取到 upload-inner 组件,然后调用abort函数 并传入文件 file
      // 终止上传
    },
    clearFiles() {
      this.uploadFiles = [];
      // 清空文件上传
    },
    submit() {
      // 触发手动上传的操作
      this.uploadFiles
        .filter(file => file.status === 'ready')//过滤出来 文件状态是 ready的数据
        .forEach(file => {//然后 循环调用 上传业务组件里的 upload 方法 并且传入file.raw
          this.$refs['upload-inner'].upload(file.raw);
        });
    },
    getMigratingConfig() {
      return {
        props: {
          'default-file-list': 'default-file-list is renamed to file-list.',
          'show-upload-list': 'show-upload-list is renamed to show-file-list.',
          'thumbnail-mode': 'thumbnail-mode has been deprecated, you can implement the same effect according to this case: http://element.eleme.io/#/zh-CN/component/upload#yong-hu-tou-xiang-shang-chuan'
        }
      };
    }
  },
  mounted() {
    debugger
  },
  beforeDestroy() {
    // 组件销毁的时候
    this.uploadFiles.forEach(file => {
      if (file.url && file.url.indexOf('blob:') === 0) {
        // URL.revokeObjectURL() 静态方法用来释放一个之前已经存在的,通过调用 URL.createObjectURL() 创建的 URL对象
        URL.revokeObjectURL(file.url);
        // 调用这个方法之后,让浏览器知道不用在内存中继续保留对这个文件的引用了
      }
    });
  },

  render(h) {
    let uploadList;

    if (this.showFileList) {
      uploadList = (
        <UploadList
          disabled={this.uploadDisabled}
          listType={this.listType}
          files={this.uploadFiles}
          on-remove={this.handleRemove}
          handlePreview={this.onPreview}>
          {
            (props) => {
              if (this.$scopedSlots.file) {
              //判断 UploadList 组件是否有file 插槽,
              //如果有就调用这个file插槽并且传入参数
                return this.$scopedSlots.file({
                  file: props.file
                });
              }
            }
          }
        </UploadList>
      );
    }
    /**
     *UploadList 参数说明
     * disabled 是否禁用 
     * listType 列表类型
     * files 文件列表
     * onRemove 文件删除触发的函数
     * onPreview 
     * *** */  
    /**
     * scopedSlots 是作用域插槽
     * 他与slot-scope 作用都是一样的 
     * 只不过
     *  slot-scope 是模板语法   一般用于 template 中
     *  scopedSlots 则是编程语法 一般用于 render函数 中
     *  */  
    const uploadData = {
      props: {
        type: this.type,//type 组件中并没有用到
        drag: this.drag,//是否开启拖拽
        action: this.action,//文件上传的服务器地址
        multiple: this.multiple,//是否支持多文件上传
        'before-upload': this.beforeUpload,//文件上传之前的逻辑判断函数,根据beforeUpload的返回值判断是可以继续上传,注意返回值可以是promise
        'with-credentials': this.withCredentials,//是否允许携带cookie
        headers: this.headers,//请求头
        name: this.name,//文件上传的key值
        data: this.data,//文件上传中的额外数据
        accept: this.accept,//文件上传接收的类型
        fileList: this.uploadFiles,//文件上传的文件列表
        autoUpload: this.autoUpload,//是否开启自动上传,选择完文件后立即上传到服务器
        listType: this.listType,// 文件列表的展示类型 text,picture,picture-card
        disabled: this.uploadDisabled,//是否禁用上传,如果表单里填写了disabled或者当前组件传入了disabled就禁用 
        limit: this.limit,//文件上传过程中的数量限制
        'on-exceed': this.onExceed,//文件数量超出报错触发的钩子函数
        'on-start': this.handleStart,//开始上传
        'on-progress': this.handleProgress,//文件上传进度监控
        'on-success': this.handleSuccess,//上传成功的回调函数
        'on-error': this.handleError,//文件上传报错的回调函数
        'on-preview': this.onPreview,//预览
        'on-remove': this.handleRemove,//删除时触发的钩子函数
        'http-request': this.httpRequest//自定义的http请求方法
      },
      ref: 'upload-inner'//ref操作dom的引用
    };

    const trigger = this.$slots.trigger || this.$slots.default;
    // trigger插槽时  获取到 组件调时 触发文件选择框的插槽
// default插槽是 默认插槽,就是组件调用时组件标签内包裹的内容,或者标签伤加了 slot = "default"
// 此处的逻辑是,如果传入了 trigger插槽,就使用trigger插槽,如果没有使用trigger就用默认的插槽
    const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
    // 使用 {...uploadData} 对象解构的形式 将 配置好的uploadData数据动态的设置为upload标签的属性 并且传入到 组件upload 
    return (
      <div>
        {this.listType === 'picture-card' ? uploadList : ''}
        {/* 在listType 值为picutre-card 的时候 优先使用 uploadList 展示图片列表
          由于 render函数中没有v-if v-show 故直接使用三元表达式来展示对应的组件
          */}
        {
          this.$slots.trigger//如果 trigger插槽存在,就渲染 uploadComponent和default(默认插槽),如果不存在就渲染 uploadComponent
            ? [uploadComponent, this.$slots.default]
            : uploadComponent
        }
        {this.$slots.tip}
        {/* 提示信息 */}
        {this.listType !== 'picture-card' ? uploadList : ''}
        {/*  在listType值 不为 picutre-card 的时候 最后使用 uploadList 展示图片列表*/}
      </div>
    );
  }
};
</script>

props参数

属性名属性说明
actionaction请求路径
headers设置上传的请求头
data上传时附带的额外参数
multiple是否支持多选文件
name上传时 ,文件流的 key(键) 名称
drag是否启用拖拽上传
withCredentials支持发送cookie 凭证信息
accept接受上传的文件类型
beforeUpload上传文件之前的钩子, 一般用于上传之前的拦截处理,如 文件类型,文件大小的拦截等
beforeRemvoe文件删除的狗钩子
onChange文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用
onPreview点击已经上传了的文件时的狗子,可用于处理文件预览的逻辑
onSuccess文件上传成功的钩子函数
onProgress文件上传时的钩子 进度
onError错误回调函数
fileList上传的文件列表
autoUpload是否在选取文件后立即进行上传
onStart开始上传的函数
listType文件列表的类型 text/picture/picture-card 待优化里欸包yang
onRemove文件移除的钩子
httpRequest覆盖默认的上传行为 可自定一上传的实现
disabled是否禁用
limi文件上传的个数
onExceed文件超出个数限制时的钩子

data参数

属性名属性说明
uploadFiles文件上传过程中暂存的文件列表,用于文件列表的显示和逻辑的处理
tempIndex定义一个临时的index字段 结合Date.now() 形成一个唯一的uid
时间戳本身就是唯一的,但是为了防止同时并发 外 加一个自增的字段

computed uploadDisabled 上传禁用处理逻辑

  • 当前上传组件具有禁用disabled参数
  • 或者上传组件所在的表单设置了disabled属性
  computed: {
    uploadDisabled() {
      return this.disabled || (this.elForm || {}).disabled;
      // 组件禁用 
      // 如果组件传入了 disabled 参数则禁用该上传组件
      // 或者 调用的表单组件设置了整体的disabled属性
      // (this.elForm || {}).disabled 为了防止报错 this.elForm 可能是null
    }
  },

watch listType fileList

watch: {
    listType(type) {
      if (type === 'picture-card' || type === 'picture') {
        this.uploadFiles = this.uploadFiles.map(file => {
          if (!file.url && file.raw) {
            try {
              file.url = URL.createObjectURL(file.raw);
              // file.raw 是上传文件信息
              // URL.createObjectURL 
              // 参数 用于创建URL的file对象、blob对象或者MediaSource
              // 返回值 获取当前文件的一个内存URL 返回的是一段base64的字符串
              // 没有fileUrl 则 调用URL.createObjectURL 将file的raw属性 转换成 url
            } catch (err) {
              // 如果出现异常,则抛出异常
              console.error('[Element Error][Upload]', err);    
            }
          }
          return file;
        });
      }
    },
    fileList: {//文件列表
      immediate: true,//immediate 表示立即触发
      handler(fileList) {
        this.uploadFiles = fileList.map(item => {
          item.uid = item.uid || (Date.now() + this.tempIndex++);
          // 文件的唯一标识 id 使用时间戳 + 自增索引
          item.status = item.status || 'success';//由于是列表回显的 所以如果有status 就使用传入的 否则使用 默认的 success
          return item;
        });
      }
    }
  },

技术点补充

  1. URL.createObjectURL
  • 参数 用于创建URL的file对象、blob对象或者MediaSource
  • 返回值 获取当前文件的一个内存URL 返回的是一段base64的字符串
  • 没有fileUrl 则 调用URL.createObjectURL 将file的raw属性 转换成 url
  1. URL.revokeObjectURL
  • 调用这个方法之>后,让浏览器知道不用在内存中继续保留对这个文件的引用了
  1. 给文件信息加上唯一标识uid 时间戳 + 自增索引
  2. upload 文件上传中的四个状态
  • ready 准备上传
  • uploading 正在上传
  • success 上传成功
  • error 上传失败

methods 方法

handleStart(rawFile)

handleStart(rawFile) {
      rawFile.uid = Date.now() + this.tempIndex++;
      // 文件的唯一标识 id 使用时间戳 + 自增索引  
      //  每次执行都需要调用,故写了多份代码,可封装成一个函数执行调用 返回唯一uid
   /*    const getUid = () => {
        return Date.noe() + Math.random() + this.tempIndex++
      } */
      let file = {
        status: 'ready',//文件状态,由于是开始上传故,直接写死状态值为 ready
        name: rawFile.name,//name 文件名称
        size: rawFile.size,//文件的大小 单位是B(比特) 1KB = 1024 * 1024 
        percentage: 0,
        uid: rawFile.uid,//文件的uid 唯一标识
        raw: rawFile//上传的文件
      };
      /**
       * uploadFiles 文件上传过程中暂存的文件列表
       *  status 
       *      ready 准备上传
       *      uploading 正在上传
       *      success 上传成功
       *      error 上传失败
       * 
       * 
        */
      if (this.listType === 'picture-card' || this.listType === 'picture') {
        // 如果  listType 的值为 picture-card(带卡片样式的图片)或者 picture (图片) 则需要显示图片 将文件信息中的 文件流 raw 调用 URL.createObjectURL 转换成 base64 浏览器可显示的
        try {
          file.url = URL.createObjectURL(rawFile);
        } catch (err) {
          console.error('[Element Error][Upload]', err);
          return;
        }
      }

      this.uploadFiles.push(file);//将组装好的file添加到 问价上传过程中的暂存列表
      this.onChange(file, this.uploadFiles);//并调用onChange 函数将当前上传的文件信息 file 和 文件上传过程中的暂存列表 uploadFiles 返回给 调用的父组件
    },
逻辑分析
  1. 获取到传入的参数并生成uid,设置一个文件对象(并将装态设置为ready)
  2. 如果listType的类型是picture-card如果 listType 的值为 picture-card(带卡片样式的图片)或者 picture (图片) 则需要显示图片 将文件信息中的 文件流 raw 调用 URL.createObjectURL 转换成 base64 浏览器可显示的
技点补充说明
  1. uid 生成
  • 文件的唯一标识 id 使用时间戳 + 自增索引
  • 每次执行都需要调用,故写了多份代码,可封装成一个函数执行调用 返回唯一uid
  • 下面是更靠谱的方法 时间戳 + 随机数 + 自增索引
const getUid = () => {
    return Date.now(0,100000) + Math.random() + this.tempIndex++
  }
  1. try catch 捕获异常
  try{
    //业务逻辑
  }catch(err){
    // 如果业务逻辑里面 报错了,会走到这里
  }finally{
    // 结束时调用,业务逻辑无论有没有报错都会走这边
  }

handleProgress 进度的逻辑处理函数

    handleProgress(ev, rawFile) {
      // ev 上传的事件对象 
      // rawFile 文件信息
      const file = this.getFile(rawFile);
      // 文件信息
      this.onProgress(ev, file, this.uploadFiles);
      // 调用onProgress 传入文件对象 临时的上传的文件列表
      file.status = 'uploading';
      // 文件的状态设置为 uploading
      file.percentage = ev.percent || 0;
      // 文件的进度,
    },
逻辑说明
  1. 调用getFile从文件列表中获取到完整的文件信息
  2. 触发传入的回调函数将 事件对象 当前文件信息 以及 暂存的文件列表抛出给调用组件
  3. 将文件的状态为 uploading 上传中
getFile(rawFile) 根据uid获取文件信息
    getFile(rawFile) {
      let fileList = this.uploadFiles;
      let target;
      fileList.every(item => {
        target = rawFile.uid === item.uid ? item : null;
        // 如果循环中的uid等于传入的数据项的uid 则 将数据项赋值给target
        // return !target; 这段代码没有用  fileList.every 函数没有被接收,故 return !target 这段代码是没有意义的
        // 合理的操作应该使用find
      });
      return target;//将查到的数据返回
    },
分析

如果循环中的uid等于传入的数据项的uid 则 将数据项赋值给target

纠错
  1. 由于是 查找某一,故应该使用find而不是every
  2. fileList.every 没有被接收,故return !target其实也没有意义

abort(file) 取消文件上传

   abort(file) {
      this.$refs['upload-inner'].abort(file);
      // 使用 $refs['upload-inner'].abort 获取到 upload-inner 组件,然后调用abort函数 并传入文件 file
      // 终止上传
    },
分析

使用$refs 获取到子组件的实例 调用子组件的abort方法并传入file取消指定文件的上传

触发手动上传的操作
    submit() {
      // 触发手动上传的操作
      this.uploadFiles
        .filter(file => file.status === 'ready')//过滤出来 文件状态是 ready的数据
        .forEach(file => {//然后 循环调用 上传业务组件里的 upload 方法 并且传入file.raw
          this.$refs['upload-inner'].upload(file.raw);
        });
    },
分析
  • 这是触发手动上传的操作
  • 过滤出来 文件状态是 ready的数据
  • 然后循环调用子组件的upload方法并传入对应的文件信息

beforeDestroy

  beforeDestroy() {
    // 组件销毁的时候
    this.uploadFiles.forEach(file => {
      if (file.url && file.url.indexOf('blob:') === 0) {
        // URL.revokeObjectURL() 静态方法用来释放一个之前已经存在的,通过调用 URL.createObjectURL() 创建的 URL对象
        URL.revokeObjectURL(file.url);
        // 调用这个方法之后,让浏览器知道不用在内存中继续保留对这个文件的引用了
      }
    });
  },

逻辑分析

  • 组件销毁的时候
  • 判断文件中的url是否是以 blob开头的
  • 如果是就调用 URL.revokeObjectURL 让浏览器知道不用在内存中继续保留对这个文件的引用了

render(h)

render(h) {
    let uploadList;

    if (this.showFileList) {
      uploadList = (
        <UploadList
          disabled={this.uploadDisabled}
          listType={this.listType}
          files={this.uploadFiles}
          on-remove={this.handleRemove}
          handlePreview={this.onPreview}>
          {
            (props) => {
              if (this.$scopedSlots.file) {
              //判断 UploadList 组件是否有file 插槽,
              //如果有就调用这个file插槽并且传入参数
                return this.$scopedSlots.file({
                  file: props.file
                });
              }
            }
          }
        </UploadList>
      );
    }
    /**
     *UploadList 参数说明
     * disabled 是否禁用 
     * listType 列表类型
     * files 文件列表
     * onRemove 文件删除触发的函数
     * onPreview 
     * *** */  
    /**
     * scopedSlots 是作用域插槽
     * 他与slot-scope 作用都是一样的 
     * 只不过
     *  slot-scope 是模板语法   一般用于 template 中
     *  scopedSlots 则是编程语法 一般用于 render函数 中
     *  */  
    const uploadData = {
      props: {
        type: this.type,//type 组件中并没有用到
        drag: this.drag,//是否开启拖拽
        action: this.action,//文件上传的服务器地址
        multiple: this.multiple,//是否支持多文件上传
        'before-upload': this.beforeUpload,//文件上传之前的逻辑判断函数,根据beforeUpload的返回值判断是可以继续上传,注意返回值可以是promise
        'with-credentials': this.withCredentials,//是否允许携带cookie
        headers: this.headers,//请求头
        name: this.name,//文件上传的key值
        data: this.data,//文件上传中的额外数据
        accept: this.accept,//文件上传接收的类型
        fileList: this.uploadFiles,//文件上传的文件列表
        autoUpload: this.autoUpload,//是否开启自动上传,选择完文件后立即上传到服务器
        listType: this.listType,// 文件列表的展示类型 text,picture,picture-card
        disabled: this.uploadDisabled,//是否禁用上传,如果表单里填写了disabled或者当前组件传入了disabled就禁用 
        limit: this.limit,//文件上传过程中的数量限制
        'on-exceed': this.onExceed,//文件数量超出报错触发的钩子函数
        'on-start': this.handleStart,//开始上传
        'on-progress': this.handleProgress,//文件上传进度监控
        'on-success': this.handleSuccess,//上传成功的回调函数
        'on-error': this.handleError,//文件上传报错的回调函数
        'on-preview': this.onPreview,//预览
        'on-remove': this.handleRemove,//删除时触发的钩子函数
        'http-request': this.httpRequest//自定义的http请求方法
      },
      ref: 'upload-inner'//ref操作dom的引用
    };

    const trigger = this.$slots.trigger || this.$slots.default;
    // trigger插槽时  获取到 组件调时 触发文件选择框的插槽
// default插槽是 默认插槽,就是组件调用时组件标签内包裹的内容,或者标签伤加了 slot = "default"
// 此处的逻辑是,如果传入了 trigger插槽,就使用trigger插槽,如果没有使用trigger就用默认的插槽
    const uploadComponent = <upload {...uploadData}>{trigger}</upload>;
    // 使用 {...uploadData} 对象解构的形式 将 配置好的uploadData数据动态的设置为upload标签的属性 并且传入到 组件upload 
    return (
      <div>
        {this.listType === 'picture-card' ? uploadList : ''}
        {/* 在listType 值为picutre-card 的时候 优先使用 uploadList 展示图片列表
          由于 render函数中没有v-if v-show 故直接使用三元表达式来展示对应的组件
          */}
        {
          this.$slots.trigger//如果 trigger插槽存在,就渲染 uploadComponent和default(默认插槽),如果不存在就渲染 uploadComponent
            ? [uploadComponent, this.$slots.default]
            : uploadComponent
        }
        {this.$slots.tip}
        {/* 提示信息 */}
        {this.listType !== 'picture-card' ? uploadList : ''}
        {/*  在listType值 不为 picutre-card 的时候 最后使用 uploadList 展示图片列表*/}
      </div>
    );
  }

技术点详解

  1. scopedSlots是什么
  • scopedSlots 是作用域插槽
  • 他与slot-scope 作用都是一样的
  • slot-scope 是模板语法 一般用于 template 中
  • scopedSlots 则是编程语法 一般用于 render函数 中
  1. render中是jsx语法,没有v-if操作,只能用三元表达式

结尾致谢

  • 感谢您百忙之中阅读我写的博客,希望能对您有所帮助
  • 由于写代码的初心是让即使是零基础的朋友能看得懂源码,故有些地方可能写的过于详细,希望能理解
  • 后期会继续更新有关elment ui的源码分析,如果有兴趣请关注下我,以便于您更好的学习
  • 如果感觉有帮助,帮忙点个赞,谢谢