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,可以自定义整个列表。因为不涉及上传,请自行学习。

相关推荐
燃先生._.1 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖2 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235242 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240253 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar3 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人4 小时前
前端知识补充—CSS
前端·css
GISer_Jing4 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245524 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v4 小时前
webpack最基础的配置
前端·webpack·node.js