应用背景
在现代Web应用中,文件上传是一个常见的功能需求,尤其是大文件上传(如视频、高清图片、大型文档等)。传统的文件上传方式在处理大文件时存在以下问题:
- 网络不稳定:大文件上传时间长,网络波动可能导致上传失败,用户需要重新上传。
- 服务器压力:一次性上传大文件会占用大量服务器资源,可能导致服务器性能下降甚至崩溃。
- 用户体验差:上传失败后需要重新上传,用户等待时间长,体验不佳。
为了解决这些问题,前端大文件上传通常采用分片上传 和断点续传的技术:
- 分片上传:将大文件分割成多个小文件(分片),逐个上传,减少单次上传的压力。
- 断点续传:如果上传中断,可以从断点处继续上传,避免重新上传整个文件。
为什么需要大文件上传?
- 提高上传成功率:分片上传可以减少单次上传的文件大小,降低因网络波动导致的上传失败概率。
- 减轻服务器压力:分片上传可以分散服务器负载,避免一次性处理大文件。
- 提升用户体验:断点续传功能可以让用户在上传中断后继续上传,减少等待时间。
- 支持大文件上传:传统上传方式对文件大小有限制,分片上传可以突破这一限制。
代码实现
以下是基于前端技术(如 Vue.js 和 Axios)实现大文件上传的代码示例。
1. 文件分片
将大文件分割成多个小文件(分片)。
typescript
const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片大小为 5MB
function createFileChunks(file: File, chunkSize: number = CHUNK_SIZE) {
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
chunks.push(chunk);
start = end;
}
return chunks;
}
2. 上传分片
使用 Axios 上传每个分片。
typescript
import axios from "axios";
async function uploadChunk(chunk: Blob, index: number, fileHash: string) {
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("index", index.toString());
formData.append("fileHash", fileHash);
try {
const response = await axios.post("/api/upload-chunk", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
} catch (error) {
console.error("上传分片失败:", error);
throw error;
}
}
3. 计算文件哈希
为文件生成唯一标识(哈希值),用于断点续传和文件校验。
typescript
import { md5 } from "js-md5";
async function calculateFileHash(file: File): Promise<string> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const hash = md5(e.target?.result as string);
resolve(hash);
};
reader.readAsBinaryString(file);
});
}
4. 合并分片
所有分片上传完成后,通知服务器合并分片。
typescript
async function mergeChunks(fileName: string, fileHash: string) {
try {
const response = await axios.post("/api/merge-chunks", {
fileName,
fileHash,
});
return response.data;
} catch (error) {
console.error("合并分片失败:", error);
throw error;
}
}
5. 断点续传
在上传前检查服务器是否已存在部分分片,避免重复上传。
typescript
async function checkUploadedChunks(fileHash: string): Promise<number[]> {
try {
const response = await axios.get("/api/uploaded-chunks", {
params: { fileHash },
});
return response.data.uploadedChunks || [];
} catch (error) {
console.error("检查已上传分片失败:", error);
return [];
}
}
6. 完整上传流程
将上述功能整合为一个完整的上传流程。
typescript
async function uploadFile(file: File) {
const fileHash = await calculateFileHash(file);
const chunks = createFileChunks(file);
const uploadedChunks = await checkUploadedChunks(fileHash);
for (let i = 0; i < chunks.length; i++) {
if (uploadedChunks.includes(i)) continue; // 跳过已上传的分片
await uploadChunk(chunks[i], i, fileHash);
}
await mergeChunks(file.name, fileHash);
console.log("文件上传完成");
}
7. 前端调用
在 Vue.js 组件中调用上传功能。
vue
<template>
<div>
<input type="file" @change="handleFileChange" />
<button @click="handleUpload">上传</button>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { uploadFile } from "./uploadUtils";
const selectedFile = ref<File | null>(null);
function handleFileChange(event: Event) {
const target = event.target as HTMLInputElement;
if (target.files && target.files[0]) {
selectedFile.value = target.files[0];
}
}
async function handleUpload() {
if (selectedFile.value) {
await uploadFile(selectedFile.value);
} else {
alert("请先选择文件");
}
}
</script>
服务器端实现(Node.js 示例)
1. 接收分片
javascript
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const app = express();
const upload = multer({ dest: "uploads/" });
app.post("/api/upload-chunk", upload.single("chunk"), (req, res) => {
const { index, fileHash } = req.body;
const chunkPath = path.join("uploads", `${fileHash}-${index}`);
fs.renameSync(req.file.path, chunkPath);
res.send({ success: true });
});
2. 合并分片
javascript
app.post("/api/merge-chunks", (req, res) => {
const { fileName, fileHash } = req.body;
const chunkDir = path.join("uploads");
const chunks = fs.readdirSync(chunkDir).filter((name) => name.startsWith(fileHash));
chunks.sort((a, b) => parseInt(a.split("-")[1]) - parseInt(b.split("-")[1]));
const filePath = path.join("uploads", fileName);
const writeStream = fs.createWriteStream(filePath);
chunks.forEach((chunk) => {
const chunkPath = path.join(chunkDir, chunk);
const chunkData = fs.readFileSync(chunkPath);
writeStream.write(chunkData);
fs.unlinkSync(chunkPath); // 删除分片
});
writeStream.end();
res.send({ success: true });
});
3. 检查已上传分片
javascript
app.get("/api/uploaded-chunks", (req, res) => {
const { fileHash } = req.query;
const chunkDir = path.join("uploads");
const uploadedChunks = fs.readdirSync(chunkDir)
.filter((name) => name.startsWith(fileHash))
.map((name) => parseInt(name.split("-")[1]));
res.send({ uploadedChunks });
});
总结
通过分片上传和断点续传技术,可以有效解决大文件上传中的网络不稳定、服务器压力大和用户体验差等问题。前端将文件分片并逐个上传,服务器接收分片并最终合并,同时支持断点续传功能。