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

前端大文件分片上传+后端(node)接收分片并合并

一、前言

 前几天面试时面试官问我封装的图片上传组件有没有做大文件上传处理,我说我知道大文件切片上传,但是没做,因为文件上传的图片视频不怎么大,几M十几M的,然后对方就抓住我没做大文件上传处理这个点不放,说我缺少深入研究没能做得更全面巴拉巴拉。。。

确实是我研究不够,得,那我就来研究下大文件上传吧

二、大文件上传思路

前端分片上传是将大文件分割成多个较小的片段(分片),分别上传这些片段,最后在服务器端将这些分片合并成完整的文件。这样做可以降低网络不稳定带来的风险,实现断点续传等功能。

  • 选择文件:通过input[type="file"]获取用户选择的文件。
  • 计算分片:根据设定的chunkSize(比如设为 1MB)计算文件需要分成的总片数totalChunks
  • 循环上传:使用for循环遍历每个分片,通过file.slice方法截取每个分片。
  • 构建请求:将每个分片以及分片的序号chunkNumber和总分片数totalChunks一起通过FormData构建请求体。
  • 发起请求:使用fetch发送 POST 请求到/upload接口进行上传,并根据响应判断上传是否成功,同时更新上传进度。
  • 结果反馈:如果所有分片都上传成功,打印成功信息,发起合并请求;否则,打印错误信息。
  • 后端:       获取分片文件并存储,接收到合并请求后合并文件,存储文件或者返回url给前端

三、前端代码 (Vue)


1、创建calculateFileMD5.js

// calculateFileMD5.jsimport SparkMD5 from 'spark-md5'; // 安装spark-md5包
export const chunkSize = 1024 * 1024; // 每次读取 1MB// 计算文件的 MD5
export default function calculateFileMD5(file) {return new Promise(resolve => {const spark = new SparkMD5.ArrayBuffer(); // 创建 SparkMD5 对象const fileSize = file.size; // 文件大小const chunks = Math.ceil(fileSize / chunkSize); // 总片数let currentChunk = 0; // 当前片数const fileReader = new FileReader(); // 创建 FileReader 对象fileReader.onload = function (e) {spark.append(e.target.result); // 将读取的块加入 spark-md5currentChunk++; // 切片数加 1// 如果还有未处理的块,则继续读取下一个块if (currentChunk < chunks) {readNextChunk();} else {resolve(spark.end()); // 完成,输出 MD5}};fileReader.onerror = function () {console.error('文件读取失败');resolve(null);};// 读取下一个块function readNextChunk() {const start = currentChunk * chunkSize;const end = Math.min(start + chunkSize, fileSize);fileReader.readAsArrayBuffer(file.slice(start, end));}readNextChunk(); // 开始读取第一个 chunk});
}

2、创建upload.js

 

import PQueue from 'p-queue'; // 安装p-queue处理请求队列
import { chunkSize } from './calculateFileMD5';// 检查文件是否存在
export async function checkFileExist(hash) {const res = await fetch(`http://127.0.0.1/check?hash=${hash}`, {headers: {'Content-Type': 'application/json',},});return res.ok && (await res.json()).exist;
}// 上传分片
export async function uploadChunks(file, hash, onProgress) {const totalChunks = Math.ceil(file.size / chunkSize); // 片数const uploadedChunks = []; // 已上传的分片// 检查已上传的分片const res = await fetch(`http://127.0.0.1/check-chunks?hash=${hash}`);const existChunks = res.ok ? await res.json() : [];// 创建并发队列,控制并发数为6const uploadQueue = new PQueue({ concurrency: 6 });// 添加所有分片任务到队列中for (let i = 0; i < totalChunks; i++) {// 进度展示if (existChunks.includes(i)) {onProgress(i + 1, totalChunks);continue;}const start = i * chunkSize; // 计算当前分片的起始位置和结束位置const end = Math.min(start + chunkSize, file.size); // 确保分片不超出文件末尾const chunk = file.slice(start, end); // 创建分片// 创建 FormData 对象const formData = new FormData();formData.append('file', chunk); // 添加分片数据formData.append('chunkNumber', i); // 添加分片编号formData.append('totalChunks', totalChunks); // 添加总分片数formData.append('fileHash', hash); // 添加文件 Hash// 发送分片上传请求uploadQueue.add(async () => {try {const response = await fetch('http://127.0.0.1/uploadFile', {method: 'POST',body: formData, // 使用 FormData});// 检查上传结果if (response.ok) {uploadedChunks.push(i);onProgress(uploadedChunks.length, totalChunks);}} catch (error) {console.error(`第 ${i} 个分片上传失败`, error);}});}// 等待所有分片上传完成await uploadQueue.onIdle();return uploadedChunks;
}// 合并分片
export async function mergeChunks(hash, totalChunks, fileType) {const res = await fetch('http://127.0.0.1/merge', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ hash, totalChunks, fileType }),});return res.ok;
}

三、选择文件,上传分片,发起合并请求

某vue文件:

<template><h1>大文件上传</h1><div class="card"><input type="file" @change="handleUpload" /><div>{{ progress }}</div></div>
</template>
<script setup>
import { ref } from 'vue';
import calculateFileMD5, { chunkSize } from '@/utils/calculateFileMD5.js';
import { checkFileExist, uploadChunks, mergeChunks } from '@/utils/upload.js';const progress = ref('');// 文件上传
async function handleUpload(e) {const file = e.target?.files?.[0];if (!file) return;progress.value = '正在计算文件哈希...';const fileHash = await calculateFileMD5(file);progress.value = '检查是否已存在该文件...';const exist = await checkFileExist(fileHash);console.log(exist);if (exist) {progress.value = '文件已存在,秒传成功!';return;}progress.value = '开始上传分片...';// 上传进度展示const onProgress = (uploaded, total) => {progress.value = `上传进度: ${((uploaded / total) * 100).toFixed(2)}%`;};// 调用 uploadChunks 函数,上传当前文件的所有未上传分片,返回值 uploadedChunks 是一个数组,包含本次成功上传的分片编号const uploadedChunks = await uploadChunks(file, fileHash, onProgress);// 判断刚刚上传的分片数量是否等于该文件的总分片数。// file.size / chunkSize 计算出理论上的分片总数,Math.ceil() 是为了向上取整(比如 1.2MB 文件按 1MB 分片,结果为 2 个分片)。// 如果相等,说明所有分片都已上传完成。if (uploadedChunks.length === Math.ceil(file.size / chunkSize)) {const fileType = file.type || 'unknown'; // 获取文件类型,若无则标记为 unknownconst [, typeSub] = fileType.split('/'); // 提取子类型// 所有分片上传完成后,调用 mergeChunks 接口通知服务器开始合并文件await mergeChunks(fileHash, uploadedChunks.length, typeSub);progress.value = '上传并合并完成!';}
}
</script>

四、node后端代码(express框架)

后端处理:

  • 文件存在性检查,若存在用于秒传
  • 分片列表检查,用于断点续传
  • 接收前端上传得分片
  • 合并分片
const express = require('express'); // 引入express,需安装
const path = require('path');
const fs = require('fs');
const fsp = require('fs/promises');
const fse = require('fs-extra'); // 引入fs-extra,需安装
const multiparty = require('multiparty'); // 引入multiparty,需安装
var router = express.Router();// 大文件分片上传const CHUNK_DIR = './uploads/chunks/'; // 设定分片目录
const UPLOAD_DIR = './uploads/files/'; // 设定文件目录// 目录不存在则自动创建
if (!fs.existsSync(CHUNK_DIR)) {fs.mkdirSync(CHUNK_DIR, { recursive: true });
}
if (!fs.existsSync(UPLOAD_DIR)) {fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}// 文件存在性检查
router.get('/check', async (req, res) => {const { hash } = req.query;try {await fsp.access(`${UPLOAD_DIR}${hash}`); // 检查文件是否存在res.json({ exist: true });} catch (error) {res.json({ exist: false });}
});// 分片列表检查
router.get('/check-chunks', async (req, res) => {const { hash } = req.query;const dir = path.join(CHUNK_DIR, hash);if (!fs.existsSync(dir)) return res.json([]);const files = fs.readdirSync(dir);res.json(files.map(Number));
});// 上传分片
router.post('/uploadFile', async (req, res) => {// 创建 multiparty 实例,用于解析请求数据const form = new multiparty.Form();form.parse(req, async (err, fields, files) => {const fileHash = fields.fileHash[0];const chunkHash = fields.fileHash[0] + '-' + fields.chunkNumber[0];const dir = path.join(CHUNK_DIR, fileHash);await fse.ensureDir(dir); // 创建分片目录,如果没有的话await fse.move(files.file[0].path, path.resolve(dir, chunkHash)); // 移动分片文件到分片目录res.json({ success: true });});
});// 合并分片
router.post('/merge', async (req, res) => {const { hash, totalChunks, fileType } = req.body;const chunkDir = path.join(CHUNK_DIR, hash); // 分片目录const targetPath = path.join(UPLOAD_DIR, `${hash}.${fileType}`); // 存储到UPLOAD_DIR目录下,文件名为hash.fileType// 创建写入流const writeStream = fs.createWriteStream(targetPath);// 读取每个分片并写入写入流for (let i = 0; i < totalChunks; i++) {const chunkPath = path.join(chunkDir, `${hash}-${i}`);const data = fs.readFileSync(chunkPath);writeStream.write(data);}writeStream.end();// fs.rmSync(chunkDir, { recursive: true }); // 清空分片目录// 返回合并后的文件URLconst host = req.get('host');const protocol = req.protocol;const imageUrl = `${protocol}://${host}/files/${hash}.${fileType}`;res.json({ merged: true, url: imageUrl });
});module.exports = router;

四、结果展示

后端文件存储

我选择一个8000KB的图片,按照1M分片,则是8片

结果如下:
 

浏览器请求

 

merge结果

五、结语 

至此,一个大文件分片上传的前后端流程就完成了,当然还有很多地方可以优化,这个具体应用时再根据实际优化,了解思路后怎么优化你都能得心应手。


渐修顿悟

相关文章:

  • Android 网络请求的选择逻辑(Connectivity Modules)
  • 深入解析 MySQL 并发控制:读写锁、锁粒度与高级优化
  • 数据库(考前两天版本)
  • 李沐动手深度学习(pycharm中运行笔记)——11.模型选择+过拟合欠拟合
  • SQL关键字三分钟入门:UNION 与 UNION ALL —— 数据合并全攻略
  • RKNN开发环境搭建3-RKNN Model Zoo 板载部署以Whisper为例
  • pyqt 简单条码系统
  • OpenStack入门
  • 搭建简易采购系统:从需求分析到供应商数据库设计
  • 【第二章:机器学习与神经网络概述】01.聚类算法理论与实践-(2)层次聚类算法(Hierarchical Clustering)
  • 【对比】DeepAR 和 N-Beats
  • 【unitrix】 3.0 基本结构体(types.rs)
  • python 解码 jwt
  • javaweb -Ajax
  • LVS—DR模式
  • 最新FVCOM 潮流、波浪、泥沙、水质、温盐、染色剂、粒子示踪、嵌套、背景流、自动化全流程
  • 在线教育平台敏捷开发项目
  • CppCon 2017 学习:C++ in Academia
  • ModbusTcp使用
  • Qt事件处理机制
  • 使用织梦系统建设网站/核心关键词是什么意思
  • 网站建设页面/收录优美图片手机版
  • 无人一区二区区别是什么/seo的搜索排名影响因素主要有
  • 青岛建设投资公司网站/中国培训网
  • 体育网站界面该怎样做/qq群排名优化软件
  • 没有平台没有网站怎么做外贸/郑州网站seo服务