vc-upload源码分析 -- ant-design-vue系列

vc-upload源码分析 – ant-design-vue系列

1 整体结构

上传组件的使用分两种:点击上传和拖拽上传。

点击的是组件或者是卡片,这个是用户通过插槽传递的。除上传外的其他功能,比如预览、自定义文件渲染等功能,也不是上传的核心功能。

上传是通过vc-upload组件来实现的。整体结构如下:

在这里插入图片描述

2 源码分析

vc-upload中,Upload.tsx的逻辑比较少,包括:设置componentTag: 'span',给<AjaxUpload>对应的节点挂上abort方法等等。主要逻辑在AjaxUpload.tsx组件中。

2.1 渲染函数(重点)

先看一下最后的渲染函数。

🎯 浏览器调用文件选择,常用的只有 <input type="file" />这种方法。

  • 为了自定义样式,所以input组件是不可见的,当我们点击Tag区域时,需要触发input的点击事件,这个可以通过input的引用:fileInput.value.click()来实现。

这里的Tag,不是组件,而是Upload.tsx组件中设置的默认值span

<Tag {...events} class={cls} role="button" style={attrs.style}>
  <input
    {...pickAttrs(otherProps, { aria: true, data: true })}
    id={id}
    type="file"
    ref={fileInput}
    onClick={e => e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948
    key={uid.value}
    style={{ display: 'none' }}
    accept={accept}
    {...dirProps}
    multiple={multiple}
    onChange={onChange}
    {...(capture != null ? { capture } : {})}
  />
  {slots.default?.()}
</Tag>
  • 看一下Tagevents事件,重点:onClick事件、onDrop事件。
const events = {
  onClick: openFileDialogOnClick ? onClick : () => {},
  onKeydown: openFileDialogOnClick ? onKeyDown : () => {},
  onMouseenter,
  onMouseleave,
  onDrop: onFileDrop,
  onDragover: onFileDrop,
  tabindex: '0',
};
  1. onClick事件:调用fileInput.value.click,打开文件选择框

    const onClick = (e: MouseEvent | KeyboardEvent) => {
      const el = fileInput.value;
      if (!el) {
        return;
      }
      const { onClick } = props;
     
      el.click();
      if (onClick) {
        onClick(e);
      }
    };
    
  2. onDrop / onDragover事件:onDragover指的是鼠标在目标区域内移动,这时候只阻止默认事;onDrop指的是在目标区域松开鼠标点击,这时候会处理上传。

    const onFileDrop = (e: DragEvent) => {
      const { multiple } = props;
    
      e.preventDefault();
    
      if (e.type === 'dragover') {
        return;
      }
    	/**
    	* 如果是文件夹,先处理文件树,然后调用第二个参数传递的uploadFiles,上传文件
    	*/
      if (props.directory) {
        traverseFileTree(
          Array.prototype.slice.call(e.dataTransfer.items),
          uploadFiles,
          (_file: RcFile) => attrAccept(_file, props.accept),
        );
      } else {
        /**
        * 如果选的是文件,上传选择成功的
        *(windows电脑可以选任意类型,但是上传的时候可能会被类型校验卡掉一些文件;mac只能选择类型匹配的文件)
        */
        const files: [RcFile[], RcFile[]] = partition(
          Array.prototype.slice.call(e.dataTransfer.files),
          (file: RcFile) => attrAccept(file, props.accept),
        );
        let successFiles = files[0];
        const errorFiles = files[1];
        if (multiple === false) {
          successFiles = successFiles.slice(0, 1);
        }
    
        uploadFiles(successFiles);
        if (errorFiles.length && props.onReject) props.onReject(errorFiles);
      }
    };
    

2.2 文件选择成功后的流程

文件选择成功后,会触发inputchange方法。主流程如下:

<Tag {...events} class={cls} role="button" style={attrs.style}>
  <input
		// ......
		onChange={onChange}
	/>
  {slots.default?.()}
</Tag>

在这里插入图片描述

2.2.1 onChange方法
 const onChange = (e: ChangeEvent) => {
   const { accept, directory } = props;
   const { files } = e.target as any;
   // 非文件夹且校验通过的文件
   const acceptedFiles = [...files].filter(
     (file: RcFile) => !directory || attrAccept(file, accept),
   );
   uploadFiles(acceptedFiles);
   reset();
 };

🚀 attrAccept方法,见 3.1

2.2.2 uploadFiles方法
 const uploadFiles = (files: File[]) => {
   const originFiles = [...files] as RcFile[];
   /**
   * 为每个文件生成一个id,调用processFile进行处理
   */
   const postFiles = originFiles.map((file: RcFile & { uid?: string }) => {
     file.uid = getUid();
     return processFile(file, originFiles);
   });

   /**
   * 所有文件处理完成后,回调onBatchStart方法,然后依次上传文件。
   */
   Promise.all(postFiles).then(fileList => {
     const { onBatchStart } = props;

     onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile })));

     fileList
       .filter(file => file.parsedFile !== null)
       .forEach(file => {
       post(file);
     });
   });
 };
2.2.3 processFile 方法
const processFile = async (file: RcFile, fileList: RcFile[]): Promise<ParsedFileInfo> => {
  const { beforeUpload } = props;

  /**
  * 1 调用用户传递的beforeUpload方法,如果这个方法返回false,则停止上传
  */
  let transformedFile: BeforeUploadFileType | void = file;
  if (beforeUpload) {
    try {
      transformedFile = await beforeUpload(file, fileList);
    } catch (e) {
      // Rejection will also trade as false
      transformedFile = false;
    }
    if (transformedFile === false) {
      return {
        origin: file,
        parsedFile: null,
        action: null,
        data: null,
      };
    }
  }

  /**
  * 2 action一般是上传地址,也可以是返回地址的函数
  */
  const { action } = props;
  let mergedAction: string;
  if (typeof action === 'function') {
    mergedAction = await action(file);
  } else {
    mergedAction = action;
  }

  /**
  * 3 上传所需参数或返回上传参数的方法
  */
  const { data } = props;
  let mergedData: Record<string, unknown>;
  if (typeof data === 'function') {
    mergedData = await data(file);
  } else {
    mergedData = data;
  }

  /**
  * 可以忽略,当作简单赋值语句即可
  */
  const parsedData =
        // string type is from legacy `transformFile`.
        // Not sure if this will work since no related test case works with it
        (typeof transformedFile === 'object' || typeof transformedFile === 'string') &&
        transformedFile
  ? transformedFile
  : file;

  /**
  * 4 最后的文件如果不是file类型,把它转换成file类型
  */
  let parsedFile: File;
  if (parsedData instanceof File) {
    parsedFile = parsedData;
  } else {
    parsedFile = new File([parsedData], file.name, { type: file.type });
  }

  const mergedParsedFile: RcFile = parsedFile as RcFile;
  mergedParsedFile.uid = file.uid;

  /**
  * 5 最后的file,叫parsedFile
  */
  return {
    origin: file,
    data: mergedData,
    parsedFile: mergedParsedFile,
    action: mergedAction,
  };
};
2.2.4 post方法

request 方法见3.2。

const post = ({ data, origin, action, parsedFile }: ParsedFileInfo) => {
  if (!isMounted) {
    return;
  }

  const { onStart, customRequest, name, headers, withCredentials, method } = props;

  const { uid } = origin;
  /**
  * 可以使用自定义的上传函数,默认使用request.ts提供的上传方法
  */
  const request = customRequest || defaultRequest;

  const requestOption = {
    action,
    filename: name,
    data,
    file: parsedFile,
    headers,
    withCredentials,
    method: method || 'post',
    onProgress: (e: UploadProgressEvent) => {
      const { onProgress } = props;
      onProgress?.(e, parsedFile);
    },
    onSuccess: (ret: any, xhr: XMLHttpRequest) => {
      const { onSuccess } = props;
      onSuccess?.(ret, parsedFile, xhr);

      delete reqs[uid];
    },
    onError: (err: UploadRequestError, ret: any) => {
      const { onError } = props;
      onError?.(err, ret, parsedFile);

      delete reqs[uid];
    },
  };

  onStart(origin);
  /**
  * reqs 是一个全局的对象,方法调用abort方法。
  */
  reqs[uid] = request(requestOption);
};

3 辅助函数

3.1 检查文件类型

源码地址:https://github.com/vueComponent/ant-design-vue/blob/main/components/vc-upload/attr-accept.ts

使用 some 函数来判断,只要文件file匹配了accept数组的某一项规则,则验证通过。

在这里插入图片描述

export default (file: RcFile, acceptedFiles: string | string[]) => {
  if (file && acceptedFiles) {
    const acceptedFilesArray = Array.isArray(acceptedFiles)
      ? acceptedFiles
      : acceptedFiles.split(',');
    const fileName = file.name || '';
    const mimeType = file.type || '';
    const baseMimeType = mimeType.replace(/\/.*$/, ''); // 把“.“以及之后的所有字符清空

    return acceptedFilesArray.some(type => {
      const validType = type.trim();
      // 如果validType是*/*或者*,那么所有文件都通过
      if (/^\*(\/\*)?$/.test(type)) { // 以 * 开头,后面可以接0个或者1个 /*
        return true;
      }

      // 如果validType是 .jpg .png之类的,检查文件名后缀
      if (validType.charAt(0) === '.') {
        const lowerFileName = fileName.toLowerCase();
        const lowerType = validType.toLowerCase();

        let affixList = [lowerType];
        if (lowerType === '.jpg' || lowerType === '.jpeg') {
          affixList = ['.jpg', '.jpeg'];
        }

        return affixList.some(affix => lowerFileName.endsWith(affix));
      }

      // 如果validType是image/*之类的,那么比较 baseMimeType 和 斜杠之前的部分
      if (/\/\*$/.test(validType)) {
        return baseMimeType === validType.replace(/\/.*$/, ''); // 把“.“以及之后的所有字符清空
      }

      // 类型完全匹配,通过
      if (mimeType === validType) {
        return true;
      }

      // 验证规则无效,也通过
      if (/^\w+$/.test(validType)) {  // \w表示数字和字符,+表示1个及以上
        warning(false, `Upload takes an invalidate 'accept' type '${validType}'.Skip for check.`);
        return true;
      }

      return false;
    });
  }
  return true;
};

3.2 xhr上传文件

源码地址:https://github.com/vueComponent/ant-design-vue/blob/main/components/vc-upload/request.ts

针对单个文件,调用upload方法,把option对象传进去,对象如下示例:

在这里插入图片描述

整体过程:

在这里插入图片描述

export default function upload(option: UploadRequestOption) {
  const xhr = new XMLHttpRequest();

  if (option.onProgress && xhr.upload) {
    /**
    * 上传过程会实时计算进度,通过onProgress回调返回给用户
    */
    xhr.upload.onprogress = function progress(e: UploadProgressEvent) {
      if (e.total > 0) {
        e.percent = (e.loaded / e.total) * 100;
      }
      option.onProgress(e);
    };
  }

  /**
  * FormData这种格式会自动修改'content-type'
  */
  const formData = new FormData();

  /**
  * data是用户自定义的属性,或者自定义的方法的返回值
  */
  if (option.data) {
    Object.keys(option.data).forEach(key => {
      const value = option.data[key];
      // support key-value array data
      if (Array.isArray(value)) {
        value.forEach(item => {
          // { list: [ 11, 22 ] }
          // formData.append('list[]', 11);
          formData.append(`${key}[]`, item);
        });
        return;
      }

      formData.append(key, value as string | Blob);
    });
  }

  /**
  * 用户上传的文件
  */
  if (option.file instanceof Blob) {
    formData.append(option.filename, option.file, (option.file as any).name);
  } else {
    formData.append(option.filename, option.file);
  }

  xhr.onerror = function error(e) {
    option.onError(e);
  };

  xhr.onload = function onload() {
    // 只有2xx认为是成功的
    if (xhr.status < 200 || xhr.status >= 300) {
      return option.onError(getError(option, xhr), getBody(xhr));
    }

    return option.onSuccess(getBody(xhr), xhr);
  };

  /**
  * 设置请求的方法、url、是否异步,这里的true代表异步
  */
  xhr.open(option.method, option.action, true);

  /**
  * 可以通过设置 withCredentials 属性为 true 来启用 cookies 和 HTTP 认证信息的发送
  */
  // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
  if (option.withCredentials && 'withCredentials' in xhr) {
    xhr.withCredentials = true;
  }

  const headers = option.headers || {};

  // when set headers['X-Requested-With'] = null , can close default XHR header
  // see https://github.com/react-component/upload/issues/33
  if (headers['X-Requested-With'] !== null) {
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  }

  /**
  * 设置所有的header
  */
  Object.keys(headers).forEach(h => {
    if (headers[h] !== null) {
      xhr.setRequestHeader(h, headers[h]);
    }
  });

  /**
  * 发送请求
  */
  xhr.send(formData);

  return {
    /**
    * 返回取消的方法,所以上传过程是可以中断的
    */
    abort() {
      xhr.abort();
    },
  };
}

4 总结

上传文件的每个步骤都已经在上文中体现,除了处理文件树的部分。剩下UploadDraggervc-upload的封装,下篇文章再进行分析。

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值