react Upload 组件
下面的代码已经去除了大部分与主线无关的代码。
效果查看
需求分析
- 上传文件
- 展示文件上传状态(成功/失败/上传中)
- 受控/非受控组件
- 拖拽上传
暴露的 API
export interface UploadProps {
// 上传文件对应的服务器地址
action?: string;
fileList?: UploadFile[];
onChange?: (e: OnChangeEvent) => void;
/** 返回 false 会中断上传 */
beforeUpload?: (fileList: File[]) => File[] | Promise<File[]> | boolean;
}
基本代码
直接使用原生的 input
来实现
const onChange = e => {
// 即为需要上传的File对象
console.log(e.target.files[0]);
};
<input type="file" onChange={onChange} />;
并将该 File 对象添加到 FormData 中最后发送给服务器。
const formData = new FormData();
// file即为上面通过input拿到的文件对象
formData.append('file name', file);
最后发送给服务端
axios.post(url, formData);
其实上面的代码基本上就是核心的代码了,只不过我们需要在这中间穿插一些其他状态的管理,例如自定义 input 的样式,增加上传进度等等...
自定义触发 input 的元素
首先将原有的 input 通过display: none
隐藏掉,然后通过 ref 的方式获取 input,在需要绑定的元素上触发 input 的 click 事件即可。
const inputRef = useRef<HTMLInputElement>(null);
const onOpenResource = (e: MouseEvent) => {
e.stopPropagation();
if (inputRef.current) {
inputRef.current.click();
}
};
<div onClick={onOpenResource}>
<input
ref={inputRef}
type="file"
accept={accept}
onChange={onInternalChange}
multiple={multiple}
disabled={disabled}
/>
{children}
</div>;
拖拽上传
和普通的点击上传几乎没有区别,只是在获取File
对象时是通过e.dataTransfer.files
来获取的。
状态的管理
我们在组件的内部自己维护一个fileList
,并为其加上一系列状态
const [internalFileList, setInternalFileList] = useState([]);
internalFileList
包含的属性
export interface UploadFile {
// 文件名
name?: string;
// 文件唯一id
uid?: string;
// 文件当前状态(uploading,done,error,removed,canceled)
status?: string;
// 上传成功后服务器返回的图片地址
url?: string;
// 上传进度
percent?: number;
// 原文件对象,也就是最后要用来上传的对象
rawFile?: File;
}
updateStatus 方法
后面会多次用到了该方法,其实就是通过 uid 找到对应项目,修改其属性并调用onChange
的操作
const updateStatus = (
currentTask: UploadFile,
info: { status?: string; url?: string | ArrayBuffer; percent?: number }
) => {
let newFileList: UploadFile[] = [];
let currentFile = {};
setInternalFileList(prev => {
newFileList = prev.map(task => {
if (task.uid !== currentTask.uid) return task;
currentFile = {
...task,
name: currentTask.rawFile?.name,
uid: currentTask.uid,
...info
};
return currentFile;
});
return newFileList;
});
onChange?.({
file: { ...currentFile, rawFile: currentTask.rawFile },
fileList: newFileList
});
};
组件的生命周期 beforeUpload
先看代码
// 在onChange中调用,将input中获取的files传入
const handleUploadTasks = async (files: File[]) => {
let handledFiles = files;
if (beforeUpload) {
const shouldUpload = await beforeUpload(files);
if (shouldUpload) {
handledFiles = shouldUpload as File[];
} else {
// 该变量用来在真正发送请求的时候做是否中断判断
shouldUploadRef.current = false;
}
}
// 为beforeUpload处理好后的List添加状态
const newTasks: UploadFile[] = handledFiles.map(file => ({
uid: getUid(),
status: UploadStatus.UPLOADING,
name: file.name,
rawFile: file
}));
setInternalFileList(prev => [...prev, ...newTasks]);
// 上传操作,下面会介绍到
await upload(newTasks);
};
/** 返回 false 会中断上传 */
beforeUpload?: (fileList: File[]) => File[] | Promise<File[]> | boolean;
该生命周期会将当前用户选中的文件当做参数传递进去,用户可以返回一个新的文件列表或者返回 false。这在当需要根据文件大小来中断上传操作的场景中很适用。
文件上传
const upload = async (tasks: UploadFile[]) => {
// beforeUpload生命周期返回false时的中断处理
if (!shouldUploadRef.current) {
tasks.map(async currentTask => {
// 将内部状态更新为canceled并中断上传
updateStatus(currentTask, { status: UploadStatus.CANCELED, url: '' });
});
return;
}
await Promise.all(
tasks.map(async currentTask => {
try {
// 进行post请求
const result = await post(currentTask);
// 将内部状态更新为done
updateStatus(currentTask, {
status: UploadStatus.DONE,
url: result.url
});
alert(`${currentTask.rawFile?.name} success!`);
} catch (e) {
// 将内部状态更新为error
updateStatus(currentTask, { status: UploadStatus.ERROR, url: '' });
alert(`${currentTask.rawFile?.name} failed!`);
throw e;
}
})
).catch(error => {
// eslint-disable-next-line no-console
console.error(error);
});
};
post 函数非常简单就是上前面提到的 formData 操作
const post = async (currentTask: UploadFile): Promise<ResponseData> => {
const formData = new FormData();
const { rawFile } = currentTask;
formData.append(rawFile.name, rawFile);
const res = await axios.post(action, formData);
return res.data;
};
文件的删除
通过 uid 找到对应项进行删除即可
const onRemove = (file: UploadFile) => {
const removedFileList = internalFileList.filter(item => item.uid !== file.uid);
setInternalFileList(removedFileList);
};
上传进度
涉及知识点:
使用到了 XMLHttpRequest.upload.progress
,axios
将其包装成了 options.onUploadProgress
,我们只要稍稍修改一下前面的 post
方法就行
axios.post(url, formData, {
onUploadProgress: (e: ProgressEvent) => {
// 更新上传进度
updateStatus(currentTask, {
status: UploadStatus.UPLOADING,
percent: Math.round((e.loaded * 100) / e.total)
});
}
});
文件列表展示
上面我们拿到了处理后的文件列表,至于文件列表的实现没有什么难点了,我们只需要使用internalFileList
的 percent、status...属性即可。这里就不做过多的介绍了。
// 具体实现不做介绍
<FileList onRemove={onRemove} items={internalFileList} />