前言
在Web开发中,大文件上传是一个常见但颇具挑战性的需求。本文将详细介绍如何实现一个完整的大文件上传解决方案,从前端切片到后端合并,一步步解析技术原理和实现细节。
前端实现
1. 文件读取与切片
function createChunk(file, size = 5 * 1024 * 1024) {
const chunkList = []
let cur = 0
while (cur < file.size) {
chunkList.push({
file: file.slice(cur, cur + size)
})
cur += size
}
return chunkList
}
这段代码实现了文件切片功能,使用File.slice()方法将大文件切割成5MB的小块。这种分片上传的方式有三大优势:
- 避免单次上传大文件导致的超时问题
- 支持断点续传
- 提高上传速度和稳定性
2. 切片编号与FormData转换
const chunks = chunkList.map(({file}, index) => {
return {
file,
size: file.size,
chunkName: `${fileObj.name}-${index}`,
fileName: fileObj.name,
index
}
})
为每个切片添加元信息,包括文件名、切片序号等,方便后端重组。
3. 并发上传控制
const requestList = formChunks.map(({formData, index}) => {
return axios.post('http://localhost:3000/upload', formData)
})
Promise.all(requestList).then(res => {
console.log(res);
// 发送合并请求
axios.post('http://localhost:3000/merge', {
fileName: fileObj.name,
size: 5 * 1024 * 1024,
}).then(res => {
console.log(res);
})
})
使用Promise.all实现并发上传,大幅提高上传效率。
后端实现
1. 接收切片
const form = new multiparty.Form()
form.parse(req, (err, fields, files) => {
const [file] = files.file
const [fileName] = fields.fileName
const [chunkName] = fields.chunkName
const chunkDir = path.resolve(__dirname, 'qiepian', `${fileName}-chunks`)
if (!fs.existsSync(chunkDir)) {
fs.mkdirsSync(chunkDir)
}
fs.moveSync(file.path, `${chunkDir}/${chunkName}`)
})
使用multiparty解析FormData数据,保存切片到临时目录。
2. 切片合并
const mergeChunks = async (filePath, fileName, size) => {
let chunksPath = fs.readdirSync(filePath)
chunksPath.sort((a, b) => a.split('-').pop() - b.split('-').pop())
// 创建最终的合并文件的路径
const finalFilePath = path.resolve(filePath, fileName)
const arr = chunksPath.map((chunkFile, index) => {
return pipeStream(
path.resolve(filePath, chunkFile),
fs.createWriteStream(finalFilePath, {
start: index * size,
end: (index + 1) * size
})
)
})
await Promise.all(arr)
}
这段代码实现了切片合并功能,核心原理是:
- 按序号排序切片
- 使用流式写入
- 按偏移量写入正确位置
3. 流式处理
const pipeStream = (path, writeStream) => {
return new Promise((resolve, reject) => {
const readStream = fs.createReadStream(path)
readStream.pipe(writeStream)
readStream.on('end', () => { // 流式资源合并完成
// 移除切片
fs.removeSync(path)
resolve()
})
})
}
使用Node.js的流式处理,避免内存溢出问题。
进阶优化
1. 断点续传
// 伪代码
function checkUploadedChunks(fileName) {
return axios.get(`/check?fileName=${fileName}`)
}
// 上传时过滤已上传的切片
const uploadedChunks = await checkUploadedChunks(fileObj.name)
const chunksToUpload = chunks.filter(chunk =>
!uploadedChunks.includes(chunk.index)
)
2. 进度显示
// 伪代码
axios.post(url, formData, {
onUploadProgress: progressEvent => {
const percent = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
)
updateProgress(percent)
}
})
3. 秒传功能
// 伪代码
function checkFileHash(hash) {
return axios.get(`/check-hash?hash=${hash}`)
}
const fileHash = await calculateFileHash(fileObj)
if (await checkFileHash(fileHash)) {
// 文件已存在,直接返回
return
}
总结
本文详细讲解了大文件上传的实现方案,从前端切片到后端合并,涵盖了:
-
文件分片原理
-
并发上传控制
-
流式合并技术
-
进阶优化方案