AntD Upload + React Uploady + 分片上传 + 断点续传 + 心跳机制(面试及代码)

一、基础必问(必考)

1. 什么是文件分片上传?为什么要分片?

  • 大文件上传时,把文件切成一小块一小块上传;
  • 原因:
    1. 突破上传大小限制
    2. 失败只重传失败分片,节省流量
    3. 支持断点续传
    4. 可做进度、并发、暂停

2. 什么是断点续传?核心原理是什么?

  • 上传中断后,下次从上次断掉的位置继续传 ,不用从头传。核心原理:
  1. 前端生成文件唯一标识(文件名 + 大小 + 修改时间 / MD5)
  2. 后端记录已上传的分片列表
  3. 续传时先问后端:哪些分片已传完
  4. 前端只传未上传的分片

3. 断点续传的关键步骤是什么?

  1. 文件切片
  2. 生成文件唯一 ID
  3. 查询后端已上传分片
  4. 跳过已上传,只传缺失分片
  5. 全部传完通知后端合并

二、React + AntD Upload 相关(高频)

4. AntD Upload 怎么禁止自动上传

javascript 复制代码
beforeUpload={() => false}

5. 为什么要禁用自动上传?

因为要自己接管上传逻辑:

  • 分片
  • 断点续传
  • 心跳
  • 暂停 / 继续

6. AntD Upload 怎么和第三方上传库(如 React Uploady)结合?

  1. beforeUpload: () => false 禁用默认上传
  2. onChange 拿到文件
  3. 把文件交给 Uploady/resumable.js
  4. 把上传进度 / 状态同步回 AntD 的 fileList

三、React Uploady / 分片库相关

7. React Uploady 实现分片上传靠什么?

javascript 复制代码
chunkedUploadEnhancer
  1. React Uploady 断点续传靠什么?
javascript 复制代码
validateChunk: true

上传前先检查分片是否已存在。

9. Uploady 暂停、继续、取消的 API?

  • abort() 暂停
  • resume() 继续
  • removeFile() 删除

四、心跳机制(重点加分题)

10. 什么是 WebSocket / 接口心跳?

  • 客户端定时发请求(ping)
  • 服务端回(pong)
  • 一段时间没回应 → 判定断连

11. 心跳有什么用?

  1. 检测网络是否断开
  2. 断连自动暂停上传
  3. 重连自动恢复上传
  4. 防止无效上传请求

12. 心跳 + 断点续传怎么配合?

  • 心跳断连 → 暂停上传
  • 心跳重连 → 恢复上传
  • 恢复时从断点继续,不从头传

五、项目实战类(面试官最爱)

13. 你项目里大文件上传怎么做的?

标准满分回答:

  1. 使用 AntD Upload 做 UI
  2. React Uploady 做分片上传、断点续传
  3. 前端生成文件唯一 ID
  4. 后端记录已上传分片
  5. 加入心跳机制,断连暂停、重连恢复
  6. 支持暂停、继续、进度、失败重试

14. 怎么保证文件唯一?

javascript 复制代码
file.name + file.size + file.lastModified
或 MD5

15. 上传过程中刷新页面怎么办?

  • 刷新后重新选择同一个文件
  • 自动查询已上传分片
  • 从断点继续上传(断点续传)

16. 怎么处理上传失败?

  • 分片失败自动重试
  • 网络失败靠心跳检测
  • 重连后恢复上传
  • 失败分片单独重传

17. 后端要提供哪几个接口?

  1. 心跳接口
  2. 分片上传接口
  3. 查询已上传分片接口
  4. 合并文件接口

六、进阶拔高题(能答出来直接加分)

18. 分片大小一般设多少?

5MB ~ 10MB

  • 太小:请求太多
  • 太大:失败重传成本高

19. 怎么实现秒传

  • 前端计算文件 MD5
  • 后端查是否已存在该文件
  • 存在直接返回成功,不用传

20. 怎么保证分片不重复、不乱序

  • 按索引上传 0、1、2、3...
  • 后端按索引保存
  • 合并时按顺序拼接

心跳检测工具

TypeScript 复制代码
import { useState, useRef, useEffect } from 'react';
import axios from 'axios';

// 心跳配置类型
interface HeartbeatConfig {
  url: string; // 心跳接口地址
  interval: number; // 心跳间隔(ms)
  timeout: number; // 心跳超时(ms)
  onReconnect?: () => void; // 重连成功回调
  onDisconnect?: () => void; // 断连回调
}

// 心跳状态类型
export type HeartbeatStatus = 'online' | 'offline' | 'reconnecting';

/**
 * 心跳检测 Hook
 * @param config 心跳配置
 * @returns 心跳状态、手动重连方法
 */
export const useHeartbeat = (config: HeartbeatConfig) => {
  const [status, setStatus] = useState<HeartbeatStatus>('online');
  const heartbeatTimer = useRef<NodeJS.Timeout | null>(null);
  const reconnectTimer = useRef<NodeJS.Timeout | null>(null);
  const axiosInstance = useRef(
    axios.create({
      timeout: config.timeout,
      headers: {
        'Content-Type': 'application/json',
        // 可添加认证头(如token)
        'Authorization': 'Bearer ' + localStorage.getItem('token') || '',
      },
    })
  );

  // 发送心跳包
  const sendHeartbeat = async () => {
    try {
      // 向服务端发送心跳请求(GET/POST均可,服务端只需返回200即可)
      await axiosInstance.current.post(config.url, { type: 'heartbeat' });
      
      // 心跳成功:标记在线,清空重连定时器
      if (status !== 'online') {
        setStatus('online');
        config.onReconnect?.(); // 触发重连成功回调
      }
    } catch (err) {
      // 心跳失败:标记离线,启动重连
      setStatus('offline');
      config.onDisconnect?.(); // 触发断连回调
      startReconnect();
    }
  };

  // 启动重连机制
  const startReconnect = () => {
    if (reconnectTimer.current) clearInterval(reconnectTimer.current);
    
    setStatus('reconnecting');
    // 每3秒重试一次心跳
    reconnectTimer.current = setInterval(() => {
      sendHeartbeat();
    }, 3000);
  };

  // 手动触发重连
  const reconnect = () => {
    sendHeartbeat();
  };

  // 初始化心跳定时器
  useEffect(() => {
    // 启动心跳(首次立即发送,之后按间隔发送)
    sendHeartbeat();
    heartbeatTimer.current = setInterval(sendHeartbeat, config.interval);

    // 组件卸载时清理定时器
    return () => {
      if (heartbeatTimer.current) clearInterval(heartbeatTimer.current);
      if (reconnectTimer.current) clearInterval(reconnectTimer.current);
    };
  }, [config.interval, config.url]);

  return {
    status, // 心跳状态:online/offline/reconnecting
    reconnect, // 手动重连方法
  };
};

整合 AntD Upload + React Uploady + 心跳

TypeScript 复制代码
import React, { useState, useCallback } from 'react';
import { Upload, Button, message, Progress, Typography, Space } from 'antd';
import type { UploadFile, UploadProps } from 'antd';
import { Uploady, useUploady } from '@rpldy/uploady';
import { chunkedUploadEnhancer } from '@rpldy/chunked-upload';
import { retryEnhancer } from '@rpldy/retry';
import { useHeartbeat, HeartbeatStatus } from './HeartbeatManager';

const { Text } = Typography;

// 分片上传 + 心跳 + AntD Upload 整合组件
const ChunkUploadCore: React.FC = () => {
  // AntD 文件列表状态
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  // 上传暂停标记(心跳断连时自动暂停)
  const [isPausedByHeartbeat, setIsPausedByHeartbeat] = useState(false);

  // 1. 初始化 React Uploady 实例
  const {
    upload,
    abort,
    resume,
    getFileState,
    addFile,
    removeFile,
    isUploading,
  } = useUploady({
    // 增强器:分片上传 + 失败重试
    enhancer: chunkedUploadEnhancer(retryEnhancer),
    destination: { 
      url: '/api/upload', // 分片上传接口
      headers: {
        'Authorization': 'Bearer ' + localStorage.getItem('token') || '',
      }
    },
    // 分片配置(断点续传核心)
    chunked: {
      chunkSize: 5 * 1024 * 1024, // 5MB/分片
      retries: 3, // 分片上传失败重试次数
      concurrent: 3, // 并发上传数
      validateChunk: true, // 上传前检测分片是否已存在(断点续传)
      generateChunkId: (file, chunkIndex) => {
        // 生成唯一分片ID(文件ID + 分片索引)
        return `${file.id}-${chunkIndex}`;
      },
    },
    // 全局重试配置
    retry: {
      maxRetries: 3,
      delayFactor: 2, // 指数退避:2s,4s,8s
    },
    // 上传进度监听:同步到 AntD 文件列表
    onFileProgress: (file) => {
      setFileList(prev => 
        prev.map(item => 
          item.uid === file.id 
            ? { ...item, percent: file.completed * 100, status: 'uploading' } 
            : item
        )
      );
    },
    // 上传成功
    onFileSuccess: (file, response) => {
      setFileList(prev => 
        prev.map(item => 
          item.uid === file.id 
            ? { ...item, status: 'done', response } 
            : item
        )
      );
      message.success(`文件 ${file.name} 上传成功`);
    },
    // 上传失败
    onFileError: (file, error) => {
      setFileList(prev => 
        prev.map(item => 
          item.uid === file.id 
            ? { ...item, status: 'error', error: { message: error.message } } 
            : item
        )
      );
      message.error(`文件 ${file.name} 上传失败:${error.message}`);
    },
  });

  // 2. 初始化心跳检测
  const { status: heartbeatStatus, reconnect } = useHeartbeat({
    url: '/api/heartbeat', // 服务端心跳接口
    interval: 30000, // 30秒发送一次心跳
    timeout: 5000, // 心跳超时5秒
    // 心跳重连成功:恢复上传
    onReconnect: useCallback(() => {
      message.success('网络重连成功,恢复上传');
      setIsPausedByHeartbeat(false);
      // 恢复所有未完成的上传
      fileList.forEach(file => {
        if (file.status === 'paused' && isPausedByHeartbeat) {
          resume(file.uid);
          setFileList(prev => 
            prev.map(item => 
              item.uid === file.uid ? { ...item, status: 'uploading' } : item
            )
          );
        }
      });
    }, [fileList, isPausedByHeartbeat, resume]),
    // 心跳断连:暂停上传
    onDisconnect: useCallback(() => {
      message.warning('网络断开,暂停上传');
      setIsPausedByHeartbeat(true);
      // 暂停所有正在上传的文件
      if (isUploading) {
        fileList.forEach(file => {
          if (file.status === 'uploading') {
            abort(file.uid);
            setFileList(prev => 
              prev.map(item => 
                item.uid === file.uid ? { ...item, status: 'paused' } : item
              )
            );
          }
        });
      }
    }, [fileList, isUploading, abort]),
  });

  // 3. AntD Upload 配置
  const uploadProps: UploadProps = {
    fileList,
    beforeUpload: () => false, // 禁用 AntD 原生上传
    // 文件选择事件:交给 React Uploady 处理
    onChange: ({ fileList: newFileList, file }) => {
      setFileList(newFileList);
      // 新增文件时初始化上传(仅在线状态)
      if (file.status === 'ready' && file.originFileObj && heartbeatStatus === 'online') {
        addFile(file.originFileObj, { id: file.uid });
        upload(); // 开始上传
      } else if (file.status === 'ready' && heartbeatStatus !== 'online') {
        message.warning('当前网络离线,无法开始上传,请等待重连');
      }
    },
    // 自定义文件列表渲染(含心跳状态、暂停/继续/取消)
    itemRender: (originNode, file, currFileList) => {
      const fileState = getFileState(file.uid);
      return (
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 8, padding: 8, border: '1px solid #f0f0f0', borderRadius: 4 }}>
          {/* 文件名称 */}
          <span style={{ flex: 1, maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {file.name}
          </span>
          
          {/* 上传进度 */}
          {file.status === 'uploading' && (
            <Progress
              percent={file.percent || 0}
              size="small"
              style={{ width: 120, margin: '0 8px' }}
            />
          )}
          
          {/* 操作按钮 */}
          <Space size="small">
            {/* 暂停/继续按钮 */}
            {file.status === 'uploading' && (
              <Button
                size="small"
                disabled={heartbeatStatus !== 'online'}
                onClick={() => {
                  abort(file.uid);
                  setFileList(prev => 
                    prev.map(item => 
                      item.uid === file.uid ? { ...item, status: 'paused' } : item
                    )
                  );
                }}
              >
                暂停
              </Button>
            )}
            {file.status === 'paused' && (
              <Button
                size="small"
                disabled={heartbeatStatus !== 'online'}
                onClick={() => {
                  resume(file.uid);
                  setFileList(prev => 
                    prev.map(item => 
                      item.uid === file.uid ? { ...item, status: 'uploading' } : item
                    )
                  );
                }}
              >
                {isPausedByHeartbeat ? '等待重连' : '继续'}
              </Button>
            )}
            
            {/* 删除/取消按钮 */}
            <Button
              size="small"
              danger
              onClick={() => {
                removeFile(file.uid);
                setFileList(currFileList.filter(item => item.uid !== file.uid));
              }}
            >
              {file.status === 'done' ? '删除' : '取消'}
            </Button>
          </Space>
        </div>
      );
    },
  };

  // 心跳状态样式
  const getHeartbeatStatusStyle = () => {
    switch (heartbeatStatus) {
      case 'online':
        return { color: '#52c41a', fontWeight: 'bold' };
      case 'offline':
        return { color: '#ff4d4f', fontWeight: 'bold' };
      case 'reconnecting':
        return { color: '#faad14', fontWeight: 'bold' };
      default:
        return {};
    }
  };

  return (
    <div style={{ padding: 20, maxWidth: 800, margin: '0 auto' }}>
      {/* 心跳状态提示 */}
      <div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <Text style={getHeartbeatStatusStyle()}>
          连接状态:{heartbeatStatus === 'online' ? '在线' : heartbeatStatus === 'offline' ? '离线' : '重连中'}
        </Text>
        <Button
          size="small"
          onClick={reconnect}
          disabled={heartbeatStatus === 'online'}
        >
          手动重连
        </Button>
      </div>

      {/* AntD Upload 组件 */}
      <Upload {...uploadProps}>
        <Button 
          type="primary" 
          disabled={heartbeatStatus !== 'online'}
        >
          选择文件上传(仅在线可上传)
        </Button>
      </Upload>
    </div>
  );
};

// 根组件:包裹 Uploady 提供者
export const AntdUploadWithHeartbeat: React.FC = () => {
  return (
    <Uploady
      enhancer={chunkedUploadEnhancer(retryEnhancer)}
      destination={{ url: '/api/upload' }}
      chunked={{ chunkSize: 5 * 1024 * 1024 }}
      retry={{ maxRetries: 3 }}
    >
      <ChunkUploadCore />
    </Uploady>
  );
};

export default AntdUploadWithHeartbeat;

记得安装依赖

bash 复制代码
npm install @rpldy/uploady @rpldy/chunked-upload @rpldy/retry antd axios --save
相关推荐
阿珊和她的猫1 小时前
浏览器跨页签数据共享方案
前端·javascript·vue.js·chrome
Su米苏2 小时前
在 Vue3 + Vite 项目里,动态路由一般有 3 种常见场景
前端
xkxnq2 小时前
第六阶段:Vue生态高级整合与优化(第82天)(Pinia高级用法)持久化方案(pinia-plugin-persistedstate)+ 安全存储策略
前端·vue.js·安全
前端 贾公子2 小时前
React 和 Vue 都离不开的表单验证库 async-validator 之策略模式的应用 (中)
前端
郑州光合科技余经理2 小时前
从零到一:构建UberEats式海外版外卖系统
java·开发语言·前端·javascript·架构·uni-app·php
2301_796512522 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:点击组件(跳转快应用)
javascript·react native·react.js·ecmascript·harmonyos
强子感冒了2 小时前
JavaWeb学习笔记:动静态Web、URL、HTTP
前端·笔记·学习
前端 贾公子2 小时前
React 和 Vue 都离不开的表单验证库 async-validator 之策略模式的应用 (上)
vue.js·react.js·策略模式