松鼠的博客

导航

统计

前端上传大文件处理(切片、断点续传)

思路
1.对文件做切片,即将一个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始
2.通知服务器合并切片,在上传完切片后,前端通知服务器做合并切片操作
3.控制多个请求的并发量,防止多个请求同时发送,造成浏览器内存溢出,导致页面卡死
4.做断点续传,当多个请求中有请求发送失败,例如出现网络故障、页面关闭等,我们得对失败的请求做处理,让它们重复发送

前端
步骤1- 切片,合并切片
在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice通过这个方法,我们就可以对二进制文件进行拆分,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=s, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput">
<button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件
document.getElementById('fileInput').onchange = function({target: {files}}){
file = files[0]
}
// 开始上传
document.getElementById('uploadBtn').onclick = async function(){
if (!file) return
// 创建切片
// let size = 1024 * 1024 * 10 //10MB 切片大小
let size = 1024 * 50 //50KB 切片大小
let fileChunks = []
let index = 0 //切片序号
for(let cur = 0; cur < file.size; cur += size){
fileChunks.push({
hash: index++,
chunk: file.slice(cur, cur + size)
})
}
// 上传切片
const uploadList = fileChunks.map((item, index) => {
let formData = new FormData()
formData.append('filename', file.name)
formData.append('hash', item.hash)
formData.append('chunk', item.chunk)
return axios({
method: 'post',
url: '/upload',
data: formData
})
})
await Promise.all(uploadList)
// 合并切片
await axios({
method: 'get',
url: '/merge',
params: {
filename: file.name
}
});
console.log('上传完成')
}
</script>
</html>

步骤2- 并发控制

结合Promise.race和异步函数实现,多个请求同时并发的数量,防止浏览器内存溢出,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=s, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput">
<button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件
document.getElementById('fileInput').onchange = function({target: {files}}){
file = files[0]
}
// 开始上传
document.getElementById('uploadBtn').onclick = async function(){
if (!file) return
// 创建切片
// let size = 1024 * 1024 * 10; //10MB 切片大小
let size = 1024 * 50 //50KB 切片大小
let fileChunks = []
let index = 0 //切片序号
for(let cur = 0; cur < file.size; cur += size){
fileChunks.push({
hash: index++,
chunk: file.slice(cur, cur + size)
});
}
// 控制并发
let pool = []//并发池
let max = 3 //最大并发量
for(let i=0;i<fileChunks.length;i++){
let item = fileChunks[i]
let formData = new FormData()
formData.append('filename', file.name)
formData.append('hash', item.hash)
formData.append('chunk', item.chunk)
// 上传切片
let task = axios({
method: 'post',
url: '/upload',
data: formData
})
task.then((data)=>{
//请求结束后将该Promise任务从并发池中移除
let index = pool.findIndex(t=> t===task)
pool.splice(index)
})
pool.push(task)
if(pool.length === max){
//每当并发池跑完一个任务,就再塞入一个任务
await Promise.race(pool)
}
}
//所有任务完成,合并切片
await axios({
method: 'get',
url: '/merge',
params: {
filename: file.name
}
});
console.log('上传完成')
}
</script>
</html>

步骤3- 断点续传

在单个请求失败后,触发catch的方法的时候,讲当前请求放到失败列表中,在本轮请求完成后,重复对失败请求做处理,具体代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=s, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.24.0/axios.min.js"></script>
</head>
<body>
<input type="file" id="fileInput">
<button id="uploadBtn">上传</button>
</body>
<script>
// 请求基准地址
axios.defaults.baseURL = 'http://localhost:3000'
// 选中的文件
var file = null
// 选择文件
document.getElementById('fileInput').onchange = function({target: {files}}){
file = files[0]
}
// 开始上传
document.getElementById('uploadBtn').onclick = function(){
if (!file) return;
// 创建切片
// let size = 1024 * 1024 * 10; //10MB 切片大小
let size = 1024 * 50; //50KB 切片大小
let fileChunks = [];
let index = 0 //切片序号
for(let cur = 0; cur < file.size; cur += size){
fileChunks.push({
hash: index++,
chunk: file.slice(cur, cur + size)
})
}
// 控制并发和断点续传
const uploadFileChunks = async function(list){
if(list.length === 0){
//所有任务完成,合并切片
await axios({
method: 'get',
url: '/merge',
params: {
filename: file.name
}
});
console.log('上传完成')
return
}
let pool = []//并发池
let max = 3 //最大并发量
let finish = 0//完成的数量
let failList = []//失败的列表
for(let i=0;i<list.length;i++){
let item = list[i]
let formData = new FormData()
formData.append('filename', file.name)
formData.append('hash', item.hash)
formData.append('chunk', item.chunk)
// 上传切片
let task = axios({
method: 'post',
url: '/upload',
data: formData
})
task.then((data)=>{
//请求结束后将该Promise任务从并发池中移除
let index = pool.findIndex(t=> t===task)
pool.splice(index)
}).catch(()=>{
failList.push(item)
}).finally(()=>{
finish++
//所有请求都请求完成
if(finish===list.length){
uploadFileChunks(failList)
}
})
pool.push(task)
if(pool.length === max){
//每当并发池跑完一个任务,就再塞入一个任务
await Promise.race(pool)
}
}
}
uploadFileChunks(fileChunks)

}
</script>
</html>

后端

步骤1.安装依赖

npm i express@4.17.2
npm i multiparty@4.2.2

步骤2.接口实现

const express = require('express')
const multiparty = require('multiparty')
const fs = require('fs')
const path = require('path')
const { Buffer } = require('buffer')
// 上传文件最终路径
const STATIC_FILES = path.join(__dirname, './static/files')
// 上传文件临时路径
const STATIC_TEMPORARY = path.join(__dirname, './static/temporary')
const server = express()
// 静态文件托管
server.use(express.static(path.join(__dirname, './dist')))
// 切片上传的接口
server.post('/upload', (req, res) => {
const form = new multiparty.Form();
form.parse(req, function(err, fields, files) {
let filename = fields.filename[0]
let hash = fields.hash[0]
let chunk = files.chunk[0]
let dir = `${STATIC_TEMPORARY}/${filename}`
// console.log(filename, hash, chunk)
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir)
const buffer = fs.readFileSync(chunk.path)
const ws = fs.createWriteStream(`${dir}/${hash}`)
ws.write(buffer)
ws.close()
res.send(`${filename}-${hash} 切片上传成功`)
} catch (error) {
console.error(error)
res.status(500).send(`${filename}-${hash} 切片上传失败`)
}
})
})
//合并切片接口
server.get('/merge', async (req, res) => {
const { filename } = req.query
try {
let len = 0
const bufferList = fs.readdirSync(`${STATIC_TEMPORARY}/${filename}`).map((hash, index) => {
const buffer = fs.readFileSync(`${STATIC_TEMPORARY}/${filename}/${index}`)
len += buffer.length
return buffer
});
//合并文件
const buffer = Buffer.concat(bufferList, len);
const ws = fs.createWriteStream(`${STATIC_FILES}/${filename}`)
ws.write(buffer);
ws.close();
res.send(`切片合并完成`);
} catch (error) {
console.error(error);
}
})

server.listen(3000, _ => {
console.log('http://localhost:3000/')
})

 

参考文章:http://blog.ncmem.com/wordpress/2023/09/23/%e5%89%8d%e7%ab%af%e4%b8%8a%e4%bc%a0%e5%a4%a7%e6%96%87%e4%bb%b6%e5%a4%84%e7%90%86%ef%bc%88%e5%88%87%e7%89%87%e3%80%81%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%ef%bc%89/

欢迎入群一起讨论

 

 

posted on   Xproer-松鼠  阅读(1103)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
历史上的今天:
2020-09-23 word图文混排复制到CKEditor图片不显示
2020-09-23 word图文混排复制到UMEditor图片不显示
2020-09-23 word图文混排复制到百度UEditor图片不显示
2020-09-23 ckeditor粘贴word图片自动上传源代码
2020-09-23 ckeditor粘贴word图片自动上传源码
2020-09-23 ckeditor粘贴word图片自动上传代码
2020-09-23 ckeditor粘贴word图片自动上传插件
点击右上角即可分享
微信分享提示