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
预览包括三种形式:text
、picture
、picture-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元素上放置内容。为了改变这个行为,让一个元素成为放入区域或者是可放置的,元素必须同时监听 dragover
和 drop
事件。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
是一个双向绑定的值,保存了用户上传成功的文件信息。
useMergedState
在cascader
组件解析中分析过,这个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 方法
onError
和onProcess
同理,不做分析。
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。
- 创建一个
canvas
,设置canvas
的样式,使其固定在页面左上角,宽度和高度均为MEASURE_SIZE
,并设置z-index
以确保它位于顶层,同时设置display: none;
使其不可见。 - 开启
2d
绘图,首先获取图片的真实宽度和高度,然后计算大小和偏移量,确保图片能够按照其原始比例缩放,并居中显示。 - 使用
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 总结
本篇分析了 Drageer
和 Upload
组件的实现,上传的整个过程都是串行的,比较容易理解。在上传的各个阶段,都可以通过钩子来获取上传文件的信息和状态。
renderUploadList
是一个单独的组件,用来渲染上传的结果。这个组件提供了很多插槽,比如itemRender
,可以自定义整个列表。因为不涉及上传,请自行学习。