具有拖放功能的响应式 React 文件上传组件

2025-06-08

具有拖放功能的响应式 React 文件上传组件

在开发一个 React 项目时,我实现了一个响应式文件上传组件,该组件无需使用任何库即可支持拖放功能。网上大多数文件上传组件都使用诸如react-dropzone之类的库来支持拖放功能。因此,我想分享一下我是如何制作这个组件的,并展示一个典型的用例。

最终结果

图 1:表单中使用的文件上传组件(示例用例)

图2:响应式文件上传组件

特点包括:

  • 无需使用任何库即可进行拖放
  • 显示图像文件的图像预览
  • 显示文件大小和名称
  • 删除“待上传”部分中的文件
  • 阻止用户上传大于指定大小的文件
    • 注意:出于安全原因,这也应该在后端完成

项目设置

先决条件:Node(用于安装 npm 包)

如果你熟悉构建 React 应用程序,那么设置新 React 项目最简单的方法是使用create-react-app。因此,在终端/命令行中运行以下命令:

npx create-react-app react-file-upload
cd react-file-upload
Enter fullscreen mode Exit fullscreen mode

为了确保运行后一切都设置正确,当您在浏览器中npm start访问时应该出现以下内容:localhost:3000

替代文本

在构建组件之前,让我们修改和删除一些文件以摆脱不必要的代码。

  • 更改App.js为以下内容:
import React from 'react';

function App() {
  return (
    <div></div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode
  • 更改index.js为以下内容:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

删除src文件夹中除以下文件之外的所有文件

  • App.js
  • index.js
  • index.css

文件上传组件

安装依赖项

我们需要的依赖项是:

样式组件

  • 用于设置组件的样式
    • 样式组件允许样式封装并通过 props 创建动态样式

节点-sass

  • 用于编译样式组件中使用的 Sass 样式(可选,可以使用 CSS)

要安装它们,请运行npm i styled-components node-sass

文件夹结构

构建文件夹和文件的一个好习惯是创建一个 components 文件夹,每个组件都对应一个文件夹。这样可以更轻松地找到每个组件的逻辑和样式。

按照此约定,在文件夹创建一个组件文件夹src,然后在文件夹中创建一个文件上传文件夹components

最后,在文件上传文件夹中,创建 2 个新文件。

  • file-upload.component.jsx
  • file-upload.styles.js

状态

由于我们正在创建一个功能组件并且需要使用状态,因此我们将使用useState 钩子

useState 钩子返回一个状态值,该值与作为第一个参数传递的值相同,以及一个用于更新它的函数。

为了达到我们的目的,我们需要状态来跟踪上传的文件。因此,在file-upload.component.jsx文件中添加以下内容:

import React, { useState } from "react";

const FileUpload = () => {
  const [files, setFiles] = useState({});

  return (
   <div></div>
  )
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

“我们不应该使用空数组而不是空对象来表示files状态吗?”

使用对象可以让我们轻松地操作(添加/删除)files状态,并防止同名文件被多次上传。以下是状态的示例files

{
 "file1.png": File,
 "file2.png": File
}
Enter fullscreen mode Exit fullscreen mode

如果使用数组,则需要更多工作。例如,要删除一个文件,我们必须遍历每个文件,直到找到要删除的文件。

注意:File 是一个 JS 对象。更多信息请访问https://developer.mozilla.org/en-US/docs/Web/API/File

useRef 钩子

如果你查看上面的图 1,你会注意到用户可以拖放文件,也可以点击“上传文件”按钮。默认情况下,点击文件输入标签后会打开文件资源管理器。但是,我们希望在点击“上传文件”按钮后打开文件资源管理器,因此我们需要一个指向文件输入标签的 DOM 引用。

要创建 DOM 引用,我们将使用useRef 钩子。 useRef 钩子返回一个可变的 ref 对象,该对象.current属性指向一个 DOM 节点(在本例中为文件输入标签)。

一旦我们使用 useRef 钩子,我们必须将返回值传递给文件输入标签的 ref 属性,如下所示:

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;
Enter fullscreen mode Exit fullscreen mode

道具

该组件将具有以下属性:

  • label
    • 确定组件的标签(例如,上图 1 中的“配置文件图像”)
  • maxFileSizeInBytes
    • 防止上传超过指定大小的文件
  • updateFilesCb
    • 用于将files状态发送给父组件的回调函数

“为什么我们需要将files状态发送给父组件?”

通常,文件上传组件会在表单中使用,在 React 中使用表单时,组件会将表单数据存储在 state 中。因此,为了让父组件也能存储上传的文件,我们需要文件上传组件来发送它。

“为什么我们需要使用回调函数将files状态发送给父组件?”

由于 React 具有单向数据流,我们无法轻松地将数据从子组件(文件上传组件)传递到父组件。为了解决这个问题,我们将传递一个在父组件中声明的函数,然后文件上传组件将使用状态files作为参数来调用该函数。关于如何将数据从子组件发送到父组件的更多解释,请参阅https://medium.com/@jasminegump/passing-data-between-a-parent-and-child-in-react-deea2ec8e654

使用解构,我们现在可以像这样添加 props:

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;
Enter fullscreen mode Exit fullscreen mode

“为什么我们在解构时要使用扩展语法otherProps?”

在解构时,我们可以将未明确解构的所有其他值分配给变量。

let props = { a: 1, b: 2, c: 3};
let {a, ...otherProps} = props;

//a = 1
//otherProps = {b: 2, c: 3};
Enter fullscreen mode Exit fullscreen mode

在这种情况下,任何未经解构的 props 都会被赋值给该otherProps变量。稍后我们会看到这个变量的用法otherProps

HTML

对于图 1 所示的图标,我们将使用 Font Awesome。要导入它,请在文件head 标签public/index.html中添加以下内容:

<link
 rel="stylesheet"
 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css"
/>
Enter fullscreen mode Exit fullscreen mode

从图 1 可以看出,我们可以将组件的 HTML 分为两个主要部分。

部件主要零件

这是包含第一部分 HTML 的组件:

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 (
      <section>
        <label>{label}</label>
        <p>Drag and drop your files anywhere or</p>
        <button type="button">
          <i className="fas fa-file-upload" />
          <span> Upload {otherProps.multiple ? "files" : "a file"}</span>
        </button>
        <input
          type="file"
          ref={fileInputField}
          title=""
          value=""
          {...otherProps}
        />
      </section>      
  );
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

之前我们讨论过,任何未经解构的 props 都会被赋值给变量(即除otherProps之外的任何 prop )。在上面的代码中,我们将该变量传递给文件输入标签。这样做是为了让我们能够通过 props 从父组件向文件输入标签添加属性。labelupdateFilesCbmaxFileSizeInBytesotherProps

“为什么我们要将标题和值属性设置为""?”

设置标题属性可以""删除鼠标悬停在输入标签上时默认显示的文本(“未选择文件”)。

将 value 属性设置为 可以""修复一个极端情况:在删除文件后立即上传文件不会改变files状态。稍后我们将看到,files只有当 input 标签的值发生变化时,状态才会改变。出现此错误是因为当我们删除文件时,input 标签的值不会改变。由于状态更改会重新渲染 HTML,因此将 value 属性设置为 会""在每次状态更改时重置 input 标签的值files

在编写第二部分的 HTML 之前,请记住 React 只允许从组件返回一个父元素<></>。因此,我们将两个部分都放在一个标签中。

以下是包含两个部分的 HTML 的组件:

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 (
    <>
      <section>
        <label>{label}</label>
        <p>Drag and drop your files anywhere or</p>
        <button type="button">
          <i className="fas fa-file-upload" />
          <span> Upload {otherProps.multiple ? "files" : "a file"}</span>
        </button>
        <input
          type="file"
          ref={fileInputField}
          title=""
          value=""
          {...otherProps}
        />
      </section>

      {/*second part starts here*/}
      <article>
        <span>To Upload</span>
        <section>
          {Object.keys(files).map((fileName, index) => {
            let file = files[fileName];
            let isImageFile = file.type.split("/")[0] === "image";
            return (
              <section key={fileName}>
                <div>
                  {isImageFile && (
                    <img
                      src={URL.createObjectURL(file)}
                      alt={`file preview ${index}`}
                    />
                  )}
                  <div isImageFile={isImageFile}>
                    <span>{file.name}</span>
                    <aside>
                      <span>{convertBytesToKB(file.size)} kb</span>
                      <i className="fas fa-trash-alt" />
                    </aside>
                  </div>
                </div>
              </section>
            );
          })}
        </section>
      </article>
    </>
  );
};

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

在 HTML 的第二部分,我们遍历状态中的每个文件并显示文件名、大小(以 KB 为单位)以及文件类型(即 png、jpg 等)的files图像预览。image/*

为了显示图像预览,我们使用了URL.createObjectURLcreateObjectURL 函数。createObjectURL 函数接受一个对象(在本例中是一个File对象),并返回一个用于访问该文件的临时 URL。然后,我们可以将该 URL 设置src为 img 标签的属性。

造型

我们现在将使用之前安装的 styled-components 包。

在文件中添加以下内容file-upload.styles.js

import styled from "styled-components";

export const FileUploadContainer = styled.section`
  position: relative;
  margin: 25px 0 15px;
  border: 2px dotted lightgray;
  padding: 35px 20px;
  border-radius: 6px;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: white;
`;

export const FormField = styled.input`
  font-size: 18px;
  display: block;
  width: 100%;
  border: none;
  text-transform: none;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0;

  &:focus {
    outline: none;
  }
`;

export const InputLabel = styled.label`
  top: -21px;
  font-size: 13px;
  color: black;
  left: 0;
  position: absolute;
`;

export const DragDropText = styled.p`
  font-weight: bold;
  letter-spacing: 2.2px;
  margin-top: 0;
  text-align: center;
`;

export const UploadFileBtn = styled.button`
  box-sizing: border-box;
  appearance: none;
  background-color: transparent;
  border: 2px solid #3498db;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1;
  padding: 1.1em 2.8em;
  text-align: center;
  text-transform: uppercase;
  font-weight: 700;
  border-radius: 6px;
  color: #3498db;
  position: relative;
  overflow: hidden;
  z-index: 1;
  transition: color 250ms ease-in-out;
  font-family: "Open Sans", sans-serif;
  width: 45%;
  display: flex;
  align-items: center;
  padding-right: 0;
  justify-content: center;

  &:after {
    content: "";
    position: absolute;
    display: block;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 0;
    height: 100%;
    background: #3498db;
    z-index: -1;
    transition: width 250ms ease-in-out;
  }

  i {
    font-size: 22px;
    margin-right: 5px;
    border-right: 2px solid;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    width: 20%;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  @media only screen and (max-width: 500px) {
    width: 70%;
  }

  @media only screen and (max-width: 350px) {
    width: 100%;
  }

  &:hover {
    color: #fff;
    outline: 0;
    background: transparent;

    &:after {
      width: 110%;
    }
  }

  &:focus {
    outline: 0;
    background: transparent;
  }

  &:disabled {
    opacity: 0.4;
    filter: grayscale(100%);
    pointer-events: none;
  }
`;

export const FilePreviewContainer = styled.article`
  margin-bottom: 35px;

  span {
    font-size: 14px;
  }
`;

export const PreviewList = styled.section`
  display: flex;
  flex-wrap: wrap;
  margin-top: 10px;

  @media only screen and (max-width: 400px) {
    flex-direction: column;
  }
`;

export const FileMetaData = styled.div`
  display: ${(props) => (props.isImageFile ? "none" : "flex")};
  flex-direction: column;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 10px;
  border-radius: 6px;
  color: white;
  font-weight: bold;
  background-color: rgba(5, 5, 5, 0.55);

  aside {
    margin-top: auto;
    display: flex;
    justify-content: space-between;
  }
`;

export const RemoveFileIcon = styled.i`
  cursor: pointer;

  &:hover {
    transform: scale(1.3);
  }
`;

export const PreviewContainer = styled.section`
  padding: 0.25rem;
  width: 20%;
  height: 120px;
  border-radius: 6px;
  box-sizing: border-box;

  &:hover {
    opacity: 0.55;

    ${FileMetaData} {
      display: flex;
    }
  }

  & > div:first-of-type {
    height: 100%;
    position: relative;
  }

  @media only screen and (max-width: 750px) {
    width: 25%;
  }

  @media only screen and (max-width: 500px) {
    width: 50%;
  }

  @media only screen and (max-width: 400px) {
    width: 100%;
    padding: 0 0 0.4em;
  }
`;

export const ImagePreview = styled.img`
  border-radius: 6px;
  width: 100%;
  height: 100%;
`;
Enter fullscreen mode Exit fullscreen mode

使用 styled-components 时,我们创建的组件会渲染带有某些样式的 HTML 标签。例如,ImagePreview是一个组件,它会渲染带有标签模板字面量img中样式的标签

由于我们正在创建组件,因此我们可以将 props 传递给它并在编写样式时访问它(例如FileMetaData上面的示例)。

我们现在已经完成了样式设置和添加拖放功能。

“但是等等,我们什么时候添加拖放功能?”

默认情况下,文件输入标签支持拖放。我们只需设置输入标签的样式,并使其绝对定位即可(参考FormField上文)。

要使用我们编写的样式,请导入所有样式组件并替换file-upload.component.jsx文件中的 HTML。

import React, { useRef, useState } from "react";
import {
  FileUploadContainer,
  FormField,
  DragDropText,
  UploadFileBtn,
  FilePreviewContainer,
  ImagePreview,
  PreviewContainer,
  PreviewList,
  FileMetaData,
  RemoveFileIcon,
  InputLabel
} from "./file-upload.styles";

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 (
    <>
      <FileUploadContainer>
        <InputLabel>{label}</InputLabel>
        <DragDropText>Drag and drop your files anywhere or</DragDropText>
        <UploadFileBtn type="button">
          <i className="fas fa-file-upload" />
          <span> Upload {otherProps.multiple ? "files" : "a file"}</span>
        </UploadFileBtn>
        <FormField
          type="file"
          ref={fileInputField}
          title=""
          value=""
          {...otherProps}
        />
      </FileUploadContainer>
      <FilePreviewContainer>
        <span>To Upload</span>
        <PreviewList>
          {Object.keys(files).map((fileName, index) => {
            let file = files[fileName];
            let isImageFile = file.type.split("/")[0] === "image";
            return (
              <PreviewContainer key={fileName}>
                <div>
                  {isImageFile && (
                    <ImagePreview
                      src={URL.createObjectURL(file)}
                      alt={`file preview ${index}`}
                    />
                  )}
                  <FileMetaData isImageFile={isImageFile}>
                    <span>{file.name}</span>
                    <aside>
                      <span>{convertBytesToKB(file.size)} kb</span>
                      <RemoveFileIcon
                        className="fas fa-trash-alt"
                      />
                    </aside>
                  </FileMetaData>
                </div>
              </PreviewContainer>
            );
          })}
        </PreviewList>
      </FilePreviewContainer>
    </>
  );
}

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

功能

我们几乎完成了文件上传组件,我们只需要添加函数以便files可以修改状态。

之前我们使用 useRef 钩子创建了一个 DOM 引用。现在,我们将使用它在点击“上传文件”按钮时打开文件资源管理器。为此,请在组件中添加以下函数:

const handleUploadBtnClick = () => {
  fileInputField.current.click();
};
Enter fullscreen mode Exit fullscreen mode

我们还需要给组件添加一个onClick属性UploadFileBtn来触发上述功能。

<UploadFileBtn type="button" onClick={handleUploadBtnClick}>
Enter fullscreen mode Exit fullscreen mode

为了处理用户点击“上传文件”按钮后选择的文件,我们需要onChange向组件添加一个属性FormField

<FormField
  type="file"
  ref={fileInputField}
  onChange={handleNewFileUpload}
  title=""
  value=""
  {...otherProps}
/>
Enter fullscreen mode Exit fullscreen mode

与任何 DOM 事件(例如 )一样onClick,处理该事件的函数将能够访问该事件对象。因此,该handleNewFileUpload函数将以该事件对象作为其第一个参数。

 const handleNewFileUpload = (e) => {
    const { files: newFiles } = e.target;
    if (newFiles.length) {
      let updatedFiles = addNewFiles(newFiles);
      setFiles(updatedFiles);
      callUpdateFilesCb(updatedFiles);
    }
  };
Enter fullscreen mode Exit fullscreen mode

在上面的函数中,我们从属性中访问用户选择的文件e.target.files,然后将其传递给名为的函数addNewFiles。然后,我们获取返回值addNewFiles并将其传递给以setFiles更新files状态。由于状态的任何更改都files必须发送到父组件,因此我们需要调用该callUpdateFilesCb函数。

addNewFiles函数接受一个FileList对象(e.target.files上面返回一个 FileList),对其进行迭代,并返回一个对象,其中键是文件名,值是 File 对象。

  const addNewFiles = (newFiles) => {
    for (let file of newFiles) {
      if (file.size <= maxFileSizeInBytes) {
        if (!otherProps.multiple) {
          return { file };
        }
        files[file.name] = file;
      }
    }
    return { ...files };
  };
Enter fullscreen mode Exit fullscreen mode

“如果没有multiple属性,为什么要检查otherProps?”

如前所述,我们使用otherProps变量为文件输入标签添加属性。因此,如果我们不multiple向文件上传组件传递 prop,则文件输入标签将无法选择多个文件。简而言之,如果传入multipleprop,则所选文件将被添加到files状态中。否则,选择新文件将删除先前的files状态,并用新选择的文件替换它。

callUpdateFilesCb函数获取从 返回的值addNewFiles,将files状态转换为数组并调用该updateFilesCb函数(从 props)。

“既然我们可以在函数内使用状态,为什么还要传递updatedFiles给它呢?”callUpdateFilesCbfiles

由于 React 状态更新是异步的,因此无法保证在callUpdateFilesCb调用时files状态会发生变化。

“为什么我们必须将files状态转换为数组?”

我们不这么做!但是,当将处于files状态的文件上传到某些第三方服务(例如 Firebase 云存储)时,使用数组会更容易。

const convertNestedObjectToArray = (nestedObj) =>
  Object.keys(nestedObj).map((key) => nestedObj[key]);

const callUpdateFilesCb = (files) => {
  const filesAsArray = convertNestedObjectToArray(files);
  updateFilesCb(filesAsArray);
};
Enter fullscreen mode Exit fullscreen mode

要删除文件,我们首先需要onClick向组件添加一个属性RemoveFileIcon

<RemoveFileIcon
  className="fas fa-trash-alt"
  onClick={() => removeFile(fileName)}
/>
Enter fullscreen mode Exit fullscreen mode

removeFile函数将获取一个文件名,将其从files状态中删除,更新files状态,并将更改通知父组件。

const removeFile = (fileName) => {
  delete files[fileName];
  setFiles({ ...files });
  callUpdateFilesCb({ ...files });
};
Enter fullscreen mode Exit fullscreen mode

这是具有上述所有功能的组件:

import React, { useRef, useState } from "react";
import {
  FileUploadContainer,
  FormField,
  DragDropText,
  UploadFileBtn,
  FilePreviewContainer,
  ImagePreview,
  PreviewContainer,
  PreviewList,
  FileMetaData,
  RemoveFileIcon,
  InputLabel
} from "./file-upload.styles";

const KILO_BYTES_PER_BYTE = 1000;
const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 500000;

const convertNestedObjectToArray = (nestedObj) =>
  Object.keys(nestedObj).map((key) => nestedObj[key]);

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({});

  const handleUploadBtnClick = () => {
    fileInputField.current.click();
  };

  const addNewFiles = (newFiles) => {
    for (let file of newFiles) {
      if (file.size < maxFileSizeInBytes) {
        if (!otherProps.multiple) {
          return { file };
        }
        files[file.name] = file;
      }
    }
    return { ...files };
  };

  const callUpdateFilesCb = (files) => {
    const filesAsArray = convertNestedObjectToArray(files);
    updateFilesCb(filesAsArray);
  };

  const handleNewFileUpload = (e) => {
    const { files: newFiles } = e.target;
    if (newFiles.length) {
      let updatedFiles = addNewFiles(newFiles);
      setFiles(updatedFiles);
      callUpdateFilesCb(updatedFiles);
    }
  };

  const removeFile = (fileName) => {
    delete files[fileName];
    setFiles({ ...files });
    callUpdateFilesCb({ ...files });
  };

  return (
    <>
      <FileUploadContainer>
        <InputLabel>{label}</InputLabel>
        <DragDropText>Drag and drop your files anywhere or</DragDropText>
        <UploadFileBtn type="button" onClick={handleUploadBtnClick}>
          <i className="fas fa-file-upload" />
          <span> Upload {otherProps.multiple ? "files" : "a file"}</span>
        </UploadFileBtn>
        <FormField
          type="file"
          ref={fileInputField}
          onChange={handleNewFileUpload}
          title=""
          value=""
          {...otherProps}
        />
      </FileUploadContainer>
      <FilePreviewContainer>
        <span>To Upload</span>
        <PreviewList>
          {Object.keys(files).map((fileName, index) => {
            let file = files[fileName];
            let isImageFile = file.type.split("/")[0] === "image";
            return (
              <PreviewContainer key={fileName}>
                <div>
                  {isImageFile && (
                    <ImagePreview
                      src={URL.createObjectURL(file)}
                      alt={`file preview ${index}`}
                    />
                  )}
                  <FileMetaData isImageFile={isImageFile}>
                    <span>{file.name}</span>
                    <aside>
                      <span>{convertBytesToKB(file.size)} kb</span>
                      <RemoveFileIcon
                        className="fas fa-trash-alt"
                        onClick={() => removeFile(fileName)}
                      />
                    </aside>
                  </FileMetaData>
                </div>
              </PreviewContainer>
            );
          })}
        </PreviewList>
      </FilePreviewContainer>
    </>
  );
};

export default FileUpload;
Enter fullscreen mode Exit fullscreen mode

用例

现在让我们使用 App 组件中的文件上传组件来查看它的实际运行情况!

在该App.js文件中,我们将创建一个简单的表单并添加状态来存储表单数据。

import React, { useState } from "react";

function App() {
  const [newUserInfo, setNewUserInfo] = useState({
    profileImages: []
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    //logic to create a new user...
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <button type="submit">Create New User</button>
      </form>
    </div>
  );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

现在添加文件上传组件。

import React, { useState } from "react";
import FileUpload from "./components/file-upload/file-upload.component";

function App() {
  const [newUserInfo, setNewUserInfo] = useState({
    profileImages: []
  });

  const handleSubmit = (event) => {
    event.preventDefault();
    //logic to create a new user...
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <FileUpload
          accept=".jpg,.png,.jpeg"
          label="Profile Image(s)"
          multiple
        />
        <button type="submit">Create New User</button>
      </form>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

注意,我们还没有添加prop。在此之前,我们需要创建一个只更新 state属性的updateFilesCb函数profileImagesnewUserInfo

const updateUploadedFiles = (files) =>
    setNewUserInfo({ ...newUserInfo, profileImages: files });
Enter fullscreen mode Exit fullscreen mode

我们现在将此函数作为updateFilesCbprop 传递。因此,每当文件上传组件的状态发生变化时files,它都会保存在状态profileImages的属性中newUserInfo

import React, { useState } from "react";
import FileUpload from "./components/file-upload/file-upload.component";

function App() {
  const [newUserInfo, setNewUserInfo] = useState({
    profileImages: []
  });

  const updateUploadedFiles = (files) =>
    setNewUserInfo({ ...newUserInfo, profileImages: files });

  const handleSubmit = (event) => {
    event.preventDefault();
    //logic to create new user...
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <FileUpload
          accept=".jpg,.png,.jpeg"
          label="Profile Image(s)"
          multiple
          updateFilesCb={updateUploadedFiles}
        />
        <button type="submit">Create New User</button>
      </form>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

“为什么我们要将acceptandmultiple属性传递给文件上传组件?”

由于任何额外的道具都会传递给文件输入标签,因此文件输入标签将具有acceptandmultiple属性。

multiple属性允许用户在文件资源管理器中选择多个文件。

accept属性可防止用户选择与指定文件类型不同的文件类型(在本例中为 jpg、png、jpeg)。

现在我们已经完成了,运行npm start并访问 localhost:3000。应该会出现以下内容:

在表单中使用文件上传组件


作为参考,可以在https://github.com/Chandra-Panta-Chhetri/react-file-upload-tutorial找到代码

鏂囩珷鏉ユ簮锛�https://dev.to/chandrapantachhetri/responsive-react-file-upload-component-with-drag-and-drop-4ef8
PREV
Hello Hadoop | 几分钟轻松学会Hadoop!
NEXT
What is Big-O Notation? Understand Time and Space Complexity in JavaScript.