大文件分片上传-设计与实现

需求分析

当要实现大文件分片上传,不如先理清流程,先设计出主要步骤出来,再在步骤中不断细化和处理边缘条件。

  1. 首先要实现文件分片,就要先有文件,第一步就是获取文件。
  2. 在分片之前,还得提前得到分片信息(分片数量、分片大小等)。
  3. 在分片信息的条件约束下,将文件进行分片。
  4. 将分片上传服务端。

步骤设计

1. 获取文件

利用 <input type="file"> 标签的选择事件就可以获取到获取到选择的文件

2. 分片信息的获取

其中分片大小可以定义一个全局变量进行存储,以便后续更改大小时便利。1024 * 1024 * 5 表示5MB

分片数量,用文件大小除以分片大小便时分片数量,

线程数量和每个线程多少分片,这里可以考虑到分片时会有MD5编码,是同步完成编码,是个耗时操作,会阻塞主线程,为了避免阻塞主线程,这里用 webworker 创建辅助线程,线程数量可以定死,但是更好的做法是,获取当前机器可用于运行线程的逻辑处理器数量:navigator.hardwareConcurrency 这个变量也应该存在全局下,而每个线程多少分片取决于有多少辅助线程。

分片顺序索引,当把分片交给不同辅助线程时,处理结果并不清楚,为了使回传分片顺序正确,这里还需要计算每个线程开始索引和结束索引,结束索引这时候由于之前向上取整可能会大于分片数量,这时候需要将结束索引进行矫正。

线程完成数,为了验证分片完成,还需要统计线程完成分片的数量,当这个值与线程数量相同时表示完成了分片,当一个线程完成时可以将该线程进行清除。

注:这里为了使文件完整分片,分片数量和每个线程多少分片都应该向上取整来保证文件分片完整

3. 分片

利用分片大小和顺序索引计算出分片开始字节和结束字节,利用 FileReader对象读取对应文件内容,这里需要注意的时第一步获取到的file是文件信息,并不是文件内容,用readAsArrayBuffer()方法将分片文件读成blob对象。

这里需要考虑文件上传是需要一个唯一信息,防止篡改以及秒传的功能,文件名显然不合适,这里采用文件hash的方式进行,使用第三方库 SparkMD5 加密的方式生成文件hash,在这过程中如果直接对整个文件每个分片进行hash的话,这就需要读取整个文件,整个文件放入内存是很占内存的。这里优化,将文件hash放在每个线程处理某段分片的时候进行,当分片完成时,也就完成文件hashSparkMD5也提供了一个append()方法用于不断的添加加密的文件,加密完就把当前文件从内存剔除,最后用end()方法结束加密。

4. 上传服务器

最终分片数据得到的是一个blob数组,对这个数组进行上传,当中途遇到网络问题等导致上传失败时,再次上传文件,可以在失败的地方接着上传,这也就是断点续传。

这里后端应该有三个接口

  • 检查文件的接口(文件hash
    • 文件上传过,并上传完成,可以直接完成请求===>文件秒传
    • 文件上传过,但是不完全,可以返回需要续传,从第几个分片开始 ===>断点续传
    • 文件未上传
  • 上传接口 :
  • 合并分片接口 提供hash :告知成功

具体实现

这里分成了 4个功能函数:main.js、cutFile.js、worker.js、createChunk.js

  • main.js

main 主函数
用来获取用户选择的文件
并且调用cutFile函数进行文件分片

import {cutFile} from "./cutFile.js";

const inpFile = document.querySelector('input[type="File"]')
inpFile.onchange = async (e)=>{
    const file = e.target.files[0]
    const chunks = await cutFile(file)
    console.log(chunks)
}
  • cutFile.js

cutFile 计算、创建线程函数

  1. 计算分片数量
  2. 计算每个线程分片数量
  3. 创建线程
  4. 传输消息
  5. 消息回传
  6. 清除线程
  7. 保证回传分片顺序正确
  8. 结束
const CHUNK_SIZE = 1024 * 1024 * 5 //分片大小 5MB
const THREAD_COUNT = navigator.hardwareConcurrency || 4 // 获取机器CPU数量线程
export async function cutFile(file) {
    return new Promise((resolve)=>{
        const result = [] // 结果
        let finishCount = 0 // 线程完成数量
        const chunkCount = Math.ceil(file.size /CHUNK_SIZE)// 计算多少个分片
        const workerChunkCount = Math.ceil(chunkCount /THREAD_COUNT)// 每个线程多少个分片
        for (let i = 0;i<THREAD_COUNT;i++){
            // 创建新的Worker线程
            const worker = new Worker('./worker.js',{
                type:"module"
            })
            // 计算每个线程开始索引和结束索引,用来使回传分片顺序正确
            const startIndex = i * workerChunkCount // 开始索引
            let endIndex = startIndex + workerChunkCount// 结束索引
            if (endIndex > chunkCount){// 当大于分片数量时,最后一个线程的结束索引可能大于总分片数量
                endIndex = chunkCount
            }
            worker.postMessage({// 传输消息
                file,
                CHUNK_SIZE,
                startIndex,
                endIndex
            })
            worker.onmessage = (e) =>{//消息回传
                for (let i = startIndex;i<endIndex;i++){// 保证回传分片顺序正确
                    result[i] = e.data[i-startIndex]
                }
                worker.terminate() // 清除线程
                finishCount++
                if (finishCount === THREAD_COUNT){
                    resolve(result.flat())
                }
                }
            }
        }
    })
}
  • worker.js

worker - 线程函数

import {createChunk} from "./createChunk.js";

onmessage = async (e)=>{
    const proms = []
    const{file, CHUNK_SIZE, startIndex, endIndex} = e.data
    for (let i =startIndex;i<endIndex;i++){
        proms.push(createChunk(file,i,CHUNK_SIZE))
    }
    const chunks = await Promise.all(proms)// 并行执行Promise
    postMessage(chunks)
}
  • createChunk.js

createChunk - 文件切片函数

import SparkMD5 from './spark-md5.js'
export function createChunk(file, index, chunkSize) {
    return new Promise((resolve)=>{
        const start = index * chunkSize// 开始字节
        const end = start + chunkSize// 结束字节
        const spark = new SparkMD5.ArrayBuffer()// MD5
        const fileReader = new FileReader() // 文件读取器
        fileReader.onload = (e) =>{
            spark.append(e.target.result)// MD5编码
            resolve({
                start,
                end,
                index,
                hash:spark.end()
            })
        }
        // 转换成二进制数据
        fileReader.readAsArrayBuffer(file.slice(start,end))
    })
}

在这里插入图片描述

  • 17
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天将降大任于我

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值