3.2 Upload源码分析 -- ant-design-vue系列

Upload源码分析 -- ant-design-vue系列

源码地址:https://github.com/vueComponent/ant-design-vue/blob/main/components/upload/Upload.tsx

1 概述

本篇是对Upload组件的分析,这个组件调用了vc-upload,是对vc-upload的封装。

作用包括:上传数据的管理、上传行为的定义、上传结果的预览等。

Upload上传包括两种形式:点击和拖动,分别如下所示:

|

|

|

| ------------------------------------------------------------ | ------------------------------------------------------------ |

Upload预览包括三种形式:textpicturepicture-card,分别如下所示:

|

|

|

|

| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |

2 源码分析

2.1 Dragger组件:拖动上传

使用<a-upload-dragger>组件来实现拖动上传,内部还是调用了Upload组件,这是只是设置了type: 'drag'

javascript 复制代码
 const draggerProps = {
   ...restProps,
   ...restAttrs,
   type: 'drag',
   style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height },
 } as any;
return <Upload {...draggerProps} v-slots={slots}></Upload>;

2.2 Upload组件

2.2.1 渲染函数

主要逻辑如下图:

  • type === 'drag',需要在VcUpload外面套一个div节点,这个节点是有作用的。

🎯 默认情况下,浏览器会阻止在大多数HTML元素上放置内容。为了改变这个行为,让一个元素成为放入区域或者是可放置的,元素必须同时监听 dragoverdrop 事件。https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API

同时renderUploadList()会把已经上传成功的文件展示在下方。

html 复制代码
const { listType, disabled, type } = props;
// ......
if (type === 'drag') {
	// ......
  return (
    <span>
      <div
        class={dragCls}
        onDrop={onFileDrop}
        onDragover={onFileDrop}
        onDragleave={onFileDrop}
      >
        <VcUpload
          {...rcUploadProps}
          ref={upload}
          v-slots={slots}
          >
          <div>{slots.default?.()}</div>
        </VcUpload>
      </div>
      {renderUploadList()}
    </span>
  );
}
  • type === 'select' && listType === 'picture-card',这时候上传按钮也是卡片列表的一部分,且在列表最后,可以通过maxCount来控制数量,如果超过最大值,上传按钮隐藏。
html 复制代码
<!-- 上传组件被传入renderUploadList函数中,方便控制样式 -->
const renderUploadButton = (uploadButtonStyle?: CSSProperties) => (
  <div class={uploadButtonCls} style={uploadButtonStyle}>
  	<VcUpload {...rcUploadProps} ref={upload} v-slots={slots} />
  </div>
);

if (listType === 'picture-card') {
  return (
    <span class={classNames(`${prefixCls.value}-picture-card-wrapper`, attrs.class)}>
       {renderUploadList(renderUploadButton, !!(children && children.length))}
    </span>
  );
}
  • 其他情况下,先渲染上传按钮,再渲染列表。
html 复制代码
return (
	<span class={attrs.class}>
  	{renderUploadButton(children && children.length ? undefined : { display: 'none' })}
  	{renderUploadList()}
	</span>
);
2.2.2 重要变量 fileList

fileList是一个双向绑定的值,保存了用户上传成功的文件信息。

useMergedStatecascader组件解析中分析过,这个hook可以根据外部是否传入变量,来切换组件的可控和非可控状态。

如果用户设置了fileList的默认值,则需要给每一个file文件对象设置一个uid,这个uid在移除文件时会用到。

javascript 复制代码
const [mergedFileList, setMergedFileList] = useMergedState(props.defaultFileList || [], {
  value: toRef(props, 'fileList'),
  postState: list => {
    const timestamp = Date.now();
    return (list ?? []).map((file, index) => {
      if (!file.uid && !Object.isFrozen(file)) {
        file.uid = `__AUTO__${timestamp}_${index}__`;
      }
      return file;
    });
  },
});
2.2.3 主要流程

vc-upload流程在上一篇已经分析过了,本篇只分析Upload组件的方法。

2.2.3.1 mergedBeforeUpload 方法
javascript 复制代码
const mergedBeforeUpload = async (file: FileType, fileListArgs: FileType[]) => {
  const { beforeUpload, transformFile } = props;

  let parsedFile: FileType | Blob | string = file;
  if (beforeUpload) {
    const result = await beforeUpload(file, fileListArgs);

    /**
    * 针对某一个file,如果返回false,则用户会自定义上传过程。后续流程不会继续。
    * 对应官方示例的"手动上传"
    */
    if (result === false) {
      return false;
    }

    
    delete (file as any)[LIST_IGNORE];
    /**
    * 如果结果是LIST_IGNORE,则给file文件增加这个属性,下一步后过滤掉这些ignore的file文件
    */
    if ((result as any) === LIST_IGNORE) {
      Object.defineProperty(file, LIST_IGNORE, {
        value: true,
        configurable: true,
      });
      return false;
    }

    if (typeof result === 'object' && result) {
      parsedFile = result as File;
    }
  }

  if (transformFile) {
    parsedFile = await transformFile(parsedFile as any);
  }

  return parsedFile as File;
};
2.2.3.2 onBatchStart 方法
javascript 复制代码
const onBatchStart: RcUploadProps['onBatchStart'] = batchFileInfoList => {
  /**
  * 过滤掉LIST_IGNORE的文件,这些文件不会进入fileList
  */
  const filteredFileInfoList = batchFileInfoList.filter(
    info => !(info.file as any)[LIST_IGNORE],
  );

  // Nothing to do since no file need upload
  if (!filteredFileInfoList.length) {
    return;
  }
  
	// 生成文件对象
  const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as FileType));

  /**
  * 合并新老文件
  */
  let newFileList = [...mergedFileList.value];
  objectFileList.forEach(fileObj => {
    // Replace file if exist
    newFileList = updateFileList(fileObj, newFileList);
  });

  /**
  * 对新文件进行处理。
  * 在vc-upload的processFile方法中,如果beforeUpload返回了false,那么返回的对象如下。
  *		{	origin: file,
  *     parsedFile: null,
  *     action: null,
  *     data: null, }
  * 这时候用户会自己处理上传过程,我们只要补充必要属性即可,不需要管上传过程。
  *
  * 否则,设置上传状态为uploading。
  */
  objectFileList.forEach((fileObj, index) => {
    // Repeat trigger `onChange` event for compatible
    let triggerFileObj: UploadFile = fileObj;

    if (!filteredFileInfoList[index].parsedFile) {
      // `beforeUpload` return false
      const { originFileObj } = fileObj;
      let clone;

      try {
        clone = new File([originFileObj], originFileObj.name, {
          type: originFileObj.type,
        }) as any as UploadFile;
      } catch (e) {
        clone = new Blob([originFileObj], {
          type: originFileObj.type,
        }) as any as UploadFile;
        clone.name = originFileObj.name;
        clone.lastModifiedDate = new Date();
        clone.lastModified = new Date().getTime();
      }

      clone.uid = fileObj.uid;
      triggerFileObj = clone;
    } else {
      // Inject `uploading` status
      fileObj.status = 'uploading';
    }

    onInternalChange(triggerFileObj, newFileList);
  });
};
2.2.3.3 onSuccess 方法

onErroronProcess同理,不做分析。

javascript 复制代码
const onSuccess = (response: any, file: FileType, xhr: any) => {
  try {
    if (typeof response === 'string') {
      response = JSON.parse(response);
    }
  } catch (e) {
    /* do nothing */
  }

  /**
  * 如果文件在上传过程中被移除了,那么就返回
  * getFileItem 的过程是在mergedFileList中查找和file的uid(或者name)相同的对象
  */
  if (!getFileItem(file, mergedFileList.value)) {
    return;
  }

  /**
  * 修改状态
  */
  const targetItem = file2Obj(file);
  targetItem.status = 'done';
  targetItem.percent = 100;
  targetItem.response = response;
  targetItem.xhr = xhr;

  const nextFileList = updateFileList(targetItem, mergedFileList.value);

  onInternalChange(targetItem, nextFileList);
};
2.2.3.4 onInternalChange 方法

在上传过程中/成功/失败,都会修改fileList数组。可以获取到上传状态和结果。

javascript 复制代码
const onInternalChange = (
  file: UploadFile,
  changedFileList: UploadFile[],
  event?: { percent: number },
) => {
  let cloneList = [...changedFileList];

  /**
  * 如果maxCount是1,取最后一项;如果不是,取前面maxCount项
  */
  if (props.maxCount === 1) {
    cloneList = cloneList.slice(-1);
  } else if (props.maxCount) {
    cloneList = cloneList.slice(0, props.maxCount);
  }

  /**
  * 修改fileList数组
  */
  setMergedFileList(cloneList);

  const changeInfo: UploadChangeParam<UploadFile> = {
    file: file as UploadFile,
    fileList: cloneList,
  };

  if (event) {
    changeInfo.event = event;
  }
  props['onUpdate:fileList']?.(changeInfo.fileList);
  props.onChange?.(changeInfo);
  
  /**
  * 和Form表单组件的联动
  */
  formItemContext.onFieldChange();
};
2.2.3.5 handleRemove 方法

这是用户点删除的时候触发的方法。这里使用到了uid,来寻找应该移除的元素。

javascript 复制代码
const handleRemove = (file: UploadFile) => {
  let currentFile: UploadFile;
  const mergedRemove = props.onRemove || props.remove;
  Promise.resolve(typeof mergedRemove === 'function' ? mergedRemove(file) : mergedRemove).then(
    ret => {
      // mergedRemove函数返回了false,则不移除
      if (ret === false) {
        return;
      }

      const removedFileList = removeFileItem(file, mergedFileList.value);

      if (removedFileList) {
        currentFile = { ...file, status: 'removed' };
        mergedFileList.value?.forEach(item => {
          const matchKey = currentFile.uid !== undefined ? 'uid' : 'name';
          if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) {
            item.status = 'removed';
          }
        });
        
        /**
        * 上传过程中的文件,需要取消上传
        */
        upload.value?.abort(currentFile);

        onInternalChange(currentFile, removedFileList);
      }
    },
  );
};

3 其他函数

3.1 图片预览

这个函数的目的是预览给定的图片文件,并将其转换为一个Base64编码的数据URL(dataURL),然后返回这个数据URL。

  1. 创建一个canvas,设置canvas的样式,使其固定在页面左上角,宽度和高度均为MEASURE_SIZE,并设置z-index以确保它位于顶层,同时设置display: none;使其不可见。
  2. 开启2d绘图,首先获取图片的真实宽度和高度,然后计算大小和偏移量,确保图片能够按照其原始比例缩放,并居中显示。
  3. 使用canvas.toDataURL()生成图片的Base64编码的数据URL,设置到图片的src。清除canvas

所以 默认的预览方式,并不需要获取后端返回的图片url

javascript 复制代码
export function previewImage(file: File | Blob): Promise<string> {
  return new Promise(resolve => {
    if (!file.type || !isImageFileType(file.type)) {
      resolve('');
      return;
    }

    const canvas = document.createElement('canvas');
    canvas.width = MEASURE_SIZE;
    canvas.height = MEASURE_SIZE;
    canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE}px; height: ${MEASURE_SIZE}px; z-index: 9999; display: none;`;
    document.body.appendChild(canvas);
    const ctx = canvas.getContext('2d');
    const img = new Image();
    img.onload = () => {
      const { width, height } = img;

      let drawWidth = MEASURE_SIZE;
      let drawHeight = MEASURE_SIZE;
      let offsetX = 0;
      let offsetY = 0;

      if (width > height) {
        drawHeight = height * (MEASURE_SIZE / width);
        offsetY = -(drawHeight - drawWidth) / 2;
      } else {
        drawWidth = width * (MEASURE_SIZE / height);
        offsetX = -(drawWidth - drawHeight) / 2;
      }

      ctx!.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
      const dataURL = canvas.toDataURL();
      document.body.removeChild(canvas);

      resolve(dataURL);
    };
    img.src = window.URL.createObjectURL(file);
  });
}

3.2 isImageUrl

判断一个url是不是图片的url,只看主要逻辑部分。

javascript 复制代码
/**
* 如果是baseUrl,那么以 data:image 开头
* 如果是图片文件,那么扩展名必须是 webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico
*/
if (
  /^data:image\//.test(url) ||
  /(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico)$/i.test(extension)
) {
  return true;
}

4 总结

本篇分析了 DrageerUpload 组件的实现,上传的整个过程都是串行的,比较容易理解。在上传的各个阶段,都可以通过钩子来获取上传文件的信息和状态。

renderUploadList是一个单独的组件,用来渲染上传的结果。这个组件提供了很多插槽,比如itemRender,可以自定义整个列表。因为不涉及上传,请自行学习。

相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu12 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss