大文件上传
大文件上传
分片上传
web worker
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。这是MDN对于Web Worker的描述。简单来说就是不会阻塞页面中的UI渲染,因此非常适合用来处理耗时计算操作,即文件的hash计算。
文件分片

如图所示,File对象原型为Blob,具备slice的分片方法,因此可以利用该方法实现文件的分片。
const uploadBtn = document.getElementById('uploadBtn')
const fileInput = document.getElementById('file')
// 切片大小10MB
const chunkSize = 1024 * 1024 * 10
const chunks = []
const worker = new Worker('./worker.js')
uploadBtn.addEventListener('click', () => {// 读取文件内容const file = fileInput.files[0]const { size } = file// 计算总切片数total = Math.ceil(size / chunkSize)chunks.push(...Array.from({ length: total }, (_, i) => file.slice(i * chunkSize, (i + 1) * chunkSize)))// 向 worker 线程发送消息worker.postMessage({chunks,filename: file.name})
})
/*** web worker 浏览器的多线程脚本* 是运行在后台的js 不会阻塞页面* 可以进行计算 可以进行io操作 不能操作DOM* 不能访问window alert document location等浏览器对象* self代表worker全局对象* spark-md5.js用于计算文件的唯一hash值*/
self.importScripts('./utils/spark-md5.js')
self.addEventListener('message', e => {const { chunks, filename } = e.data// 计算md5const spark = new self.SparkMD5.ArrayBuffer()let current = 0function loadNext() {const reader = new FileReader()reader.onload = e => {spark.append(e.target.result)current++if (current < chunks.length) {loadNext()} else {// 全部读取完毕 发送结果给主线程self.postMessage({filename,hash: spark.end()})}}reader.readAsArrayBuffer(chunks[current])}loadNext()
})
文件上传
/*** 处理文件上传存放的位置* 处理文件名字*/
const storage = multer.diskStorage({destination: (req, file, cb) => {fs.mkdirSync(`uploads/${req.body.hash}`, { recursive: true }) // 确保上传目录存在cb(null, `uploads/${req.body.hash}/`)},filename: (req, file, cb) => {cb(null, `${req.body.filename}-${req.body.index}`) // 使用hash值和切片下标作为文件名}
})const upload = multer({ storage })/*** 文件上传接口* file字段名需和前端保持一致*/
app.post('/upload', upload.single('file'), (req, res) => {res.json({ message: 'File uploaded successfully', success: true })
})
分片的合并
/*** 合并切片(通过文件流合并)*/
app.get('/merge', async (req, res) => {const { filename, hash } = req.query// 获取切片所在目录const files = fs.readdirSync(`uploads/${hash}`)// 根据index排序const fileArraySort = files.sort((a, b) => {const indexA = parseInt(a.split('-').pop())const indexB = parseInt(b.split('-').pop())return indexA - indexB})// 创建存放最终合并的文件 目录const filePath = path.join(__dirname, hash)fs.mkdirSync(filePath, { recursive: true })// 创建写入流const writeStream = fs.createWriteStream(path.join(filePath, filename))for (const file of fileArraySort) {await new Promise((resolve, reject) => {// 创建读取流(读取每一个切片)const readStream = fs.createReadStream(path.join(__dirname, 'uploads', hash, file))readStream.pipe(writeStream, { end: false }) // 管道流写入但不关闭写入流readStream.on('end', () => {fs.unlinkSync(path.join(__dirname, 'uploads', hash, file)) // 删除切片resolve()})readStream.on('error', reject)})}// 手动关闭writeStream.end()res.json({success: true})
})
秒传
秒传表示上传过的文件不会再次上传,因此只需验证某个hash值的目录是否存在合并后的文件即可。
app.get('/verify', (req, res) => {const { hash, filename } = req.query// 判断是否秒传const filePath = path.join(__dirname, hash, filename)if (fs.existsSync(filePath)) {return res.json({ success: true, fileExist: true })}return res.json({ success: true, fileExist: false })
})
断点续传
断点续传表示当网络中断时延续过去的进度继续上传。
app.get('/verify', (req, res) => {const { hash, filename } = req.query// 判断是否秒传const filePath = path.join(__dirname, hash, filename)if (fs.existsSync(filePath)) {return res.json({ success: true, fileExist: true })}// 断点续传(返回已上传的文件列表)const isExist = fs.existsSync(path.join(__dirname, 'uploads', hash))if (!isExist) {return res.json({ success: true, files: [] })}const files = fs.readdirSync(path.join(__dirname, 'uploads', hash))res.json({ success: true, files })
})
🎇秒传与断点续传都需要在文件上传前调用
const res = await fetch(`http://localhost:3000/verify?hash=${hash}&filename=${filename}`)const { files, fileExist } = await res.json()if (fileExist) {return}const uploadedSet = new Set(files)const tasks = chunks.map((chunk, index) => ({ chunk, index })).filter(({ index }) => !uploadedSet.has(`${filename}-${index}`))// 文件上传
