如何在React中实现拖拽上传组件并实时显示上传进度?

来源:Ruby教程作者:狼行天下头衔:草根站长
导读:本期聚焦于狼行天下创作的《如何在React中实现拖拽上传组件并实时显示上传进度?》,敬请观看详情。想给React项目加一个拖拽上传区域,但不知道如何结合原生Drag and Drop API和XMLHttpRequest的进度事件?这篇文章从零搭建一个完整的拖拽上传组件,覆盖dragover、drop等事件的处理细节,并且利用axios或原生XHR的onUploadProgress回调来动态更新进度条。你会看到如何阻止浏览器默认打开文件的行为、如何优雅地管理拖拽状态样式,以及如何处理多文件上传、上传取消和错误重试。代码示例可直接复制到React函数组件中使用,同时解释了为什么在React中需要借助ref来避免事件监听带来的闭包陷阱。读完就能在自己的管理后台或图床工具中落地一个流畅的拖拽上传体验。

如何在React中实现拖拽上传组件并实时显示上传进度?

从浏览器原生拖拽事件说起

React并没有内置拖拽上传的组件,通常需要结合浏览器提供的Drag and Drop API来实现。核心事件有四个:dragenter、dragover、dragleave和drop。其中dragover必须调用preventDefault(),否则浏览器会默认在新窗口打开拖入的文件,导致上传逻辑无法触发。另外drop事件也需要阻止默认行为,并通过event.dataTransfer.files拿到用户拖入的文件列表。

在React函数组件中,直接给div元素写onDragOver={handleDragOver}这样的内联事件处理器没有任何问题,但需要注意一个细节:拖拽状态通常需要一个布尔值来控制高亮样式。许多开发者会习惯性地在dragenter时设置setDragging(true),在dragleave时设置setDragging(false)。但是当鼠标从拖拽区域的一个子元素移动到另一个子元素时,dragleave和dragenter会连续触发,导致高亮闪烁。为了避免这个问题,可以使用一个计数器ref来记录当前进入拖拽区域的深度,只有深度归零时才取消高亮。

下面是一段处理拖拽状态的示例代码,使用useRef保存计数器,避免频繁setState带来的性能损耗。

import React, { useRef, useState } from 'react';

function DragUploadArea() {
  const [isDragging, setIsDragging] = useState(false);
  const dragCounter = useRef(0);
  const inputRef = useRef(null);

  const handleDragEnter = (e) => {
    e.preventDefault();
    dragCounter.current += 1;
    setIsDragging(true);
  };

  const handleDragLeave = (e) => {
    e.preventDefault();
    dragCounter.current -= 1;
    if (dragCounter.current === 0) {
      setIsDragging(false);
    }
  };

  const handleDragOver = (e) => {
    e.preventDefault(); // 必须调用,否则无法触发drop
    e.dataTransfer.dropEffect = 'copy';
  };

  const handleDrop = (e) => {
    e.preventDefault();
    dragCounter.current = 0;
    setIsDragging(false);
    const files = e.dataTransfer.files;
    if (files.length > 0) {
      uploadFiles(files);
    }
  };

  return (
    <div
      onDragEnter={handleDragEnter}
      onDragLeave={handleDragLeave}
      onDragOver={handleDragOver}
      onDrop={handleDrop}
      style={{
        border: '2px dashed #ccc',
        padding: '40px',
        textAlign: 'center',
        background: isDragging ? '#e6f7ff' : '#fff',
        transition: 'background 0.2s'
      }}
    >
      将文件拖拽到这里,或
      <button onClick={() => inputRef.current.click()}>点击选择文件</button>
      <input
        ref={inputRef}
        type="file"
        multiple
        style={{ display: 'none' }}
        onChange={(e) => uploadFiles(e.target.files)}
      />
    </div>
  );
}

上述代码中dragCounter的用法是拖拽组件常见的防抖处理。当用户拖入子元素时,dragenter先触发并加一,随后dragleave也会触发并减一,但不会归零,所以高亮不会消失。只有当鼠标真正离开整个上传区域,计数器才会变为0,此时才取消高亮。同时为了支持点击上传,嵌入了一个隐藏的<input type="file">并通过ref触发点击。

结合XMLHttpRequest实现上传进度显示

拖拽到位之后,需要将文件上传到服务器并显示进度条。虽然可以使用fetch API,但fetch目前不支持上传进度事件,所以最直接的方式是使用XMLHttpRequest。XHR提供了upload.onprogress事件,可以在上传过程中获取loaded和total两个值,从而计算百分比。如果项目中使用axios,也可以利用axios的onUploadProgress配置项,它底层同样基于XHR。

实现进度条的关键是在React状态中维护每个文件的上传进度。由于可能同时上传多个文件,建议使用一个对象或数组来保存每个文件对应的进度。在uploadFiles函数中遍历文件列表,为每个文件创建一个FormData对象,然后发起独立的XHR请求。每个请求的onprogress回调中更新对应文件的进度state。这里需要谨慎处理闭包问题:如果直接在回调中引用循环变量,会出现所有文件进度都指向最后一个文件的情况。解决方案是为每个文件创建一个独立的函数作用域,或者使用let配合块级作用域。

function uploadFiles(files) {
  const fileArray = Array.from(files);
  fileArray.forEach((file, index) => {
    const formData = new FormData();
    formData.append('file', file);
    formData.append('filename', file.name);

    // 初始化该文件的进度为0
    setProgressMap(prev => ({
      ...prev,
      [file.name]: 0
    }));

    const xhr = new XMLHttpRequest();
    xhr.open('POST', 'https://api.ipipp.com/upload');

    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const percent = Math.round((event.loaded / event.total) * 100);
        setProgressMap(prev => ({
          ...prev,
          [file.name]: percent
        }));
      }
    };

    xhr.onload = () => {
      if (xhr.status === 200) {
        setProgressMap(prev => ({
          ...prev,
          [file.name]: 100
        }));
        console.log(`${file.name} 上传成功`);
      } else {
        setProgressMap(prev => ({
          ...prev,
          [file.name]: -1 // -1表示失败
        }));
      }
    };

    xhr.onerror = () => {
      setProgressMap(prev => ({
        ...prev,
        [file.name]: -1
      }));
    };

    xhr.send(formData);
  });
}

进度条的UI可以用div嵌套实现,外层是灰色背景,内层div的宽度根据进度百分比动态设置。为了更好的用户体验,还可以在上传过程中显示文件名和百分比数字。当上传失败(进度为-1)时,用红色文字提示错误,并提供重试按钮。

需要注意,xhr.upload.onprogress的回调中event.total可能是0或者不准确,所以要用event.lengthComputable判断是否可计算。另外如果上传速度很快,progress事件可能只触发一两次,百分比直接从0跳到100,这是正常现象。

组件化的进度列表与状态管理优化

把上传区域和进度列表拆分成两个组件会让代码更清晰:DragUploadArea负责交互和文件选择,UploadProgressList负责展示每个文件的上传状态和进度条。父组件维护一个包含文件信息(名称、大小、进度、状态)的数组,并通过props传递给进度列表组件。

为了管理更复杂的状态,可以使用useReducer替代多个useState。例如定义action类型:ADD_FILES、UPDATE_PROGRESS、UPLOAD_SUCCESS、UPLOAD_FAIL。reducer根据action更新对应的文件记录。这样做的好处是所有状态变更集中处理,避免多个setState之间的竞态问题。

const initialState = { files: [] };

function uploadReducer(state, action) {
  switch (action.type) {
    case 'ADD_FILES':
      const newFiles = action.payload.map(file => ({
        id: `${file.name}_${Date.now()}`,
        name: file.name,
        size: file.size,
        progress: 0,
        status: 'pending'
      }));
      return { files: [...state.files, ...newFiles] };
    case 'UPDATE_PROGRESS':
      return {
        files: state.files.map(f =>
          f.id === action.payload.id
            ? { ...f, progress: action.payload.progress, status: 'uploading' }
            : f
        )
      };
    case 'UPLOAD_SUCCESS':
      return {
        files: state.files.map(f =>
          f.id === action.payload.id
            ? { ...f, progress: 100, status: 'success' }
            : f
        )
      };
    case 'UPLOAD_FAIL':
      return {
        files: state.files.map(f =>
          f.id === action.payload.id
            ? { ...f, status: 'failed' }
            : f
        )
      };
    default:
      return state;
  }
}

在上传逻辑中,需要为每个文件生成一个唯一id,XHR回调中通过dispatch({ type: 'UPDATE_PROGRESS', payload: { id, progress } })来更新进度。重试功能只需要重新发起上传请求,将对应文件的状态重置为pending并重新执行XHR即可。

另外要注意组件卸载时取消未完成的上传请求,避免内存泄漏。可以在useEffect的清理函数中遍历保存的XHR实例并调用abort()。如果使用axios,可以通过CancelToken实现取消。对于拖拽上传场景,取消操作不太常见,但仍然是良好实践。

最后,针对大文件上传,可以考虑加入断点续传或分片上传,但这部分超出了本文的范围。当前实现的单文件XHR上传足以应对大多数中小型文件场景。

React拖拽上传Drag and Drop API上传进度条修改时间:2026-09-28 01:14:20

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0928/62780.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。