zoukankan      html  css  js  c++  java
  • element-ui Upload 上传组件源码分析整理笔记(十四)

    简单写了部分注释,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>
    
    
  • 相关阅读:
    weblogic---- Remote远程调用
    weblogic的web.xml报错----Malformed UTF-8 char -- is an XML encoding declaration missing
    jar、war、ear
    weblogic报错----Received exception while creating connection for pool "TDMSKD": The Network Adapter could not establish the connection
    Weblogic java生成wlfullclient.jar
    Tomcat8-windows不能在本地计算机启动tomcat,有更多的信息,查阅系统事件日志。
    jdk历史各个版本下载
    分享一个很有意思的环境探针
    Spring+SpringMVC+MyBatis+easyUI整合进阶篇(七)一次线上Mysql数据库崩溃事故的记录
    说卸载英雄联盟的人真的不是矫情
  • 原文地址:https://www.cnblogs.com/fangnianqin/p/11447906.html
Copyright © 2011-2022 走看看