当前位置: 首页 > news >正文

vue3和element plus, node和express实现大文件上传, 分片上传,断点续传完整开发代码

1. 前端UploadFile.vue上传组件

<template><div class="upload-container"><!-- 上传 --><el-upload class="upload-demo" :action="action" :auto-upload="false" :on-change="handleFileChange":on-success="handleSuccess" :on-error="handleError" :on-progress="handleProgress"><i class="el-icon-upload"></i><div class="upload-text">{{ uploadText }}</div></el-upload><div v-if="progress !== null" class="progress-container"><div class="progress-bar"><div class="progress-fill" :style="{ width: progress + '%' }"></div></div><span class="progress-text">{{ progress }}%</span></div><!-- 调试信息 --><div class="debug-info"><p>上传地址: {{ props.action }}</p><p>当前文件: {{ currentFile ? currentFile.name : '无' }}</p><p>进度: {{ progress !== null ? progress + '%' : '未开始' }}</p><p>上传状态: {{ isUploading ? '上传中' : '空闲' }}</p><p>已上传分片: {{ uploadedChunks.size }}/{{ totalChunks }}</p><button @click="testUpload" class="test-btn">测试上传功能</button><button @click="resetUpload" class="test-btn" style="background-color: #f56c6c; margin-left: 10px;">重置上传</button></div></div>
</template><script setup>
import { ref } from 'vue';const props = defineProps({action: {type: String,default: 'https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15',},uploadText: {type: String,default: '点击或拖拽文件上传',},
});const fileList = ref([]);
const progress = ref(null);
let fileChunks = [];
let totalChunks = 0;
let currentFile = null;
let uploadedChunks = new Set(); // 记录已上传的分片
let isUploading = false; // 上传状态const handleFileChange = (file, fileList) => {console.log('文件选择成功:', file.name, file.size);console.log('文件列表:', fileList);// 如果正在上传,不允许选择新文件if (isUploading) {console.log('正在上传中,请等待完成后再选择新文件');return;}// 重置状态progress.value = 0;uploadedChunks.clear();// 保存文件currentFile = file.raw || file; // 处理 Element Plus 的文件对象checkUploadStatus(currentFile);
};// 检查上传状态(断点续传)
const checkUploadStatus = async (file) => {console.log('检查文件上传状态:', file.name);const chunkSize = 5 * 1024 * 1024; // 每个文件片的大小 5MBtotalChunks = Math.ceil(file.size / chunkSize);try {const response = await fetch(`${props.action.replace('/upload', '/check-upload')}`, {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({fileName: file.name,totalChunks: totalChunks})});const data = await response.json();console.log('上传状态检查结果:', data);if (data.success) {// 记录已上传的分片uploadedChunks = new Set(data.uploadedChunks || []);if (data.isComplete) {console.log('文件已完全上传');progress.value = 100;return;}// 计算当前进度const currentProgress = Math.round((uploadedChunks.size / totalChunks) * 100);progress.value = currentProgress;console.log(`断点续传:已上传 ${uploadedChunks.size}/${totalChunks} 个分片,进度: ${currentProgress}%`);// 开始分片处理chunkFile(file);} else {console.log('检查上传状态失败,开始全新上传');chunkFile(file);}} catch (error) {console.error('检查上传状态出错:', error);console.log('开始全新上传');chunkFile(file);}
};const chunkFile = (file) => {console.log('开始分片处理:', file.name, '文件大小:', file.size);const chunkSize = 5 * 1024 * 1024; // 每个文件片的大小 5MBtotalChunks = Math.ceil(file.size / chunkSize);fileChunks = [];console.log('文件将分为', totalChunks, '个分片');for (let i = 0; i < totalChunks; i++) {const start = i * chunkSize;const end = Math.min(start + chunkSize, file.size);const chunk = file.slice(start, end);fileChunks.push(chunk);}console.log('分片完成,开始上传');// 开始上传每个分片uploadChunks();
};const uploadChunks = () => {isUploading = true;const uploadNextChunk = (chunkIndex) => {// 跳过已上传的分片while (chunkIndex < fileChunks.length && uploadedChunks.has(chunkIndex)) {console.log(`分片 ${chunkIndex} 已上传,跳过`);chunkIndex++;}if (chunkIndex >= fileChunks.length) {console.log('文件上传完成');progress.value = 100;isUploading = false;return;}console.log(`开始上传第 ${chunkIndex + 1}/${totalChunks} 个分片`);const chunk = fileChunks[chunkIndex];const formData = new FormData();formData.append('file', chunk);formData.append('fileName', currentFile.name);formData.append('chunkIndex', chunkIndex);formData.append('totalChunks', totalChunks);console.log('上传地址:', props.action);// 使用 fetch API 上传文件切片fetch(props.action, {method: 'POST',body: formData,}).then((response) => {console.log('服务器响应状态:', response.status);if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}return response.json();}).then((data) => {console.log('服务器响应数据:', data);if (data.success) {// 标记分片为已上传uploadedChunks.add(chunkIndex);// 更新进度progress.value = Math.round((uploadedChunks.size / totalChunks) * 100);console.log(`第 ${chunkIndex + 1} 个分片上传成功,进度: ${progress.value}%`);// 检查是否完成if (data.isComplete) {console.log('所有分片上传完成');progress.value = 100;isUploading = false;return;}uploadNextChunk(chunkIndex + 1); // 上传下一个切片} else {console.error('服务器返回失败:', data);isUploading = false;}}).catch((error) => {console.error('上传失败:', error);isUploading = false;// 不重置进度,保持断点续传状态});};// 从第一个未上传的分片开始let startIndex = 0;while (startIndex < fileChunks.length && uploadedChunks.has(startIndex)) {startIndex++;}if (startIndex < fileChunks.length) {uploadNextChunk(startIndex);} else {console.log('所有分片都已上传完成');progress.value = 100;isUploading = false;}
};const handleError = (error, file, fileList) => {console.log('上传失败', error);
};const handleProgress = (event, file, fileList) => {console.log(`文件 ${file.name} 上传进度:${event.percent}%`);
};const handleSuccess = (response, file, fileList) => {console.log('上传成功', response);
};// 测试函数
const testUpload = () => {console.log('测试上传功能');console.log('Props:', props);console.log('File:', currentFile);console.log('Progress:', progress.value);console.log('Uploaded chunks:', Array.from(uploadedChunks));console.log('Is uploading:', isUploading);
};// 重置上传
const resetUpload = () => {console.log('重置上传状态');isUploading = false;progress.value = null;uploadedChunks.clear();currentFile = null;fileChunks = [];totalChunks = 0;
};</script><style scoped>
.upload-container {padding: 20px;text-align: center;border: 1px solid #000;
}.upload-text {margin-top: 10px;
}.progress-container {margin-top: 20px;display: flex;align-items: center;gap: 10px;justify-content: center;
}.progress-bar {width: 200px;height: 8px;background-color: #f0f0f0;border-radius: 4px;overflow: hidden;
}.progress-fill {height: 100%;background-color: #409eff;transition: width 0.3s ease;
}.progress-text {font-size: 14px;color: #666;min-width: 40px;
}.debug-info {margin-top: 20px;padding: 10px;background-color: #f5f5f5;border-radius: 4px;text-align: left;
}.debug-info p {margin: 5px 0;font-size: 12px;color: #666;
}.test-btn {margin-top: 10px;padding: 5px 10px;background-color: #409eff;color: white;border: none;border-radius: 4px;cursor: pointer;font-size: 12px;
}.test-btn:hover {background-color: #337ecc;
}
</style>

2. 后端本地启动的express服务

2.1 初始化文件夹和安装依赖命令
npm init -y
npm i cors
npm i express
npm i multer
2.2 server.js代码
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const cors = require('cors');const app = express();// 重要:CORS配置必须在路由之前
app.use(cors({origin: ['http://localhost:5173', 'http://localhost:3000'], // 允许的前端地址credentials: true,methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],allowedHeaders: ['Content-Type', 'Authorization']
}));// 解析JSON和URL编码的请求体
app.use(express.json());
app.use(express.urlencoded({ extended: true }));const upload = multer({ dest: 'public/uploads/' });const UPLOAD_DIR = './public/uploads/';// 确保上传目录存在
if (!fs.existsSync(UPLOAD_DIR)) {fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}// 检查文件上传状态(断点续传)
app.post('/check-upload', (req, res) => {const { fileName, totalChunks } = req.body;console.log(`检查文件上传状态: ${fileName}`);const chunkDir = path.join(UPLOAD_DIR, fileName);const uploadedChunks = [];if (fs.existsSync(chunkDir)) {for (let i = 0; i < totalChunks; i++) {const chunkPath = path.join(chunkDir, `chunk-${i}`);if (fs.existsSync(chunkPath)) {uploadedChunks.push(i);}}}console.log(`已上传分片: ${uploadedChunks.join(', ')}`);res.json({ success: true, uploadedChunks,isComplete: uploadedChunks.length === totalChunks});
});// 接收文件分片
app.post('/upload', upload.single('file'), (req, res) => {console.log('收到上传请求:', req.body);const { file, body } = req;const { fileName, chunkIndex, totalChunks } = body;if (!file) {return res.status(400).json({ success: false, error: 'No file received' });}const chunkDir = path.join(UPLOAD_DIR, fileName);if (!fs.existsSync(chunkDir)) {fs.mkdirSync(chunkDir, { recursive: true });}const chunkPath = path.join(chunkDir, `chunk-${chunkIndex}`);fs.renameSync(file.path, chunkPath);console.log(`Received chunk ${chunkIndex} of ${totalChunks} for file: ${fileName}`);// 检查是否所有分片都已上传完成const uploadedChunks = [];for (let i = 0; i < totalChunks; i++) {const checkChunkPath = path.join(chunkDir, `chunk-${i}`);if (fs.existsSync(checkChunkPath)) {uploadedChunks.push(i);}}// 合并所有分片(在上传完成后进行)if (uploadedChunks.length === totalChunks) {console.log(`所有分片上传完成,开始合并: ${fileName}`);mergeChunks(fileName, totalChunks);}res.json({ success: true, message: `Chunk ${chunkIndex} uploaded successfully`,uploadedChunks,isComplete: uploadedChunks.length === totalChunks});
});// 合并文件分片
function mergeChunks(fileName, totalChunks) {const chunkDir = path.join(UPLOAD_DIR, fileName);const filePath = path.join(UPLOAD_DIR, fileName);console.log(`开始合并文件: ${fileName}, 分片数: ${totalChunks}`);console.log(`分片目录: ${chunkDir}`);console.log(`目标文件: ${filePath}`);// 如果只有一个分片,直接重命名if (totalChunks === 1) {const chunkPath = path.join(chunkDir, 'chunk-0');if (fs.existsSync(chunkPath)) {fs.renameSync(chunkPath, filePath);fs.rmdirSync(chunkDir);console.log(`单分片文件 ${fileName} 合并完成`);} else {console.error(`分片文件不存在: ${chunkPath}`);}return;}// 多个分片需要合并const writeStream = fs.createWriteStream(filePath);let completedChunks = 0;for (let i = 0; i < totalChunks; i++) {const chunkPath = path.join(chunkDir, `chunk-${i}`);if (!fs.existsSync(chunkPath)) {console.error(`分片文件不存在: ${chunkPath}`);continue;}const chunkStream = fs.createReadStream(chunkPath);chunkStream.pipe(writeStream, { end: false });chunkStream.on('end', () => {completedChunks++;fs.unlinkSync(chunkPath); // 删除分片文件console.log(`分片 ${i} 合并完成`);if (completedChunks === totalChunks) {writeStream.end();console.log(`文件 ${fileName} 上传完成`);// 删除分片目录try {fs.rmdirSync(chunkDir);console.log(`分片目录已删除: ${chunkDir}`);} catch (error) {console.error(`删除分片目录失败: ${error.message}`);}}});chunkStream.on('error', (error) => {console.error(`读取分片 ${i} 失败:`, error);});}writeStream.on('finish', () => {console.log(`文件 ${fileName} 合并完成`);});writeStream.on('error', (error) => {console.error(`写入文件 ${fileName} 失败:`, error);});
}// 添加根路径路由
app.get('/', (req, res) => {res.json({ message: 'File upload server is running!',endpoints: {upload: 'POST /upload - Upload file chunks'}});
});// 错误处理中间件
app.use((error, req, res, next) => {console.error('Server error:', error);res.status(500).json({ success: false, error: 'Internal server error' });
});app.listen(3000, () => {console.log('Server running on port 3000');console.log('CORS enabled for:', ['http://localhost:5173', 'http://localhost:3000']);
});


文章转载自:

http://qM0mrNtp.rfmzc.cn
http://gQu4GdGi.rfmzc.cn
http://ysiNZdmW.rfmzc.cn
http://oOEDAAkN.rfmzc.cn
http://hBAtGxqD.rfmzc.cn
http://Z08An1Ry.rfmzc.cn
http://yPGwS9x1.rfmzc.cn
http://h8KoHZ1q.rfmzc.cn
http://gZDdV71Z.rfmzc.cn
http://0dlX2NFY.rfmzc.cn
http://TyF8oBSj.rfmzc.cn
http://wM4M7UtS.rfmzc.cn
http://5cSmjatz.rfmzc.cn
http://L7TLI8C8.rfmzc.cn
http://ukEe6mfW.rfmzc.cn
http://O6t7bz26.rfmzc.cn
http://W4WBB5Mj.rfmzc.cn
http://Gau9nq45.rfmzc.cn
http://LNjKQpMT.rfmzc.cn
http://aEpV93Lp.rfmzc.cn
http://WA1ugsMb.rfmzc.cn
http://IvLAEPsS.rfmzc.cn
http://qzHG7Pwr.rfmzc.cn
http://mZvGZemH.rfmzc.cn
http://9HwUTs52.rfmzc.cn
http://YnM7hVtL.rfmzc.cn
http://INjf5LO4.rfmzc.cn
http://4XP4KHXn.rfmzc.cn
http://Tns4kJYN.rfmzc.cn
http://8qeJ4PTA.rfmzc.cn
http://www.dtcms.com/a/387584.html

相关文章:

  • electron-egg使用ThinkPHP项目指南
  • 温州工业自动化科技工厂如何实现1台服务器10个研发设计同时用
  • 如何用PM2托管静态文件
  • Java程序设计:基本数据类型
  • 在k8s环境下部署kanboard项目管理平台
  • 为什么 MySQL utf8 存不下 Emoji?utf8mb4 实战演示
  • 2025 年 PHP 常见面试题整理以及对应答案和代码示例
  • (二十五)、在 k8s 中部署证书,为网站增加https安全认证
  • 风机巡检目前有什么新技术?
  • 震坤行工业超市开放平台接口实战:工业品精准检索与详情解析全方案
  • 河南萌新联赛2025第(八)场:南阳理工学院
  • docker回收和mysql备份导入导致数据丢失恢复---惜分飞
  • 「Memene 摸鱼日报 2025.9.17」上海张江人工智能创新小镇正式启动,华为 DCP 技术获网络顶会奖项
  • 【数据结构】顺序表,ArrayList
  • 第十二章 Arm C1-Premium GIC CPU接口详解
  • 【数据结构---并查集】(并查集的原理,实现与应用)
  • 【数据结构-KMP算法(学习篇)】
  • Start application catch exception
  • 机器视觉在半导体封装检测中的应用
  • 雅菲奥朗SRE知识墙分享(九):『变更管理的定义与实践』
  • 51c视觉~3D~合集6
  • webRTC 的协议族
  • 线激光相机 眼在手上六轴机器人手眼标定 备忘记录
  • QML学习笔记(一)基本了解和工程配置
  • 大数据毕业设计选题推荐-基于大数据的牛油果数据可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • Hadoop单机模式下运行grep实例,output文件目录不存在
  • 【docker】清理中断构建后产生的镜像和缓存
  • Vue2项目集成打包分析工具webpack-bundle-analyzer
  • 【阶梯波发生器如何控制电压和周期】2022-12-9
  • Java 设计模式之桥接模式(Bridge Pattern)