短视频源码,大文件切片上传的实现逻辑
短视频源码,大文件切片上传的实现逻辑
逻辑梗概
将大文件分割成多个文件块
逐个上传文件块
服务端将文件块顺序合并成完整文件
优势分析
减轻服务器压力:如果一次性上传大文件,服务器的存储和网络带宽压力都会非常大,而通过切片,可以将这些压力分散到多个小文件中,减轻服务器的压力。
断点续传、错误重试:因为大文件被肢解了,如果因为一些原因中断、错误了,已经上传的部分就不用再重新上传了,只需要把后续的传上就好了。
前端部分
1.1 切文件(前端)
1.2 判定切片是否完成上传完成(前端)
客户端记录切片的上传状态,只需要上传未成功的切片
1.3 断点、错误续传(前端)
客户端上传文件时,记录已上传的切片位置
下次上传时,根据记录的位置,继续上传
后端部分
1.1 收切片、存切片
将相关切片保存在目标文件夹
1.2 合并切片
服务端根据切片的顺序,将切片合并成完整文件
1.3 文件是否存在校验
服务端根据文件Hash值、文件名,校验该文件是否已经上传
1、搭建基础项目
服务器(基于express)
const express = require('express') const app = express() app.listen(3000, () => { console.log('服务已运行:http://localhost:3000'); })
前端
基础页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> input{ display: block; margin: 10px 0; } </style> </head> <body> <input type="file" id="file"> <input type="button" id="upload" value="上传"> <input type="button" id="continue" value="继续上传"> </body> </html>
引入资源
<script type="module" src="./spark-md5.js"></script> <script type="module" src="./operate.js"></script> operate.js // 获取文件域 const fileEle = document.querySelector("#file"); const uploadButton = document.querySelector("#upload"); const continueButton = document.querySelector("#continue"); uploadButton.addEventListener("click", async () => { console.log("点击了上传按钮") }) continueButton.addEventListener('click', async () => { console.log("点击了继续上传按钮") })
3、静态资源托管(server)
app.use(express.static('static'))
4、上传接口
搭建上传接口(server)
使用body-parser中间价解析请求体
// 导入中间件 const bodyParser = require('body-parser') // 使用中间件 // 处理URL编码格式的数据 app.use(bodyParser.urlencoded({ extended: false })); // 处理JSON格式的数据 app.use(bodyParser.json());
上传接口
app.post('/upload', (req, res) => { res.send({ msg: '上传成功', success: true }) })
测试接口(前端)
// 单个文件上传 const uploadHandler = async (file) => { fetch('http://localhost:3000/upload', { method: "POST", headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ fileName: '大文件', }), }) } uploadButton.addEventListener("click", async (e) => { uploadHandler() })
5、文件上传接口存储文件(server)
使用multer中间件处理上传文件
设置uploadFiles文件夹为文件存储路径
const multer = require('multer') const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, './uploadFiles'); }, }); const upload = multer({ storage }) app.post('/upload', upload.single('file'), (req, res) => { })
测试
// 单个文件上传 const uploadHandler = async (file) => { let fd = new FormData(); fd.append('file', file); fetch('http://localhost:3000/upload', { method: "POST", body: fd }) } uploadButton.addEventListener("click", async () => { let file = fileEle.files[0]; uploadHandler(file) })
6、文件切片
注意
假设切片大小为1M
保存切片顺序(为了合成大文件时正确性)
上传状态(为了断点续传、前端显示进度条)
// 使用单独常量保存预设切片大小 1MB const chunkSize = 1024 * 1024 * 1; // 文件切片 const createChunks = (file) => { // 接受一个文件对象,要把这个文件对象切片,返回一个切片数组 const chunks = []; // 文件大小.slice(开始位置,结束位置) let start = 0; let index = 0; while (start < file.size) { let curChunk = file.slice(start, start + chunkSize); chunks.push({ file: curChunk, uploaded: false, chunkIndex: index, }); index++; start += chunkSize; } return chunks; }
测试文件切片函数
// 存储当前文件所有切片 let chunks = []; uploadButton.addEventListener("click", async () => { let file = fileEle.files[0]; chunks = createChunks(file); console.log(chunks); })
注意:将来要把这些切片全部都上传到服务器,并且最后需要把这些切片合并成一个文件,且要做出文件秒传功能,需要保留当前文件的hash值和文件名,以辨别文件和合并文件。
在页面中引入spark-md5.js
<script type="module" src="./spark-md5.js"></script>
获取文件Hash值
const getHash = (file) => { return new Promise((resolve) => { const fileReader = new FileReader(); fileReader.readAsArrayBuffer(file); fileReader.onload = function (e) { let fileMd5 = SparkMD5.ArrayBuffer.hash(e.target.result); resolve(fileMd5); } }); }
把文件的hash值保存在切片信息中
// 文件hash值 let fileHash = ""; // 文件名 let fileName = ""; // 创建切片数组 const createChunks = (file) => { // 接受一个文件对象,要把这个文件对象切片,返回一个切片数组 const chunks = []; // 文件大小.slice(开始位置,结束位置) let start = 0; let index = 0; while (start < file.size) { let curChunk = file.slice(start, start + chunkSize); chunks.push({ file: curChunk, uploaded: false, fileHash: fileHash, chunkIndex: index, }); index++; start += chunkSize; } return chunks; } // 上传执行函数 const uploadFile = async(file) => { // 设置文件名 fileName = file.name; // 获取文件hash值 fileHash = await getHash(file); chunks = createChunks(file); console.log(chunks); }
7、上传逻辑修改
前端部分
单个文件上传函数修改:
插入文件名、文件Hash值、切片索引
上传成功之后修改状态标识(可用于断点续传、上传进度回显)
// 单个文件上传 const uploadHandler = (chunk) => { return new Promise(async (resolve, reject) => { try { let fd = new FormData(); fd.append('file', chunk.file); fd.append('fileHash', chunk.fileHash); fd.append('chunkIndex', chunk.chunkIndex); let result = await fetch('http://localhost:3000/upload', { method: 'POST', body: fd }).then(res => res.json()); chunk.uploaded = true; resolve(result) } catch (err) { reject(err) } }) }
批量上传切片
限制并发数量(减轻服务器压力)
// 批量上传切片 const uploadChunks = (chunks, maxRequest = 6) => { return new Promise((resolve, reject) => { if (chunks.length == 0) { resolve([]); } let requestSliceArr = [] let start = 0; while (start < chunks.length) { requestSliceArr.push(chunks.slice(start, start + maxRequest)) start += maxRequest; } let index = 0; let requestReaults = []; let requestErrReaults = []; const request = async () => { if (index > requestSliceArr.length - 1) { resolve(requestReaults) return; } let sliceChunks = requestSliceArr[index]; Promise.all( sliceChunks.map(chunk => uploadHandler(chunk)) ).then((res) => { requestReaults.push(...(Array.isArray(res) ? res : [])) index++; request() }).catch((err) => { requestErrReaults.push(...(Array.isArray(err) ? err : [])) reject(requestErrReaults) }) } request() }) }
抽离上传操作
// 文件上传 const uploadFile = async (file) => { // 设置文件名 fileName = file.name; // 获取文件hash值 fileHash = await getHash(file); // 获取切片 chunks = createChunks(file); try { await uploadChunks(chunks) } catch (err) { return { mag: "文件上传错误", success: false } } }
后端部分
修改上传接口,增加功能
使用一个文件Hash值同名的文件夹保存所有切片
这里使用了node内置模块path处理路径
使用fs-extra第三方模块处理文件操作
const path = require('path') const fse = require('fs-extra') app.post('/upload', upload.single('file'), (req, res) => { const { fileHash, chunkIndex } = req.body; // 上传文件临时目录文件夹 let tempFileDir = path.resolve('uploadFiles', fileHash); // 如果当前文件的临时文件夹不存在,则创建该文件夹 if (!fse.pathExistsSync(tempFileDir)) { fse.mkdirSync(tempFileDir) } // 如果无临时文件夹或不存在该切片,则将用户上传的切片移到临时文件夹里 // 如果有临时文件夹并存在该切片,则删除用户上传的切片(因为用不到了) // 目标切片位置 const tempChunkPath = path.resolve(tempFileDir, chunkIndex); // 当前切片位置(multer默认保存的位置) let currentChunkPath = path.resolve(req.file.path); if (!fse.existsSync(tempChunkPath)) { fse.moveSync(currentChunkPath, tempChunkPath) } else { fse.removeSync(currentChunkPath) } res.send({ msg: '上传成功', success: true }) })
8、合并文件
编写合并接口(server)
合并成的文件名为 文件哈希值.文件扩展名
所以需要传入文件Hash值、文件名
app.get('/merge', async (req, res) => { const { fileHash, fileName } = req.query; res.send({ msg: `Hash:${fileHash},文件名:${fileName}`, success: true }); })
请求合并接口(前端)
封装合并请求函数
// 合并分片请求 const mergeRequest = (fileHash, fileName) => { return fetch(`http://localhost:3000/merge?fileHash=${fileHash}&fileName=${fileName}`, { method: "GET", }).then(res => res.json()); };
在切片上传完成后,调用合并接口
// 文件上传 const uploadFile = async (file) => { // 设置文件名 fileName = file.name; // 获取文件hash值 fileHash = await getHash(file); // 获取切片 chunks = createChunks(file); try { await uploadChunks(chunks) await mergeRequest(fileHash, fileName) } catch (err) { return { mag: "文件上传错误", success: false } } }
合并接口逻辑
1、根据文件Hash值,找到所有切片
app.get('/merge', async (req, res) => { const { fileHash, fileName } = req.query; // 最终合并的文件路径 const filePath = path.resolve('uploadFiles', fileHash + path.extname(fileName)); // 临时文件夹路径 let tempFileDir = path.resolve('uploadFiles', fileHash); // 读取临时文件夹,获取所有切片 const chunkPaths = fse.readdirSync(tempFileDir); console.log('chunkPaths:', chunkPaths); res.send({ msg: "合并成功", success: true }); })
合并接口逻辑
2、遍历获取所有切片路径数组,根据路径找到切片,合并成一个文件,删除原有文件夹
app.get('/merge', async (req, res) => { const { fileHash, fileName } = req.query; // 最终合并的文件路径 const filePath = path.resolve('uploadFiles', fileHash + path.extname(fileName)); // 临时文件夹路径 let tempFileDir = path.resolve('uploadFiles', fileHash); // 读取临时文件夹,获取所有切片 const chunkPaths = fse.readdirSync(tempFileDir); console.log('chunkPaths:', chunkPaths); // 将切片追加到文件中 let mergeTasks = []; for (let index = 0; index < chunkPaths.length; index++) { mergeTasks.push(new Promise((resolve) => { // 当前遍历的切片路径 const chunkPath = path.resolve(tempFileDir, index + ''); // 将当前遍历的切片切片追加到文件中 fse.appendFileSync(filePath, fse.readFileSync(chunkPath)); // 删除当前遍历的切片 fse.unlinkSync(chunkPath); resolve(); })) } await Promise.all(mergeTasks); // 等待所有切片追加到文件后,删除临时文件夹 fse.removeSync(tempFileDir); res.send({ msg: "合并成功", success: true }); })
10、断点续传
封装continueUpload方法
在continueUpload方法中,只上传 uploaded 为false的切片
修改后此功能对用户来说即是黑盒,用户只需要重复调用continueUpload方法即可
// 文件上传 const continueUpload = async (file) => { if(chunks.length == 0 || !fileHash || !fileName){ return; } try { await uploadChunks(chunks.filter(chunk => !chunk.uploaded)) await mergeRequest(fileHash, fileName) } catch (err) { return { mag: "文件上传错误", success: false } } }
以上就是短视频源码,大文件切片上传的实现逻辑, 更多内容欢迎关注之后的文章
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2023-08-31 直播平台开发,multiple-select 单选框多选框的用法
2023-08-31 直播app开发搭建,动态广场内,图片上传到软件
2023-08-31 直播软件搭建,element ui Backtop 回到顶部
2022-08-31 成品直播源码推荐,TabLayout 去掉按下时的阴影效果
2022-08-31 视频直播app源码,Flutter实战底部导航栏
2022-08-31 app直播源代码,如何自适应页面剩余高度
2021-08-31 直播带货系统源码利用FloatingActionButton实现 展开/折叠多级悬浮菜单