You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中使用RTK Query实现带加载进度条的文件上传功能咨询

基于RTK Query实现文件上传组件的实现方案

1. 定义支持进度监听的RTK Query上传端点

RTK Query的fetchBaseQuery原生支持上传进度回调配置,不需要额外引入第三方请求库,核心配置如下:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const uploadApi = createApi({
  reducerPath: 'uploadApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    uploadFile: builder.mutation({
      query: (formData) => ({
        url: '/file/upload',
        method: 'POST',
        body: formData,
        // 开启上传进度监听
        fetchOptions: {
          onUploadProgress: (progressEvent) => {
            const percentCompleted = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
            // 执行透传的进度更新回调
            formData.get('onProgress')?.(percentCompleted);
          }
        }
      }),
      // 统一预处理返回错误
      transformErrorResponse: (response) => {
        const errMap = {
          400: '文件格式不合法,请重新上传',
          413: '文件大小超出服务端限制',
          500: '服务端异常,请稍后重试'
        }
        return {
          status: response.status,
          message: errMap[response.status] || response.data?.message || '上传失败'
        }
      }
    })
  })
});

export const { useUploadFileMutation } = uploadApi;

注意:FormData中传入的进度回调函数不会被序列化提交到服务端,不会干扰正常请求参数。

2. 拖拽上传功能实现

直接用原生拖拽事件实现,不需要引入额外依赖,核心逻辑如下:

import { useState } from 'react';
import { useUploadFileMutation } from './uploadApi';

const FileUpload = () => {
  const [isDragOver, setIsDragOver] = useState(false);
  const [uploadProgress, setUploadProgress] = useState(0);
  const [uploadError, setUploadError] = useState(null);
  const [uploadFile, { isLoading }] = useUploadFileMutation();

  // 阻止默认拖拽行为,避免浏览器直接打开文件
  const handleDragOver = (e) => {
    e.preventDefault();
    setIsDragOver(true);
  };
  const handleDragLeave = (e) => {
    e.preventDefault();
    setIsDragOver(false);
  };
  const handleDrop = (e) => {
    e.preventDefault();
    setIsDragOver(false);
    const file = e.dataTransfer.files[0];
    validateAndUpload(file);
  };

  // 统一文件校验和上传逻辑,普通文件选择也复用该方法
  const validateAndUpload = (file) => {
    if (!file) return;
    // 前置格式校验
    const allowedTypes = ['.csv', '.xlsx', '.xls'];
    const fileExt = file.name.slice(file.name.lastIndexOf('.')).toLowerCase();
    if (!allowedTypes.includes(fileExt)) {
      setUploadError('仅支持上传CSV、XLS、XLSX格式的文件');
      return;
    }
    // 前置大小校验,可根据需求调整上限
    if (file.size > 10 * 1024 * 1024) {
      setUploadError('文件大小不能超过10M');
      return;
    }
    setUploadError(null);
    setUploadProgress(0);

    const formData = new FormData();
    formData.append('file', file);
    formData.append('onProgress', setUploadProgress);

    // 触发上传
    uploadFile(formData)
      .unwrap()
      .then(() => {
        setUploadProgress(100);
        // 这里补充上传成功后的业务逻辑
      })
      .catch((err) => {
        setUploadError(err.message);
        setUploadProgress(0);
      });
  };

  // 取消上传功能
  const handleCancel = () => {
    uploadFile.abort();
    setUploadProgress(0);
    setUploadError(null);
  };

拖放区域UI代码参考:

return (
    <div className="w-full max-w-2xl mx-auto p-4">
      {/* 拖拽区域 */}
      <div
        onDragOver={handleDragOver}
        onDragLeave={handleDragLeave}
        onDrop={handleDrop}
        className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
          isDragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
        }`}
      >
        <p>拖拽文件到此处上传,或点击下方按钮选择文件</p>
        <input
          type="file"
          accept=".csv,.xlsx,.xls"
          onChange={(e) => validateAndUpload(e.target.files[0])}
          className="hidden"
          id="file-upload"
        />
        <label htmlFor="file-upload" className="text-blue-500 underline cursor-pointer mt-2 inline-block">
          选择文件
        </label>
      </div>

      {/* 进度条 */}
      {isLoading && (
        <div className="mt-4">
          <div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
            <div 
              className="h-full bg-blue-500 transition-all duration-300 ease-in-out"
              style={{ width: `${uploadProgress}%` }}
            />
          </div>
          <div className="flex justify-between items-center mt-1">
            <span className="text-xs text-gray-600">{uploadProgress}%</span>
            <button onClick={handleCancel} className="text-xs text-red-500">取消上传</button>
          </div>
        </div>
      )}

      {/* 错误提示 */}
      {uploadError && (
        <p className="mt-2 text-sm text-red-500">{uploadError}</p>
      )}
    </div>
  )
}
export default FileUpload;

3. 补充优化点

  • 多文件上传:修改校验逻辑遍历FileList数组,给每个文件生成独立的进度状态即可
  • 大文件分片上传:可以结合file.slice方法拆分文件,RTK Query批量发送分片请求,服务端拼接即可
  • 上传成功状态持久化:可以把上传成功的文件信息存在RTK的cache里,避免重复上传

内容的提问来源于stack exchange,提问作者slycooper14

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 08:15:02