Skip to content

大文件上传

分片上传

web worker

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

文件分片

file-prototype

如图所示,File对象原型为Blob,具备slice的分片方法,因此可以利用该方法实现文件的分片。

js
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
	})
})
js
/**
 * 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
	// 计算md5
	const spark = new self.SparkMD5.ArrayBuffer()
	let current = 0
	function 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()
})

文件上传

js
/**
 * 处理文件上传存放的位置
 * 处理文件名字
 */
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 })
})

分片的合并

js
/**
 * 合并切片(通过文件流合并)
 */
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值的目录是否存在合并后的文件即可。

js
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 })
})

断点续传

断点续传表示当网络中断时延续过去的进度继续上传。

js
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 })
})

🎇秒传与断点续传都需要在文件上传前调用

js
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}`))
    // 文件上传
最近更新