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
相关推荐
默默学前端7 小时前
ES6模板语法与字符串处理详解
前端·ecmascript·es6
lxh01137 小时前
记忆函数 II 题解
前端·javascript
我不吃饼干7 小时前
TypeScript 类型体操练习笔记(三)
前端·typescript
华仔啊7 小时前
除了防抖和节流,还有哪些 JS 性能优化手段?
前端·javascript·vue.js
CHU7290357 小时前
随时随地学新知——线上网课教学小程序前端功能详解
前端·小程序
清粥油条可乐炸鸡7 小时前
motion入门教程
前端·css·react.js
这是个栗子7 小时前
【Vue3项目】电商前台项目(四)
前端·vue.js·pinia·表单校验·面包屑导航
前端Hardy7 小时前
Electrobun 正式登场:仅 12MB,JS 桌面开发迎来轻量化新方案!
前端·javascript·electron
树上有只程序猿7 小时前
新世界的入场券,不再只发给程序员
前端·人工智能
confiself8 小时前
deer-flow前端分析
前端