1. 核心思路
-
分片上传:将大文件分割成多个小文件(chunk),逐个上传。
-
记录上传进度:通过本地存储(如
localStorage
)或服务端记录已上传的分片。 -
断点续传:上传中断后,重新上传时只上传未完成的分片。
-
合并文件:所有分片上传完成后,通知服务端合并文件。
2. 实现步骤
1) 前端分片上传
-
使用
File
对象的slice
方法将文件分割成多个分片。 -
通过
FormData
将分片上传到服务端。
2) 记录上传进度
-
每个分片上传成功后,记录已上传的分片信息(如分片索引、文件唯一标识等)。
-
可以使用
localStorage
或服务端存储记录上传进度。
3) 断点续传
-
重新上传时,先检查已上传的分片,跳过已上传的部分。
-
只上传未完成的分片。
4) 合并文件
-
所有分片上传完成后,通知服务端合并文件。
3. 代码实现
前端代码(原生JS)
class FileUploader { constructor(file, chunkSize = 5 * 1024 * 1024) { // 默认分片大小为5MB this.file = file; this.chunkSize = chunkSize; this.totalChunks = Math.ceil(file.size / chunkSize); // 总分片数 this.chunkIndex = 0; // 当前分片索引 this.fileId = null; // 文件唯一标识 this.uploadedChunks = new Set(); // 已上传的分片索引 } // 生成文件唯一标识(可以用文件名+文件大小+最后修改时间) generateFileId() { return `${this.file.name}-${this.file.size}-${this.file.lastModified}`; } // 获取未上传的分片 getUnuploadedChunks() { const unuploadedChunks = []; for (let i = 0; i < this.totalChunks; i++) { if (!this.uploadedChunks.has(i)) { unuploadedChunks.push(i); } } return unuploadedChunks; } // 上传分片 async uploadChunk(chunkIndex) { const start = chunkIndex * this.chunkSize; const end = Math.min(start + this.chunkSize, this.file.size); const chunk = this.file.slice(start, end); const formData = new FormData(); formData.append("file", chunk); formData.append("chunkIndex", chunkIndex); formData.append("totalChunks", this.totalChunks); formData.append("fileId", this.fileId); try { await fetch("/upload", { method: "POST", body: formData, }); this.uploadedChunks.add(chunkIndex); // 记录已上传的分片 this.saveProgress(); // 保存上传进度 } catch (error) { console.error("上传失败:", error); throw error; } } // 保存上传进度到 localStorage saveProgress() { const progress = { fileId: this.fileId, uploadedChunks: Array.from(this.uploadedChunks), }; localStorage.setItem(this.fileId, JSON.stringify(progress)); } // 从 localStorage 加载上传进度 loadProgress() { const progress = JSON.parse(localStorage.getItem(this.fileId)); if (progress) { this.uploadedChunks = new Set(progress.uploadedChunks); } } // 开始上传 async startUpload() { this.fileId = this.generateFileId(); this.loadProgress(); // 加载上传进度 const unuploadedChunks = this.getUnuploadedChunks(); for (const chunkIndex of unuploadedChunks) { await this.uploadChunk(chunkIndex); console.log(`分片 ${chunkIndex} 上传完成`); } console.log("所有分片上传完成,通知服务端合并文件"); await this.mergeFile(); } // 通知服务端合并文件 async mergeFile() { await fetch("/merge", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ fileId: this.fileId, fileName: this.file.name, totalChunks: this.totalChunks, }), }); console.log("文件合并完成"); localStorage.removeItem(this.fileId); // 清除上传进度 } } // 使用示例 const fileInput = document.querySelector('input[type="file"]'); fileInput.addEventListener("change", async (e) => { const file = e.target.files[0]; if (file) { const uploader = new FileUploader(file); await uploader.startUpload(); } });
前端代码(VUE)
<template> <div> <input type="file" @change="handleFileChange" /> <button @click="startUpload" :disabled="!file || isUploading"> {{ isUploading ? '上传中...' : '开始上传' }} </button> <div v-if="progress > 0"> 上传进度: {{ progress }}% </div> </div> </template> <script> import { ref } from 'vue'; export default { name: 'FileUploader', setup() { const file = ref(null); // 选择的文件 const isUploading = ref(false); // 是否正在上传 const progress = ref(0); // 上传进度 const chunkSize = 5 * 1024 * 1024; // 分片大小(5MB) const fileId = ref(''); // 文件唯一标识 const uploadedChunks = ref(new Set()); // 已上传的分片索引 // 生成文件唯一标识 const generateFileId = (file) => { return `${file.name}-${file.size}-${file.lastModified}`; }; // 获取未上传的分片 const getUnuploadedChunks = (totalChunks) => { const unuploadedChunks = []; for (let i = 0; i < totalChunks; i++) { if (!uploadedChunks.value.has(i)) { unuploadedChunks.push(i); } } return unuploadedChunks; }; // 上传分片 const uploadChunk = async (chunkIndex, totalChunks) => { const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, file.value.size); const chunk = file.value.slice(start, end); const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', chunkIndex); formData.append('totalChunks', totalChunks); formData.append('fileId', fileId.value); try { await fetch('/upload', { method: 'POST', body: formData, }); uploadedChunks.value.add(chunkIndex); // 记录已上传的分片 saveProgress(); // 保存上传进度 } catch (error) { console.error('上传失败:', error); throw error; } }; // 保存上传进度到 localStorage const saveProgress = () => { const progressData = { fileId: fileId.value, uploadedChunks: Array.from(uploadedChunks.value), }; localStorage.setItem(fileId.value, JSON.stringify(progressData)); }; // 从 localStorage 加载上传进度 const loadProgress = () => { const progressData = JSON.parse(localStorage.getItem(fileId.value)); if (progressData) { uploadedChunks.value = new Set(progressData.uploadedChunks); } }; // 通知服务端合并文件 const mergeFile = async () => { await fetch('/merge', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ fileId: fileId.value, fileName: file.value.name, totalChunks: Math.ceil(file.value.size / chunkSize), }), }); console.log('文件合并完成'); localStorage.removeItem(fileId.value); // 清除上传进度 }; // 开始上传 const startUpload = async () => { if (!file.value) return; isUploading.value = true; fileId.value = generateFileId(file.value); loadProgress(); // 加载上传进度 const totalChunks = Math.ceil(file.value.size / chunkSize); const unuploadedChunks = getUnuploadedChunks(totalChunks); for (const chunkIndex of unuploadedChunks) { await uploadChunk(chunkIndex, totalChunks); progress.value = Math.round((uploadedChunks.value.size / totalChunks) * 100); console.log(`分片 ${chunkIndex} 上传完成`); } console.log('所有分片上传完成,通知服务端合并文件'); await mergeFile(); isUploading.value = false; progress.value = 100; }; // 选择文件 const handleFileChange = (event) => { const selectedFile = event.target.files[0]; if (selectedFile) { file.value = selectedFile; progress.value = 0; uploadedChunks.value = new Set(); } }; return { file, isUploading, progress, startUpload, handleFileChange, }; }, }; </script> <style scoped> /* 样式可以根据需要自定义 */ div { margin: 20px; } button { margin-top: 10px; } </style>
服务器端代码
const express = require('express'); const fs = require('fs'); const path = require('path'); const multer = require('multer'); const app = express(); const upload = multer({ dest: 'uploads/' }); // 分片存储目录 // 上传分片 app.post('/upload', upload.single('file'), (req, res) => { const { chunkIndex, fileId } = req.body; const chunkPath = path.join('uploads', `${fileId}-${chunkIndex}`); // 将分片移动到指定位置 fs.renameSync(req.file.path, chunkPath); res.send('分片上传成功'); }); // 合并文件 app.post('/merge', express.json(), (req, res) => { const { fileId, fileName, totalChunks } = req.body; const filePath = path.join('uploads', fileName); // 创建可写流 const writeStream = fs.createWriteStream(filePath); // 依次读取分片并写入文件 for (let i = 0; i < totalChunks; i++) { const chunkPath = path.join('uploads', `${fileId}-${i}`); const chunk = fs.readFileSync(chunkPath); writeStream.write(chunk); fs.unlinkSync(chunkPath); // 删除分片 } writeStream.end(); res.send('文件合并完成'); }); app.listen(3000, () => { console.log('服务端运行在 http://localhost:3000'); });
总结
到此这篇关于前端如何写一个大文件上传组件的文章就介绍到这了,更多相关前端大文件上传组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
来源链接:https://www.jb51.net/javascript/339271sh0.htm
© 版权声明
本站所有资源来自于网络,仅供学习与参考,请勿用于商业用途,否则产生的一切后果将由您(转载者)自己承担!
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
THE END
暂无评论内容