概述
大文件上传通常采用分片上传。如果因为某些原因上传突然中断,解决问题之后可以接着之前的分片上传,而不需要从头开始上传,也就是断点续传。此外还可以利用多个网络连接并行上传多个分片,提高上传速度。
注:前端不能使用 live-server 去启动, live-server 启动会在上传文件时自动刷新页面,阻止默认事件。所以使用
npm i http-server
去启动http-server -p 9999
这里用了 9999 端口。
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input id="file" type="file">
<script>
const file = document.querySelector('#file')
file.addEventListener('change', e => {
let file = e.target.files[0] // file 对象
console.log(file)
})
</script>
</body>
</html>
此时上传一个 map4 文件。
分片上传。
完整代码
前端
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input id="file" type="file">
<script>
const file = document.querySelector('#file')
// 2 MB 的文件 我这里 以 1MB 为一个切片
const chunksFn = (file, size = 1024 * 1024) => {
const chunks = []
for (let i = 0; i < file.size; i += size) {
chunks.push(file.slice(i, i + size))
}
return chunks
}
const uploadFiles = (chunks) => {
// 批量上传 Promise.all
const list = []
for (let i = 0; i < chunks.length; i++) {
const formData = new FormData()
formData.append('index', i) // 标识
formData.append('filename', 'videoTest')
formData.append('file', chunks[i]) // 必须写最后,因为multer读到file,之后的index,filename等(自定义的)就不会处理了
list.push(fetch('http://localhost:3000/upload', {
method: 'POST',
body: formData
}))
}
// 等待所有请求发送完成,通知后端合并切片
Promise.all(list).then(res => {
fetch('http://localhost:3000/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
filename: 'videoTest'
})
})
console.log('上传成功')
})
}
file.addEventListener('change', e => {
// files 对象 底层继承 blob 可以调用 slice 方法进行切割
let file = e.target.files[0]
const chunks = chunksFn(file)
uploadFiles(chunks)
})
</script>
</body>
</html>
后端
import multer from 'multer'
import express from 'express'
import cors from 'cors'
import fs from 'fs'
import path from "path";
// 初始化 multer
const storage = multer.diskStorage({
destination: function (req, res, cb) {
cb(null, 'upload/') // 分片存储的目录
},
filename(req, res, cb) {
cb(null, `${req.body.index}-${req.body.filename}`)
}
})
const upload = multer({storage})
const app = express()
app.use(cors())
app.use(express.json())
app.post('/upload', upload.single('file'), (req, res) => {
res.send('ok')
})
app.post('/merge', (req, res) => {
// 读取目录下面的所有切片
const uploadDir = path.join(process.cwd(), 'upload')
const uploadArr = fs.readdirSync(uploadDir)
// 排序,再进行拼接
uploadArr.sort((a, b) => a.split('-')[0] - b.split('-')[0])
const videoDir = path.join(process.cwd(), 'video', `${req.body.filename}.mp4`)
uploadArr.forEach(item => {
fs.appendFileSync(videoDir, fs.readFileSync(path.join(uploadDir, item)))
// 分片合并后就可以清除了
fs.unlinkSync(path.join(uploadDir, item))
})
res.send('ok')
})
app.listen(3000, (req, res) => {
console.log('3000端口已启动')
})