欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

element-ui Upload 上传组件源码分析整理笔记(十四)

程序员文章站 2022-07-21 13:42:17
简单写了部分注释,upload dragger.vue(拖拽上传时显示此组件)、upload list.vue(已上传文件列表)源码暂未添加多少注释,等有空再补充,先记下来... index.vue upload.vue pythod import ajax from './ajax'; impor ......

简单写了部分注释,upload-dragger.vue(拖拽上传时显示此组件)、upload-list.vue(已上传文件列表)源码暂未添加多少注释,等有空再补充,先记下来...

index.vue

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

  inject: {
    elform: {
      default: ''
    }
  },

  props: {
    action: { //必选参数,上传的地址
      type: string,
      required: true
    },
    headers: { //设置上传的请求头部
      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, //接受上传的文件类型(thumbnail-mode 模式下此参数无效)
    type: {
      type: string,
      default: 'select'
    },
    beforeupload: function, //上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 promise 且被 reject,则停止上传。
    beforeremove: function, //删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 promise 且被 reject,则停止上传。
    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: { //上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}]
      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
    };
  },

  computed: {
    uploaddisabled() {
      return this.disabled || (this.elform || {}).disabled;
    }
  },

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

  methods: {
    //文件上传之前调用的方法
    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);
    },
    handleprogress(ev, rawfile) {
      const file = this.getfile(rawfile);
      this.onprogress(ev, file, this.uploadfiles);
      file.status = 'uploading';
      file.percentage = ev.percent || 0;
    },
    //文件上传成功后改用该方法,在该方法中调用用户设置的on-success和on-change方法,并将对应的参数传递出去
    handlesuccess(res, rawfile) {
      const file = this.getfile(rawfile);

      if (file) {
        file.status = 'success';
        file.response = res;

        this.onsuccess(res, file, this.uploadfiles);
        this.onchange(file, this.uploadfiles);
      }
    },
    //文件上传失败后改用该方法,在该方法中调用用户设置的on-error和on-change方法,并将对应的参数传递出去
    handleerror(err, rawfile) {
      const file = this.getfile(rawfile);
      const filelist = this.uploadfiles;

      file.status = 'fail';

      filelist.splice(filelist.indexof(file), 1);

      this.onerror(err, file, this.uploadfiles);
      this.onchange(file, this.uploadfiles);
    },
    //文件列表移除文件时调用该方法
    handleremove(file, raw) {
      if (raw) {
        file = this.getfile(raw);
      }
      let doremove = () => {
        this.abort(file);
        let filelist = this.uploadfiles;
        filelist.splice(filelist.indexof(file), 1);
        this.onremove(file, filelist);
      };

      if (!this.beforeremove) {
        doremove();
      } else if (typeof this.beforeremove === 'function') {
        const before = this.beforeremove(file, this.uploadfiles);
        if (before && before.then) {
          before.then(() => {
            doremove();
          }, noop);
        } else if (before !== false) {
          doremove();
        }
      }
    },
    getfile(rawfile) {
      let filelist = this.uploadfiles;
      let target;
      filelist.every(item => {
        target = rawfile.uid === item.uid ? item : null;
        return !target;
      });
      return target;
    },
    abort(file) {
      this.$refs['upload-inner'].abort(file);
    },
    clearfiles() {
      this.uploadfiles = [];
    },
    submit() {
      this.uploadfiles
        .filter(file => file.status === 'ready')
        .foreach(file => {
          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'
        }
      };
    }
  },

  beforedestroy() {
    this.uploadfiles.foreach(file => {
      if (file.url && file.url.indexof('blob:') === 0) {
        url.revokeobjecturl(file.url);
      }
    });
  },

  render(h) {
    let uploadlist;
    //如果用户设置showfilelist为true,则显示上传文件列表
    if (this.showfilelist) {
      uploadlist = (
        <uploadlist
          disabled={this.uploaddisabled}
          listtype={this.listtype}
          files={this.uploadfiles}
          on-remove={this.handleremove}
          handlepreview={this.onpreview}>
        </uploadlist>
      );
    }

    const uploaddata = {
      props: {
        type: this.type,
        drag: this.drag,
        action: this.action,
        multiple: this.multiple,
        'before-upload': this.beforeupload,
        'with-credentials': this.withcredentials,
        headers: this.headers,
        name: this.name,
        data: this.data,
        accept: this.accept,
        filelist: this.uploadfiles,
        autoupload: this.autoupload,
        listtype: this.listtype,
        disabled: this.uploaddisabled,
        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
      },
      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>
    );
  }
};
</script>

upload.vue

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

export default {
  inject: ['uploader'],
  components: {
    uploaddragger
  },
  props: {
    type: string,
    action: { //必选参数,上传的地址
      type: string,
      required: true
    },
    name: { //上传的文件字段名
      type: string,
      default: 'file'
    },
    data: object, //上传时附带的额外参数
    headers: object, //设置上传的请求头部
    withcredentials: boolean, //支持发送 cookie 凭证信息
    multiple: boolean, //是否支持多选文件
    accept: string, //接受上传的文件类型(thumbnail-mode 模式下此参数无效)
    onstart: function,
    onprogress: function, //文件上传时的钩子
    onsuccess: function, //文件上传成功时的钩子
    onerror: function, //文件上传失败时的钩子
    beforeupload: function, //上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 promise 且被 reject,则停止上传。
    drag: boolean, //是否启用拖拽上传
    onpreview: { //点击文件列表中已上传的文件时的钩子
      type: function,
      default: function() {}
    },
    onremove: { //文件列表移除文件时的钩子
      type: function,
      default: function() {}
    },
    filelist: array, //上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}]
    autoupload: boolean, //是否在选取文件后立即进行上传
    listtype: string, //文件列表的类型
    httprequest: { //覆盖默认的上传行为,可以自定义上传的实现
      type: function,
      default: ajax
    },
    disabled: boolean,//是否禁用
    limit: number,//最大允许上传个数
    onexceed: function //文件超出个数限制时的钩子
  },

  data() {
    return {
      mouseover: false,
      reqs: {}
    };
  },

  methods: {
    isimage(str) {
      return str.indexof('image') !== -1;
    },
    handlechange(ev) {
      const files = ev.target.files;

      if (!files) return;
      this.uploadfiles(files);
    },
    uploadfiles(files) {
      //文件超出个数限制时,调用onexceed钩子函数
      if (this.limit && this.filelist.length + files.length > this.limit) {
        this.onexceed && this.onexceed(files, this.filelist);
        return;
      }
      //将files转成数组
      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);
        //选取文件后调用upload方法立即进行上传文件
        if (this.autoupload) this.upload(rawfile);
      });
    },
    upload(rawfile) {
      this.$refs.input.value = null;
      //beforeupload 上传文件之前的钩子不存在就直接调用post上传文件
      if (!this.beforeupload) {
        return this.post(rawfile);
      }
      // beforeupload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 promise 且被 reject,则停止上传
      const before = this.beforeupload(rawfile);
      // 在调用beforeupload钩子后返回的是true,则继续上传
      if (before && before.then) {
        before.then(processedfile => {
          //processedfile转成对象
          const filetype = object.prototype.tostring.call(processedfile);

          if (filetype === '[object file]' || filetype === '[object blob]') {
            if (filetype === '[object blob]') {
              processedfile = new file([processedfile], rawfile.name, {
                type: rawfile.type
              });
            }
            for (const p in rawfile) {
              if (rawfile.hasownproperty(p)) {
                processedfile[p] = rawfile[p];
              }
            }
            this.post(processedfile);
          } else {
            this.post(rawfile);
          }
        }, () => {
          this.onremove(null, rawfile);
        });
      } else if (before !== false) { //调用beforeupload之后没有返回值,此时before为undefined,继续上传
        this.post(rawfile);
      } else {  //调用beforeupload之后返回值为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;
      const options = {
        headers: this.headers,
        withcredentials: this.withcredentials,
        file: rawfile,
        data: this.data,
        filename: this.name,
        action: this.action,
        onprogress: e => { //文件上传时的钩子函数
          this.onprogress(e, rawfile);
        },
        onsuccess: res => { //文件上传成功的钩子函数
          //上传成功调用onsuccess方法,即index.vue中的handlesuccess方法
          this.onsuccess(res, rawfile);
          delete this.reqs[uid];
        },
        onerror: err => { //文件上传失败的钩子函数
          this.onerror(err, rawfile);
          delete this.reqs[uid];
        }
      };
      //httprequest可以自定义上传文件,如果没有定义,默认通过ajax文件中的方法上传
      const req = this.httprequest(options);
      this.reqs[uid] = req;
      if (req && req.then) {
        req.then(options.onsuccess, options.onerror);
      }
    },
    handleclick() {
      //点击组件时调用input的click方法
      if (!this.disabled) {
        this.$refs.input.value = null;
        this.$refs.input.click();
      }
    },
    handlekeydown(e) {
      if (e.target !== e.currenttarget) return;
      //如果当前按下的是回车键和空格键,调用handleclick事件
      if (e.keycode === 13 || e.keycode === 32) {
        this.handleclick();
      }
    }
  },

  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 (
      //判断是否允许拖拽,允许的话显示upload-dragger组件,不允许就显示所有插槽中的节点
      <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>
    );
  }
};
</script>

ajax.js

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);
  } catch (e) {
    return text;
  }
}
//默认的上传文件的方法
export default function upload(option) {
  //xmlhttprequest 对象用于在后台与服务器交换数据。
  if (typeof xmlhttprequest === 'undefined') {
    return;
  }
  //创建xmlhttprequest对象
  const xhr = new xmlhttprequest();
  const action = option.action; //上传的地址

  //xmlhttprequest.upload 属性返回一个 xmlhttprequestupload对象,用来表示上传的进度。这个对象是不透明的,但是作为一个xmlhttprequesteventtarget,可以通过对其绑定事件来追踪它的进度。
  if (xhr.upload) {
    //上传进度调用方法,上传过程中会频繁调用该方法
    xhr.upload.onprogress = function progress(e) {
      if (e.total > 0) {
        // e.total是需要传输的总字节,e.loaded是已经传输的字节
        e.percent = e.loaded / e.total * 100;
      }
      //调文件上传时的钩子函数
      option.onprogress(e);
    };
  }
  // 创建一个formdata 对象
  const formdata = new formdata();
  //用户设置了上传时附带的额外参数时
  if (option.data) {
    object.keys(option.data).foreach(key => {
      // 添加一个新值到 formdata 对象内的一个已存在的键中,如果键不存在则会添加该键。
      formdata.append(key, option.data[key]);
    });
  }

  formdata.append(option.filename, option.file, option.file.name);
  //请求出错
  xhr.onerror = function error(e) {
    option.onerror(e);
  };
  //请求成功回调函数
  xhr.onload = function onload() {
    if (xhr.status < 200 || xhr.status >= 300) {
      return option.onerror(geterror(action, option, xhr));
    }
    //调用upload.vue文件中的onsuccess方法,将上传接口返回值作为参数传递
    option.onsuccess(getbody(xhr));
  };
  //初始化请求
  xhr.open('post', action, true);

  if (option.withcredentials && 'withcredentials' in xhr) {
    xhr.withcredentials = true;
  }

  const headers = option.headers || {};

  for (let item in headers) {
    if (headers.hasownproperty(item) && headers[item] !== null) {
      //设置请求头
      xhr.setrequestheader(item, headers[item]);
    }
  }
  //发送请求
  xhr.send(formdata);
  return xhr;
}

upload-dragger.vue

<template>
  <!--拖拽上传时显示此组件-->
  <div
    class="el-upload-dragger"
    :class="{
      'is-dragover': dragover
    }"
    @drop.prevent="ondrop"
    @dragover.prevent="ondragover"
    @dragleave.prevent="dragover = false"
  >
    <slot></slot>
  </div>
</template>
<script>
  export default {
    name: 'eluploaddrag',
    props: {
      disabled: boolean
    },
    inject: {
      uploader: {
        default: ''
      }
    },
    data() {
      return {
        dragover: false
      };
    },
    methods: {
      ondragover() {
        if (!this.disabled) {
          this.dragover = true;
        }
      },
      ondrop(e) {
        if (this.disabled || !this.uploader) return;
        //接受上传的文件类型(thumbnail-mode 模式下此参数无效),此处判断该文件是都符合能上传的类型
        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;
            });
        }));
      }
    }
  };
</script>

upload-list.vue

<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"
    >
      <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)">
        <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>
      <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>
    </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);
      },
      handleclick(file) {
        this.handlepreview && this.handlepreview(file);
      }
    }
  };
</script>