原生js+express分片上传大文件
原理
- 前端靠blob切好每个部分,发送至服务端
- 服务端收到文件后保存唯一部分,全部上传完成后合并文件
依赖包
要点
- 前端判断文件大小
let end = start + chunkSize >= f.size ? f.size : start + chunkSize;
- 前端切片部分
// blob 和 file 相互转换
let aimfile = new File(
[f.slice(start, end)],
`${f.name.split('.')[0]}.${index}.${f.name.split('.')[1]}`, {
type: f.type,
lastModified: Date.now()
}
)
- 前端ajax方法封装
function ajax(file, cb, index, method = 'POST', url = 'http://localhost:5000/upload') {
let xhr = new XMLHttpRequest()
xhr.open(method, url+'?current='+index, true)
xhr.send(file)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
cb(xhr.responseText)
}
}
}
- 后端接口请求处理
最关键的信息,storage
磁盘存储引擎 (DiskStorage)
磁盘存储引擎可以让你控制文件的存储。
//官方介绍
var storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, '/tmp/my-uploads')
},
filename: function (req, file, cb) {
cb(null, file.fieldname + '-' + Date.now())
}
})
var upload = multer({ storage: storage })
有两个选项可用,destination 和 filename。他们都是用来确定文件存储位置的函数。
destination 是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string (例如 ‘/tmp/uploads’)。如果没有设置 destination,则使用操作系统默认的临时文件夹。
注意: 如果你提供的 destination 是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。
filename 用于确定文件夹中的文件名的确定。 如果没有设置 filename,每个文件将设置为一个随机文件名,并且是没有扩展名的。
注意: Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。
每个函数都传递了请求对象 (req) 和一些关于这个文件的信息 (file),有助于你的决定。
注意 req.body 可能还没有完全填充,这取决于向客户端发送字段和文件到服务器的顺序。
- 加工处理
let timer = null
let storage = multer.diskStorage({
destination: function (req, file, cb) {
// 新增目录存放文件
const chunksPath = path.join('public/upload', file.originalname.split('.')[0]);
if (!fs.existsSync(chunksPath)) {
fs.mkdirSync(chunksPath, { recursive: true }); // 新建文件夹
}
// 配置文件保存路径
cb(null, path.join('public/upload', file.originalname.split('.')[0]))
// 合并文件并删除目录
clearTimeout(timer)
timer = setTimeout(async () => {
// 合并文件(获取子文件名称)
await fs.readdir(chunksPath, (err, files) => {
if (err) throw err
files.map((res) => {
// 同步合并文件
fs.appendFileSync(
`public/upload/${file.originalname.split('.')[0]}.${file.originalname.split('.')[2]}`,
fs.readFileSync(path.join(chunksPath, res)), // 读取文件
(err) => { if (err) throw err }
)
});
})
// 删除文件
const delFile = () => {
fs.readdir(chunksPath, (err, files) => {
if (err) throw err
if (!files.length) {
delFile()
} else {
files.map(res => {
fs.unlinkSync(path.join(chunksPath, res), (e) => { if (err) throw err });
})
}
})
}
await delFile()
// 删除文件夹
const del = () => {
// 判断子文件是否为空
fs.readdir(chunksPath, (err, files) => {
if (files.length != 0) {
del()
} else {
// 为空则删除文件夹
fs.rmdir(chunksPath, (err) => { if (err) throw err })
}
})
}
await del()
}, 500)
},
filename: function (req, file, cb) {
const split = file.originalname.split('.')
cb(null, `${split[0]}-${split[1]}`) // 文件名
}
});
let upload = multer({ storage: storage });
app.use('/upload', upload.single('file'), function (req, res, next) {
if (req.file) res.send('file ' + req.query.current + ' upload success!')
})
使用中间件处理请求文件。
全部
前端部分
<!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>
#pro {
position: relative;
margin-top: 10px;
width: 500px;
height: 10px;
background-color: #ccc;
border-radius: 10px;
overflow: hidden;
}
#pro-current {
position: absolute;
left: 0;
top: 0;
width: 0;
height: 10px;
border-radius: 10px;
background-color: #f0f;
transition: all .5s;
}
</style>
</head>
<body>
<input type="file" id="file">
<!-- 进度条 -->
<div id="pro">
<div id="pro-current">
</div>
</div>
<span id="pro-current-num"></span>
<script>
let [file, pro, num] = [document.getElementById('file'), document.getElementById('pro-current'), document.getElementById('pro-current-num')]
file.onchange = () => {
upload()
}
function upload(start = 0, index = 0, chunkSize = 1024 * 1024) {
let f = file.files[0]
if (start >= f.size) return
let end = start + chunkSize >= f.size ? f.size : start + chunkSize
// blob 和 file 相互转换
let aimfile = new File(
[f.slice(start, end)],
`${f.name.split('.')[0]}.${index}.${f.name.split('.')[1]}`, {
type: f.type,
lastModified: Date.now()
}
)
let form = new FormData()
form.append('file', aimfile)
ajax(form, (flag) => {
if (flag) {
pro.style.width = end / f.size * 500 + 'px';
num.innerText = end / f.size * 500 / 5 + '%';
upload(end, ++index)
}
}, index)
}
function ajax(file, cb, index, method = 'POST', url = 'http://localhost:5000/upload') {
let xhr = new XMLHttpRequest()
xhr.open(method, url+'?current='+index, true)
xhr.send(file)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
cb(xhr.responseText)
}
}
}
</script>
</body>
</html>
后端部分
const express = require('express')
const path = require('path')
const multer = require('multer')
const fs = require('fs')
let timer = null
let storage = multer.diskStorage({
destination: function (req, file, cb) {
// 新增目录存放文件
const chunksPath = path.join('public/upload', file.originalname.split('.')[0]);
if (!fs.existsSync(chunksPath)) {
fs.mkdirSync(chunksPath, { recursive: true }); // 新建文件夹
}
// 配置文件保存路径
cb(null, path.join('public/upload', file.originalname.split('.')[0]))
// 合并文件并删除目录
clearTimeout(timer)
timer = setTimeout(async () => {
// 合并文件(获取子文件名称)
await fs.readdir(chunksPath, (err, files) => {
if (err) throw err
files.map((res) => {
// 同步合并文件
fs.appendFileSync(
`public/upload/${file.originalname.split('.')[0]}.${file.originalname.split('.')[2]}`,
fs.readFileSync(path.join(chunksPath, res)), // 读取文件
(err) => { if (err) throw err }
)
});
})
// 删除文件
const delFile = () => {
fs.readdir(chunksPath, (err, files) => {
if (err) throw err
if (!files.length) {
delFile()
} else {
files.map(res => {
fs.unlinkSync(path.join(chunksPath, res), (e) => { if (err) throw err });
})
}
})
}
await delFile()
// 删除文件夹
const del = () => {
// 判断子文件是否为空
fs.readdir(chunksPath, (err, files) => {
if (files.length != 0) {
del()
} else {
// 为空则删除文件夹
fs.rmdir(chunksPath, (err) => { if (err) throw err })
}
})
}
await del()
}, 500)
},
filename: function (req, file, cb) {
const split = file.originalname.split('.')
cb(null, `${split[0]}-${split[1]}`) // 文件名
}
});
let upload = multer({ storage: storage });
let app = express()
// 解决跨域
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Credentials', 'true')
res.header('Access-Control-Allow-Origin', req.headers.origin)
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS')
res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Token, Accept, X-Requested-With')
next()
})
app.use('/public', express.static(path.join(__dirname, 'public')))
app.use('/upload', upload.single('file'), function (req, res, next) {
if (req.file) res.send('file ' + req.query.current + ' upload success!')
})
app.listen(5000, () => console.log('Example app listening on port 5000!'))