前言
自己想写一个拖拽上传组件的原因是现在github上star多的仓库都不满足自己的需求。有的是选择新图片之后会覆盖已添加的图片,有的是没用预览功能,并且不能删除已添加的图片。以上种种原因使我自己想要做一个拖拽上传组件,包含的功能有:
- 拖拽上传
- 多次添加文件,不会覆盖原有文件
- 预览文件,显示文件大小
- 支持删除
- 文件大小限制
- 响应式
最终效果预览:
环境搭建
创建 react 项目
使用 npx creat-react-app 创建react项目。
添加依赖
本项目使用 styled-components 书写css 样式
项目结构
- file-upload-component
- src
- App.js
- FileUpload.jsx
- FileUpload.styles.js
- index.js
代码
state
本项目使用函数式组件,组件中的状态保存用户上传的文件。
在 FileUpload.jsx 中,添加如下代码:
import React, { useState } from "react";
const FileUpload = () => {
const [files, setFiles] = useState({});
return (
<div></div>
)
}
export default FileUpload;
使用对象保存文件的原因是对象更容易添加或删除文件,并且确保不会添加重复文件。
useRef hook
由于原生 html 的上传组件样式太丑,所以在本项目是通过一个按钮来打开它,因此我们需要 useRef hook 来引用 dom。
import React, { useState, useRef } from "react";
const FileUpload = (props) => {
const fileInputField = useRef(null);
const [files, setFiles] = useState({});
return (
<input type="file" ref={fileInputField} />
)
}
export default FileUpload;
props
本组件需要以下prop:
maxFileSizeInBytes限制上传文件的总大小updateFilesCb回调函数,给父组件传递当前 files 状态
本组件不提供上传到服务端的功能,因此添加文件后需要将文件传递给父组件。并且由于react 组件间的单向数据流,子组件给父组件传递数据的常用方式就是回调函数。
import React, { useRef, useState } from "react";
const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000;
const FileUpload = ({
label,
updateFilesCb,
maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES,
...otherProps
}) => {
const fileInputField = useRef(null);
const [files, setFiles] = useState({});
return (
<input type="file" ref={fileInputField} />
)
}
export default FileUpload;
完善 jsx
import React, { useRef, useState } from "react";
const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000;
const KILO_BYTES_PER_BYTE = 1000;
const convertBytesToKB = (bytes) => Math.round(bytes / KILO_BYTES_PER_BYTE);
const FileUpload = ({
label,
updateFilesCb,
maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES,
...otherProps
}) => {
const fileInputField = useRef(null);
const [files, setFiles] = useState({});
return (
<>
<div>
<label>{label}</label>
<p>拖拽上传或点击以下按钮</p>
<button type="button">
<i className="fas fa-file-upload" />
<span>上传{otherProps.multiple ? "文件" : "一个文件"}</span>
</button>
<input
type="file"
ref={fileInputField}
title=""
value=""
{...otherProps}
/>
</div>
<div>
<span>文件预览</span>
<div>
{Object.keys(files).map((fileName, index) => {
let file = files[fileName];
let isImageFile = file.type.split("/")[0] === "image";
return (
<div key={fileName}>
<div>
{isImageFile && (
<img
src={URL.createObjectURL(file)}
alt={`file preview ${index}`}
/>
)}
<div isImageFile={isImageFile}>
<span>{file.name}</span>
<span>
<span>{convertBytesToKB(file.size)} kb</span>
<i className="fas fa-trash-alt" />
</span>
</div>
</div>
</div>
);
})}
</div>
</div>
</>
);
};
export default FileUpload;
其中有几个细节需要注意:
-
input标签中title=""title属性在鼠标移至input标签上时会显示,所以把title属性的内容置为空字符串。 -
图片预览。 为了预览图片,我们可以采用
URL.createObjectURL方法。该方法接收一个File类型的对象,返回该文件的一个暂时URL,我们可以把这个 url 置为 img标签的src属性的值。
样式
完整的样式链接:完整样式
我们把 input 标签隐藏,并使用绝对定位,将上传组件固定到整个上传框中。你可能会有疑问,文件的拖拽上传是如何实现的?实际上,input 标签默认实现了拖拽上传。我们把 input 标签固定到整个上传框中,用户拖动文件到该区域就会自动上传。
功能实现
当点击上传按钮时,打开系统的文件选择窗口,需要用到 ref.current.click()
const handleUploadBtnClick = () => {
fileInputField.current.click();
};
处理文件上传
<FormField
type="file"
ref={fileInputField}
onChange={handleNewFileUpload}
title=""
value=""
{...otherProps}
/>
const handleNewFileUpload = (e) => {
const { files: newFiles } = e.target;
if (newFiles.length) {
let updatedFiles = addNewFiles(newFiles);
setFiles(updatedFiles);
callUpdateFilesCb(updatedFiles);
}
};
const addNewFiles = (newFiles) => {
for (let file of newFiles) {
if (file.size <= maxFileSizeInBytes) {
if (!otherProps.multiple) {
return { file };
}
files[file.name] = file;
}
}
return { ...files };
};
const convertNestedObjectToArray = (nestedObj) =>
Object.keys(nestedObj).map((key) => nestedObj[key]);
const callUpdateFilesCb = (files) => {
const filesAsArray = convertNestedObjectToArray(files);
updateFilesCb(filesAsArray);
};
你可能会注意到调用callUpdateFilesCb(updatedFiles)传递的参数是updatedFiles 而不是 files 状态中保存的值。这是因为 setFiles 是异步更新,你无法保证setFiles 先于callUpdateFilesCb 执行。
最终为们传递给父组件的文件是数组的形式。
删除文件
<RemoveFileIcon
className="fas fa-trash-alt"
onClick={() => removeFile(fileName)}
/>
const removeFile = (fileName) => {
delete files[fileName];
setFiles({ ...files });
callUpdateFilesCb({ ...files });
};
ok,代码部分已经完成,下面来看一下如何使用。
使用
在父组件使用中使用时,为们可以向该组件传递一些属性。
比如之前提到的 maxFileSizeInBytes, updateFilesCb 等。此外,我们还可以传递 input 标签的原生属性。例如 accept="image/*" 规定imput标签只能上传图片。multiple 属性规定可以上传多个组件。
完整代码链接
React 拖拽上传组件的实现 喜欢不妨点个 star ~