在现代 Web 应用中,上传大文件是一个常见的需求。为了提高上传效率并增强用户体验,切片上传是一种非常有效的解决方案。本文将详细介绍如何在 Vue3 项目中实现大文件切片上传,并提供完整的代码示例,包括前端、后端以及 Web Worker 的实现。
场景与挑战
在上传大文件时,可能会遇到以下问题:
- 网络中断:上传过程中网络断开,导致上传失败。
- 网络波动:网络不稳定可能导致部分切片上传失败。
- 意外中断:用户设备意外关机或刷新页面,上传中断。
为了解决这些问题,我们可以采用以下技术:
- 断点续传:支持从中断处继续上传,避免重复上传已完成的部分。
- 失败重传:网络恢复后自动重新上传失败的切片。
- 切片上传:将文件分割为多个小块逐一上传,降低单次上传的压力。
实现方案
1. 切片上传的基本流程
- 文件切片:将大文件分割为多个小块(chunk),每块大小通常为 500KB。
- 切片上传:逐一上传切片到后端,同时附带
hashId和index信息,用于标识文件及切片顺序。 - 后端合并:后端在接收所有切片后,按照顺序合并为完整文件。
2. 进一步优化方案
- 多线程切片:通过 Web Worker 实现文件切片的多线程处理,避免阻塞主线程,提升性能。
- 断点续传支持:利用
IndexedDB存储切片上传状态,确保上传中断后可以从中断处继续。 - 实时进度更新:通过 WebSocket 实现上传进度的实时通知,为用户提供更好的体验。
Vue3 实现代码示例
以下是基于 Vue3 的大文件切片上传实现示例,包括前端代码、后端代码以及 Web Worker 的实现。
前端代码
<template>
<div>
<input type="file" @change="handleFileChange" />
<button @click="uploadFile">上传文件</button>
<p>上传进度:{{ progress }}%</p>
</div>
</template>
<script>
import SparkMD5 from "spark-md5";
export default {
data() {
return {
file: null,
progress: 0,
hashId: "",
uploadedChunks: new Set(), // 已上传的切片索引
};
},
methods: {
handleFileChange(event) {
this.file = event.target.files[0];
this.calculateHash();
},
calculateHash() {
const chunkSize = 2 * 1024 * 1024; // 每块大小 2MB
const chunks = Math.ceil(this.file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
let currentChunk = 0;
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNextChunk();
} else {
this.hashId = spark.end();
console.log("文件的 hash 值为:", this.hashId);
this.checkUploadedChunks(); // 检查已上传的切片
}
};
const loadNextChunk = () => {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, this.file.size);
fileReader.readAsArrayBuffer(this.file.slice(start, end));
};
loadNextChunk();
},
async checkUploadedChunks() {
const response = await fetch(`http://localhost:3000/uploaded-chunks?hashId=${this.hashId}`);
const { uploadedChunks } = await response.json();
this.uploadedChunks = new Set(uploadedChunks);
},
async uploadFile() {
if (!this.file) return alert("请选择文件!");
if (!this.hashId) return alert("文件 hash 计算中,请稍后再试!");
const chunkSize = 500 * 1024; // 每块大小 500KB
const chunks = Math.ceil(this.file.size / chunkSize);
const fileChunks = [];
// 使用 Web Worker 进行切片
const worker = new Worker(new URL("./worker.js", import.meta.url));
worker.postMessage({ file: this.file, chunkSize });
worker.onmessage = async (e) => {
const { chunks } = e.data;
for (let i = 0; i < chunks.length; i++) {
if (this.uploadedChunks.has(i)) {
this.progress = Math.round(((i + 1) / chunks.length) * 100);
continue; // 跳过已上传的切片
}
const formData = new FormData();
formData.append("chunk", chunks[i]);
formData.append("index", i);
formData.append("hashId", this.hashId);
try {
await fetch("http://localhost:3000/upload", {
method: "POST",
body: formData,
});
this.uploadedChunks.add(i); // 标记切片已上传
this.progress = Math.round(((i + 1) / chunks.length) * 100);
} catch (error) {
console.error(`切片 ${i} 上传失败,将重试`);
i--; // 重试当前切片
}
}
alert("文件上传完成!");
};
},
},
};
</script>
Web Worker 实现
// worker.js
self.onmessage = (e) => {
const { file, chunkSize } = e.data;
const chunks = [];
const totalChunks = Math.ceil(file.size / chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = start + chunkSize;
chunks.push(file.slice(start, end));
}
self.postMessage({ chunks });
};
后端代码(Node.js 示例)
切片上传接口
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.use(express.json());
app.get("/uploaded-chunks", (req, res) => {
const { hashId } = req.query;
const targetDir = path.join(__dirname, "uploads", hashId);
if (!fs.existsSync(targetDir)) {
return res.json({ uploadedChunks: [] });
}
const uploadedChunks = fs.readdirSync(targetDir).map((file) => {
return parseInt(file.split("-")[1]);
});
res.json({ uploadedChunks });
});
app.post("/upload", upload.single("chunk"), (req, res) => {
const { index, hashId } = req.body;
const chunkPath = req.file.path;
const targetDir = path.join(__dirname, "uploads", hashId);
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
fs.renameSync(chunkPath, path.join(targetDir, `chunk-${index}`));
res.send("Chunk uploaded");
});
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
切片合并逻辑
const mergeChunks = (hashId, outputPath) => {
const dirPath = path.join(__dirname, "uploads", hashId);
const chunkFiles = fs.readdirSync(dirPath).sort((a, b) => {
const indexA = parseInt(a.split("-")[1]);
const indexB = parseInt(b.split("-")[1]);
return indexA - indexB;
});
const writeStream = fs.createWriteStream(outputPath);
chunkFiles.forEach((file) => {
const filePath = path.join(dirPath, file);
const data = fs.readFileSync(filePath);
writeStream.write(data);
fs.unlinkSync(filePath); // 删除切片
});
writeStream.end();
fs.rmdirSync(dirPath); // 删除目录
};
总结
通过切片上传技术,我们可以有效解决大文件上传中的网络问题,并显著提升用户体验。结合 Vue3 的响应式特性和现代浏览器的 API(如 Web Worker 和 IndexedDB),可以进一步优化上传性能和稳定性。希望本文的示例代码能为你的项目提供实用的参考!
1247



被折叠的 条评论
为什么被折叠?



