前端切切切,后端拼拼拼:大文件上传的奇妙冒险 🚀

117 阅读2分钟

前言

在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的小块。这种分片上传的方式有三大优势:

  1. 避免单次上传大文件导致的超时问题
  2. 支持断点续传
  3. 提高上传速度和稳定性

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

这段代码实现了切片合并功能,核心原理是:

  1. 按序号排序切片
  2. 使用流式写入
  3. 按偏移量写入正确位置

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
}

总结

本文详细讲解了大文件上传的实现方案,从前端切片到后端合并,涵盖了:

  1. 文件分片原理

  2. 并发上传控制

  3. 流式合并技术

  4. 进阶优化方案