前端如何寫一個大文件上傳組件詳細步驟
1. 核心思路
分片上傳:將大文件分割成多個小文件(chunk),逐個上傳。
記錄上傳進度:通過本地存儲(如
localStorage
)或服務(wù)端記錄已上傳的分片。斷點續(xù)傳:上傳中斷后,重新上傳時只上傳未完成的分片。
合并文件:所有分片上傳完成后,通知服務(wù)端合并文件。
2. 實現(xiàn)步驟
1) 前端分片上傳
使用
File
對象的slice
方法將文件分割成多個分片。通過
FormData
將分片上傳到服務(wù)端。
2) 記錄上傳進度
每個分片上傳成功后,記錄已上傳的分片信息(如分片索引、文件唯一標識等)。
可以使用
localStorage
或服務(wù)端存儲記錄上傳進度。
3) 斷點續(xù)傳
重新上傳時,先檢查已上傳的分片,跳過已上傳的部分。
只上傳未完成的分片。
4) 合并文件
所有分片上傳完成后,通知服務(wù)端合并文件。
3. 代碼實現(xiàn)
前端代碼(原生JS)
class FileUploader { constructor(file, chunkSize = 5 * 1024 * 1024) { // 默認分片大小為5MB this.file = file; this.chunkSize = chunkSize; this.totalChunks = Math.ceil(file.size / chunkSize); // 總分片數(shù) this.chunkIndex = 0; // 當(dāng)前分片索引 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("所有分片上傳完成,通知服務(wù)端合并文件"); await this.mergeFile(); } // 通知服務(wù)端合并文件 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; // 分片大?。?MB) 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); } }; // 通知服務(wù)端合并文件 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('所有分片上傳完成,通知服務(wù)端合并文件'); 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> /* 樣式可以根據(jù)需要自定義 */ div { margin: 20px; } button { margin-top: 10px; } </style>
服務(wù)器端代碼
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); // 創(chuàng)建可寫流 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('服務(wù)端運行在 http://localhost:3000'); });
總結(jié)
到此這篇關(guān)于前端如何寫一個大文件上傳組件的文章就介紹到這了,更多相關(guān)前端大文件上傳組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Javascript中字符串相關(guān)常用的使用方法總結(jié)
本篇文章主要介紹了Javascript中字符串相關(guān)常用的使用方法。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03通過JavaScript腳本復(fù)制網(wǎng)頁上的一個表格
通過JavaScript腳本復(fù)制網(wǎng)頁上的一個表格...2006-07-07JS利用?clip-path?實現(xiàn)動態(tài)區(qū)域裁剪功能
這篇文章主要介紹了JS利用?clip-path?實現(xiàn)動態(tài)區(qū)域裁剪功能,文中主要通過使用 box-shadow 實現(xiàn),代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12