发布于 2026-01-06 5 阅读
0

我是如何构建自定义图像文件上传器的

我是如何构建自定义图像文件上传器的

整个问题源于我尝试使用 Cloudinary 文件上传器时遇到的一些问题。在花了几个小时尝试配置但最终失败后,我决定为我正在进行的项目构建自己的自定义图像文件上传器。

一开始我完全不知道该怎么实现,但我知道我总能找到办法。(大多数开发者不都是这样吗?😀)

这是一个使用 Next.js 和 TypeScript 的 React 项目。您可能需要创建一个 Next.js 项目来跟随教程进行操作,并从 npm 安装一些库来快速上手⚾。

npm i react-dropzone
Enter fullscreen mode Exit fullscreen mode

让我们创建一个名为 `<component_name>` 的 React 组件UploadImageFiles(哎呀,我不太擅长起名字,请见谅😋)。以下是该组件的基本框架以及我们需要用到的一些状态:

export default function UploadImageFiles() {
  const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
  const [fileList, setFileList] = useState<FileUpload[]>([]);
  const [fileError, setFileError] = useState<string | null>(null);

return (
 <>
    <main>
       {/* some UIish 😄 JSX*/}
    </main>
 </>
)
}

Enter fullscreen mode Exit fullscreen mode

为了更深入地探讨这个问题,我喜欢把我的实现思路整理成一个列表。你可以把它看作是伪代码。🤷

  • 处理用户文件选择
  • 验证文件
  • 显示文件列表(我称之为暂存区🎬)

让我们借助 TypeScript 来处理文件选择,这样就不会破产了。😄

 function handleFileChange(e: ChangeEvent<HTMLInputElement>) {
    const inputFiles = Array.from(e.target.files || []);

    const validatedFiles: File[] = [];

    files.forEach((file) => {
      if (!file.type.startsWith("image/")) { // check for valid image file
        setFileError("File should be an image"); 
      } else if (file.size > 1024 * 1024 * 25) { // check for image size
        setFileError("File max size is 24MB");
      } else {
        validatedFiles.push(file);
      }
    });
    if (validatedFiles.length > 0) {
      setSelectedFiles(validatedFiles);
      setFileError(null);
    }
  }
Enter fullscreen mode Exit fullscreen mode

handleFileChange()函数用于文件选择,并验证文件类型和图像文件大小是否符合预期。如果不符合,则会设置 fileError 状态。如果所有检查都通过,则将文件添加到 validatedFiles 数组中。代码的最后一部分使用 setSelectedFiles 操作和 validatedFiles 列表来设置 selectedFiles 状态。

🚫🚫🚫 文件上传功能在哪里?

干得好:

interface FileUpload {
  url: string;
  name: string;
  size: number;
}
Enter fullscreen mode Exit fullscreen mode

文件选择和验证完成后,我们来做一些准备工作……🎬

为了向暂存区添加新文件,我们的暂存功能将位于 useEffect 中,以便在选中新文件时将其添加进去。

useEffect(() => {
    function stagingFiles() {
      if (selectedFiles.length) {
        const imageFile = selectedFiles.map((file: File) => {
          return {
            url: URL.createObjectURL(file),
            name: file.name,
            size: file.size,
          };
        });
        setFileList((curState) => {
          const newImageFile = imageFile.filter((file) => !curState.some((item) => item.name === file.name))
          return [...curState, ...newImageFile];
        });
      }
    }
    stagingFiles();
  }, [selectedFiles]);
Enter fullscreen mode Exit fullscreen mode

在暂存区,我们需要图片 URL、名称和尺寸。我们使用 URL 来显示图片。由于我们的数据selectedFiles只是一个图片列表,我们遍历它并从中返回所需的属性(如上所述),将其作为对象返回。`createObjectURL ()` 函数URL.createObjectURL()有助于创建图片 URL

setFileList((curState) => {
          const newImageFile = imageFile.filter((file) => !curState.some((item) => item.name === file.name))
          return [...curState, ...newImageFile];
        });
Enter fullscreen mode Exit fullscreen mode

暂存功能的第二部分是设置一个独特的图像,确保不会向暂存区添加重复的图像文件。

不要忘记将其添加selectedFilesuseEffect依赖项数组中。

我们可能需要从暂存区删除一些文件。方法快捷简便:

function removeFromList(item: number) {
    setFileList((curState) => {
      return curState.filter((_, index) => index !== item);
    });
  }

Enter fullscreen mode Exit fullscreen mode

您可能想知道,我们的拖放逻辑在哪里?稍等片刻……

const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop: (files: File[]) => {
      processFiles(files);
    },
    multiple: true,
    noClick: true, // avoid duplicate opening of file selection ui
  });
Enter fullscreen mode Exit fullscreen mode

React Dropzone 为我们提供了一个useDropzone实现拖放功能的钩子。通过这个钩子,我们可以解构所需的属性。更多信息请查看其官方文档:react-dropzone。

我们将对现有handleFileChange函数进行一些更新,将其部分功能移至另一个processFiles函数中:

 function processFiles(files: File[]) {
    const validatedFiles: File[] = [];

    files.forEach((file) => {
      if (!file.type.startsWith("image/")) {
        setFileError("File should be an image");
      } else if (file.size > 1024 * 1024 * 25) {
        setFileError("File max size is 24MB");
      } else {
        validatedFiles.push(file);
      }
    });
    if (validatedFiles.length > 0) {
      setSelectedFiles(validatedFiles);
      setFileError(null);
    }
  }
Enter fullscreen mode Exit fullscreen mode

最后再做两处修改。分别在根元素和输入元素中添加 `<head> getRootProps` 和`<body>`:getInputProps

  return (
    <div>
      <div {...getRootProps()}>
        <input {...getInputProps()} onChange={handleFileChange} />
        {isDragActive ? <p>Drop the files here ...</p> : <p>Drag 'n' drop some files here, or click to select files</p>}
      </div>
      {fileError && <p>{fileError}</p>}
      <ul>
        {fileList.map((file, index) => (
          <li key={index}>
            <img src={file.url} alt={file.name} width={50} />
            <button onClick={() => removeFromList(index)}>Remove</button>
          </li>
        ))}
      </ul>
    </div>
  );
Enter fullscreen mode Exit fullscreen mode

这是我目前的实现版本。

图片描述

希望你和我一样玩得开心。欢迎你添加自己的功能。

文章来源:https://dev.to/bolajbolajoko51/how-i-built-a-custom-image-file-uploader-5aje