一、基础必问(必考)
1. 什么是文件分片上传?为什么要分片?
- 大文件上传时,把文件切成一小块一小块上传;
- 原因:
- 突破上传大小限制
- 失败只重传失败分片,节省流量
- 支持断点续传
- 可做进度、并发、暂停
2. 什么是断点续传?核心原理是什么?
- 上传中断后,下次从上次断掉的位置继续传 ,不用从头传。核心原理:
- 前端生成文件唯一标识(文件名 + 大小 + 修改时间 / MD5)
- 后端记录已上传的分片列表
- 续传时先问后端:哪些分片已传完
- 前端只传未上传的分片
3. 断点续传的关键步骤是什么?
- 文件切片
- 生成文件唯一 ID
- 查询后端已上传分片
- 跳过已上传,只传缺失分片
- 全部传完通知后端合并
二、React + AntD Upload 相关(高频)
4. AntD Upload 怎么禁止自动上传?
javascript
beforeUpload={() => false}
5. 为什么要禁用自动上传?
因为要自己接管上传逻辑:
- 分片
- 断点续传
- 心跳
- 暂停 / 继续
6. AntD Upload 怎么和第三方上传库(如 React Uploady)结合?
beforeUpload: () => false禁用默认上传- 在
onChange拿到文件 - 把文件交给 Uploady/resumable.js
- 把上传进度 / 状态同步回 AntD 的
fileList
三、React Uploady / 分片库相关
7. React Uploady 实现分片上传靠什么?
javascript
chunkedUploadEnhancer
- React Uploady 断点续传靠什么?
javascript
validateChunk: true
上传前先检查分片是否已存在。
9. Uploady 暂停、继续、取消的 API?
abort()暂停resume()继续removeFile()删除
四、心跳机制(重点加分题)
10. 什么是 WebSocket / 接口心跳?
- 客户端定时发请求(ping)
- 服务端回(pong)
- 一段时间没回应 → 判定断连
11. 心跳有什么用?
- 检测网络是否断开
- 断连自动暂停上传
- 重连自动恢复上传
- 防止无效上传请求
12. 心跳 + 断点续传怎么配合?
- 心跳断连 → 暂停上传
- 心跳重连 → 恢复上传
- 恢复时从断点继续,不从头传
五、项目实战类(面试官最爱)
13. 你项目里大文件上传怎么做的?
标准满分回答:
- 使用 AntD Upload 做 UI
- React Uploady 做分片上传、断点续传
- 前端生成文件唯一 ID
- 后端记录已上传分片
- 加入心跳机制,断连暂停、重连恢复
- 支持暂停、继续、进度、失败重试
14. 怎么保证文件唯一?
javascript
file.name + file.size + file.lastModified
或 MD5
15. 上传过程中刷新页面怎么办?
- 刷新后重新选择同一个文件
- 自动查询已上传分片
- 从断点继续上传(断点续传)
16. 怎么处理上传失败?
- 分片失败自动重试
- 网络失败靠心跳检测
- 重连后恢复上传
- 失败分片单独重传
17. 后端要提供哪几个接口?
- 心跳接口
- 分片上传接口
- 查询已上传分片接口
- 合并文件接口
六、进阶拔高题(能答出来直接加分)
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