首先,前端部分需要用到Vue3的Composition API,可能需要用ref和reactive来管理状态。然后,文件上传要用到File API,特别是Blob对象的slice方法来分片。每个分片需要有一个唯一的标识,可能是文件名加上分片索引,或者使用文件内容的hash,但为了简化,可能先使用文件名和索引的组合。
接下来,上传进度的显示,需要计算已上传的分片数和总分片数,然后更新进度条。断点续传的话,需要后端支持,记录已上传的分片,所以前端在上传前应该先询问后端哪些分片已经上传过了,避免重复上传。
然后,关于并发控制,可能需要限制同时上传的分片数,避免过多的请求导致浏览器崩溃或服务器压力过大。可以使用Promise.all或者类似的机制来管理并发请求。
后端的部分,虽然用户可能更关注前端,但需要简单提一下后端的实现要点。比如,每个分片需要保存,并在所有分片上传完成后合并成原文件。后端需要提供接口检查分片状态,接收分片数据,以及合并文件的接口。
可能遇到的问题包括文件分片的大小设置,太大或太小都可能影响性能。通常设置为1MB左右。另外,文件唯一标识的生成,可能需要用到文件名、大小、最后修改时间等,不过对于精确识别,可能需要计算文件的hash,比如MD5,但这在前端计算可能会比较耗时,特别是大文件。
断点续传的实现,需要前端在上传前先发送一个请求,检查哪些分片已经上传,然后过滤掉已上传的分片,只上传剩下的。这需要后端有一个机制来记录每个文件的分片上传状态。
另外,合并分片时,后端需要按顺序将分片合并成原文件,确保文件的正确性。分片的顺序需要前端在上传时维护,比如按索引顺序上传,或者在上传时附带分片顺序信息。
前端的代码结构大概包括:选择文件,计算分片,检查分片状态,上传分片,显示进度,合并请求。需要使用axios或其他HTTP库来发送分片数据,可能需要用FormData来包装分片和元数据。
对于Vue3组件,可能需要一个文件输入框,一个上传按钮,进度条显示。状态管理包括当前文件、分片列表、已上传的分片索引、上传进度等。
测试的时候,可以模拟网络中断,或者手动停止上传,然后再次选择同一文件,检查是否跳过已上传的分片,继续上传。
可能还需要处理文件重命名的问题,如果用户上传同名但内容不同的文件,需要确保不会覆盖。可以通过生成唯一标识来解决,比如时间戳或随机字符串附加到文件名。
总结一下,步骤大概是:
选择文件,读取文件信息。
分片,生成每个分片的唯一标识。
检查后端,获取已上传的分片列表。
过滤掉已上传的分片,上传剩下的。
并发上传分片,控制并发数。
所有分片上传完成后,发送合并请求。
显示上传进度和状态。
需要注意的点包括:分片大小、唯一标识生成、并发控制、错误重试、进度计算、后端接口设计等。
在Vue3中实现大文件分片上传和断点续传,可以按照以下步骤进行:
一、核心实现思路
- 文件分片:将大文件切割为固定大小的块(如1MB)
- 唯一标识:使用文件内容hash或文件元数据生成唯一标识
- 断点续传:服务端记录已上传分片,前端上传前先查询
- 分片上传:并发上传文件分片(需控制并发数)
- 分片合并:所有分片上传完成后通知服务端合并
二、前端实现代码(Vue3 + TypeScript)
<template>
<div>
<input type="file" @change="handleFileChange" />
<button @click="startUpload">开始上传</button>
<progress :value="progress" max="100"></progress>
<div>{{ progress }}%</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import axios from 'axios';
interface Chunk {
index: number;
file: Blob;
hash: string;
}
const CHUNK_SIZE = 1024 * 1024; // 1MB
const CONCURRENCY = 3; // 并发数
const file = ref<File | null>(null);
const progress = ref(0);
const uploadedChunks = ref<Set<string>>(new Set());
// 生成文件唯一标识(简化版)
const generateFileHash = async (file: File) => {
return `${file.name}-${file.size}-${file.lastModified}`;
};
// 文件分片
const createChunks = (file: File, fileHash: string): Chunk[] => {
const chunks: Chunk[] = [];
let index = 0;
for (let cur = 0; cur < file.size; cur += CHUNK_SIZE) {
const chunk = file.slice(cur, cur + CHUNK_SIZE);
chunks.push({
index,
file: chunk,
hash: `${fileHash}-${index}`
});
index++;
}
return chunks;
};
// 检查已上传分片
const checkUploadedChunks = async (fileHash: string) => {
try {
const { data } = await axios.get('/api/check', {
params: { fileHash }
});
uploadedChunks.value = new Set(data.uploadedChunks);
} catch (error) {
console.error('检查已上传分片失败:', error);
}
};
// 上传分片
const uploadChunk = async (chunk: Chunk, fileHash: string) => {
const formData = new FormData();
formData.append('file', chunk.file);
formData.append('hash', chunk.hash);
formData.append('fileHash', fileHash);
formData.append('index', chunk.index.toString());
await axios.post('/api/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
uploadedChunks.value.add(chunk.hash);
};
// 开始上传
const startUpload = async () => {
if (!file.value) return;
const fileHash = await generateFileHash(file.value);
const chunks = createChunks(file.value, fileHash);
await checkUploadedChunks(fileHash);
const pendingChunks = chunks.filter(
chunk => !uploadedChunks.value.has(chunk.hash)
);
// 控制并发上传
const pool = [];
for (let i = 0; i < pendingChunks.length; i++) {
const chunk = pendingChunks[i];
const task = uploadChunk(chunk, fileHash).then(() => {
pool.splice(pool.indexOf(task), 1);
updateProgress();
});
pool.push(task);
if (pool.length >= CONCURRENCY) {
await Promise.race(pool);
}
}
await Promise.all(pool);
// 所有分片上传完成,请求合并
await axios.post('/api/merge', {
fileHash,
totalChunks: chunks.length,
fileName: file.value.name
});
};
// 更新进度
const updateProgress = () => {
const total = file.value?.size || 0;
const uploaded = Array.from(uploadedChunks.value).reduce(
(sum, hash) => sum + CHUNK_SIZE,
0
);
progress.value = Math.min(100, (uploaded / total) * 100);
};
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement;
if (target.files) {
file.value = target.files[0];
}
};
</script>
三、后端实现要点(Node.js示例)
// 检查已上传分片
router.get('/check', (req, res) => {
const { fileHash } = req.query;
const uploaded = fs.existsSync(`./uploads/${fileHash}`)
? JSON.parse(fs.readFileSync(`./uploads/${fileHash}/info.json`))
: [];
res.json({ uploadedChunks: uploaded });
});
// 分片上传
router.post('/upload', (req, res) => {
const { fileHash, hash, index } = req.body;0
const chunk = req.files.file;
const chunkDir = `./uploads/${fileHash}`;
if (!fs.existsSync(chunkDir)) {
fs.mkdirSync(chunkDir, { recursive: true });
}
fs.renameSync(chunk.path, `${chunkDir}/${hash}`);
const infoFile = `${chunkDir}/info.json`;
const uploaded = fs.existsSync(infoFile)
? JSON.parse(fs.readFileSync(infoFile))
: [];
if (!uploaded.includes(hash)) {
uploaded.push(hash);
fs.writeFileSync(infoFile, JSON.stringify(uploaded));
}
res.json({ success: true });
});
// 合并文件
router.post('/merge', async (req, res) => {
const { fileHash, fileName, totalChunks } = req.body;
const chunkDir = `./uploads/${fileHash}`;
const files = fs.readdirSync(chunkDir)
.filter(f => f !== 'info.json')
.sort((a, b) => a.split('-')[1] - b.split('-')[1]);
const writeStream = fs.createWriteStream(`./uploads/${fileName}`);
for (const file of files) {
const chunkPath = `${chunkDir}/${file}`;
await new Promise(resolve => {
const readStream = fs.createReadStream(chunkPath);
readStream.pipe(writeStream, { end: false });
readStream.on('end', resolve);
});
}
writeStream.end();
// 清理临时文件
fs.rmSync(chunkDir, { recursive: true });
res.json({ success: true });
});
四、优化建议
- 文件Hash生成:使用SparkMD5计算文件内容hash,更准确
- 错误重试:为每个分片添加重试机制
- 进度计算:使用实际传输字节数代替分片数量计算
- 暂停/恢复:添加暂停按钮和恢复上传逻辑
- 分片大小自适应:根据网络情况动态调整分片大小
- 内存优化:使用流式处理替代整个文件读取
五、注意事项
- 服务端需要处理跨域问题
- 文件保存路径需要做好安全防护
- 生产环境需要添加用户认证和权限控制
- 大文件合并时建议使用流式写入
- 分布式部署时需要共享存储或使用对象存储服务
完整实现需要结合具体业务需求进行调整,建议使用现成的文件上传库(如tus-js-client)作为基础进行二次开发。
Comments