前端如何寫一個大文件上傳組件詳細步驟
1. 核心思路
分片上傳:將大文件分割成多個小文件(chunk),逐個上傳。
記錄上傳進度:通過本地存儲(如
localStorage)或服務端記錄已上傳的分片。斷點續(xù)傳:上傳中斷后,重新上傳時只上傳未完成的分片。
合并文件:所有分片上傳完成后,通知服務端合并文件。
2. 實現(xiàn)步驟
1) 前端分片上傳
使用
File對象的slice方法將文件分割成多個分片。通過
FormData將分片上傳到服務端。
2) 記錄上傳進度
每個分片上傳成功后,記錄已上傳的分片信息(如分片索引、文件唯一標識等)。
可以使用
localStorage或服務端存儲記錄上傳進度。
3) 斷點續(xù)傳
重新上傳時,先檢查已上傳的分片,跳過已上傳的部分。
只上傳未完成的分片。
4) 合并文件
所有分片上傳完成后,通知服務端合并文件。
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; // 當前分片索引
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; // 分片大?。?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);
}
};
// 通知服務端合并文件
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>
/* 樣式可以根據(jù)需要自定義 */
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);
// 創(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('服務端運行在 http://localhost:3000');
});總結
到此這篇關于前端如何寫一個大文件上傳組件的文章就介紹到這了,更多相關前端大文件上傳組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
通過JavaScript腳本復制網(wǎng)頁上的一個表格
通過JavaScript腳本復制網(wǎng)頁上的一個表格...2006-07-07
JS利用?clip-path?實現(xiàn)動態(tài)區(qū)域裁剪功能
這篇文章主要介紹了JS利用?clip-path?實現(xiàn)動態(tài)區(qū)域裁剪功能,文中主要通過使用 box-shadow 實現(xiàn),代碼簡單易懂,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12

