React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React文件上传操作与避坑

React实现文件上传的操作方法与避坑指南(预览URL回收、多文件逐个进度与拖拽放置)

作者:codedevin

文件上传看着简单,一个 <input type="file"> 而已,可真写起来到处是坑,预览图内存越用越多、选同一个文件第二次不触发、多文件上传进度只能看总量等等,所以这篇把这些坑一个个填平,最后给你一个能直接抄进项目的上传组件,需要的朋友可以参考下

文件上传看着简单,一个 <input type="file"> 而已,可真写起来到处是坑:预览图内存越用越多、选同一个文件第二次不触发、多文件上传进度只能看总量、拖拽区域松手时浏览器直接把文件当新页面打开了。这篇把这些坑一个个填平,最后给你一个能直接抄进项目的上传组件。

第一个坑:file input 到底是受控还是非受控

先说结论:<input type="file"> 只能是非受控的。你没法给它设 value(出于安全,浏览器不允许 JS 把一个文件路径塞进去),所以别想着用 useState 双向绑定它。

正确姿势是监听 onChange,自己把 File 对象存进 state:

import { useState } from "react";

function SimpleUpload() {
  const [file, setFile] = useState(null);

  function handleChange(e) {
    // e.target.files 是 FileList,不是数组
    const picked = e.target.files?.[0];
    if (picked) setFile(picked);
  }

  return (
    <div>
      <input type="file" accept="image/*" onChange={handleChange} />
      {file && <p>已选:{file.name}({(file.size / 1024).toFixed(1)} KB)</p>}
    </div>
  );
}

注意 e.target.files 是一个类数组的 FileList,想遍历得先 Array.from(e.target.files)。

第二个坑:选同一个文件第二次,onChange 不触发

用户选了 a.png,发现选错了想重新选还是 a.png(比如刚在本地改了图),结果 onChange 一声不吭。因为 input 的 value 没变化,浏览器认为「没变」就不触发事件。

解法:每次处理完把 input 的 value 清空,这样下次哪怕选同一个文件也算「从空变成有」:

function handleChange(e) {
  const picked = e.target.files?.[0];
  if (picked) setFile(picked);
  // 关键:清空,保证下次选同一个文件也触发 onChange
  e.target.value = "";
}

第三个坑:预览图会漏内存

要显示本地预览,常见做法是 URL.createObjectURL(file)。但很多人只创建、不释放,选十几张图后 blob 一直挂在内存里。createObjectURL 生成的 URL 会一直占用内存直到你手动 revokeObjectURL 或页面卸载。

正确做法:在 useEffect 里创建,并在清理函数里回收:

import { useState, useEffect } from "react";

function ImagePreview({ file }) {
  const [url, setUrl] = useState("");

  useEffect(() => {
    if (!file) return;
    const objectUrl = URL.createObjectURL(file);
    setUrl(objectUrl);

    // 清理:file 变了或组件卸载时,回收上一个 URL,避免内存泄漏
    return () => URL.revokeObjectURL(objectUrl);
  }, [file]);

  if (!file) return null;
  return <img src={url} alt="预览" style={{ maxWidth: 200 }} />;
}

千万别在 render 里直接 src={URL.createObjectURL(file)} —— 每次重渲染都会新建一个 URL,旧的全泄漏。

第四个坑:上传进度用 fetch 拿不到

fetch 到今天也没有靠谱的上传进度回调。想要进度条,老老实实用 XMLHttpRequest,它的 upload.onprogress 才给你上传字节数:

function uploadWithProgress(file, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const form = new FormData();
    form.append("file", file);

    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        onProgress(Math.round((e.loaded / e.total) * 100));
      }
    };
    xhr.onload = () =>
      xhr.status < 400 ? resolve(xhr.responseText) : reject(new Error(xhr.status));
    xhr.onerror = () => reject(new Error("network error"));

    xhr.open("POST", "/api/upload");
    xhr.send(form);
  });
}

多文件想显示每个文件各自的进度,就给每个 file 存一条独立进度,别只存一个总数。

第五个坑:拖拽放置区,松手直接打开了文件

不加处理的话,把文件拖进浏览器松手,浏览器会直接导航去打开这个文件,你的页面就没了。必须在 onDragOver 和 onDrop 里都 preventDefault(),缺一不可:

import { useState } from "react";

function DropZone({ onFiles }) {
  const [dragging, setDragging] = useState(false);

  function handleDrop(e) {
    e.preventDefault();      // 必须,否则浏览器打开文件
    setDragging(false);
    const files = Array.from(e.dataTransfer.files);
    if (files.length) onFiles(files);
  }

  return (
    <div
      onDragOver={(e) => {
        e.preventDefault();  // 必须,否则 drop 不会触发
        setDragging(true);
      }}
      onDragLeave={() => setDragging(false)}
      onDrop={handleDrop}
      style={{
        border: `2px dashed ${dragging ? "#4f8cff" : "#bbb"}`,
        padding: 40,
        textAlign: "center",
        color: "#666",
      }}
    >
      {dragging ? "松手上传" : "把文件拖到这里,或点击选择"}
    </div>
  );
}

onDragOver 里的 preventDefault 尤其容易漏 —— 少了它,onDrop 根本不会触发,你会一脸懵为什么松手没反应。

组装:多文件 + 预览 + 逐个进度

把上面几块拼起来,得到一个能用的上传器。核心是用一个数组管理每个文件的 { file, progress, status }:

import { useState } from "react";

function Uploader() {
  const [items, setItems] = useState([]); // { file, progress, status }

  function addFiles(files) {
    setItems((prev) => [
      ...prev,
      ...files.map((file) => ({ file, progress: 0, status: "ready" })),
    ]);
  }

  function updateItem(index, patch) {
    setItems((prev) =>
      prev.map((it, i) => (i === index ? { ...it, ...patch } : it))
    );
  }

  async function startUpload() {
    for (let i = 0; i < items.length; i++) {
      if (items[i].status !== "ready") continue;
      updateItem(i, { status: "uploading" });
      try {
        await uploadWithProgress(items[i].file, (p) =>
          updateItem(i, { progress: p })
        );
        updateItem(i, { status: "done", progress: 100 });
      } catch {
        updateItem(i, { status: "error" });
      }
    }
  }

  return (
    <div>
      <DropZone onFiles={addFiles} />
      <input
        type="file"
        multiple
        onChange={(e) => {
          addFiles(Array.from(e.target.files));
          e.target.value = ""; // 清空,支持重复选同一批
        }}
      />
      <ul>
        {items.map((it, i) => (
          <li key={i}>
            {it.file.name} — {it.status} {it.progress}%
          </li>
        ))}
      </ul>
      <button onClick={startUpload}>开始上传</button>
    </div>
  );
}

这里 key 用 index 是因为列表只追加、不做中间删除排序;如果你要支持删除某一项,请改用文件的唯一标识(比如 file.name + file.lastModified)当 key,否则删除后进度会错位。

小结

一句话记忆点:上传的坑八成在「浏览器默认行为」和「blob 内存」上 —— 记得清 value、回收 URL、拦默认导航,组件就稳了。

以上就是React实现文件上传的操作方法与避坑指南(预览URL回收、多文件逐个进度与拖拽放置)的详细内容,更多关于React文件上传操作与避坑的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文