老K博客 - 一个源码和技术分享的博客

大文件切片上传优化,子线程计算文件hash,pLimit库并发控制上传

老K博客
2024-05-31 / 0 评论 / 22 阅读 / 正在检测是否收录...
广告

效果演示:
16659ad2a57f00.gif

生成hash

无论是客户端还是服务端,都要用到文件和切片的 hash,生成 hash 最简单的方法是 文件名 + 切片下标,但是如果文件名一旦修改,生成的 hash 就会失效。事实上只要文件内容不变, hash 就不应该变化,所以我们根据文件内容生成 hash。

这里我们选用 spark-md5库,它可以根据文件内容计算出文件的hash值。

imort SparkMD5 from 'spark-md5.min.js'
/**
 * 生成文件hash
 */
const chunkHash = async () => {
    message.innerText = "生成hash开始" // 生成文件hash开始
    const hash = await getFileHash(chunksList)// 生成文件hash
    message.innerText = hash // 显示hash值
    return hash
}

/**
 *
 * 获取全部文件内容hash
 * @param {any} fileList
 */
 async function getFileHash(fileList) {
    const spark = new SparkMD5.ArrayBuffer()
    const result = fileList.map((item, key) => getFileContent(item))
    try {
        const contentList = await Promise.all(result)
        for (let i = 0; i < contentList.length; i++) {
            spark.append(contentList[i])
        }

        return spark.end() // 返回hash总值
    } catch (e) {
        console.log(e)
    }
    }

/**
 *
 * 获取全部文件内容
 * @param {any} file:Blob
 * @returns
 */
function getFileContent(file) {
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader()
        //读取文件内容
        fileReader.readAsArrayBuffer(file)
        fileReader.onload = e => {
            resolve(e.target.result)
        }
        fileReader.onerror = e => {
            reject(fileReader.error)
            fileReader.abort()
        }
    })
 }

如果上传的文件过大时,读取文件内容计算hash非常耗时,并且会引起 UI 阻塞,导致页面假死,所以我们使用 web-workerworker 线程计算 hash,这样仍可以在主界面正常做交互。( web-worker 使用方式不清楚的参考MDN介绍)具体做法如下:

/**
 * 生成hash
 */
const calculateHash = (fileList) => {
    message.innerText = "计算hash...";
    return new Promise((resolve, reject) => {
        window.w = new Worker('../js/setMd5.js')
        // 接收子线程内容
        window.w.onmessage = ev => {
            message.innerText = "";
            resolve(ev.data)
            w.terminate() // 停止子线程
        }
        // 发生错误,终止子线程
        window.w.onerror = err => {
            w.terminate()
            reject(error)
            console.log(error.filename, error.lineno, error.message) // 发生错误的文件名、行号、错误内容
        }
        // 发送信息
        window.w.postMessage(fileList)
    })
}

setMd5.js文件:

// 引入spaprk-md5库
self.importScripts('./spark-md5.min.js')

//接受主进程发送过来的数据
self.onmessage = function (e) {
    const fileChunkList = e.data

    getFileHash(fileChunkList)
        .then(hash => {
            self.postMessage({
                hash: hash,
            })
        })
        .catch(() => {
            self.postMessage({
                error: 'crate hash error',
            })
        })
}

/**
 *
 * 获取全部文件内容hash
 * @param {any} fileList
 */
async function getFileHash(fileList) {
    const spark = new SparkMD5.ArrayBuffer()
    const result = fileList.map((item, key) => getFileContent(item))

    try {
        const contentList = await Promise.all(result)
        for (let i = 0; i < contentList.length; i++) {
            spark.append(contentList[i])
        }
        return spark.end()
    } catch (e) {
        console.log(e)
    }
}

/**
 *
 * 获取全部文件内容
 * @param {any} file
 * @returns
 */
function getFileContent(file) {
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader()
        //读取文件内容
        fileReader.readAsArrayBuffer(file)
        fileReader.onload = e => {
            resolve(e.target.result)
        }
        fileReader.onerror = e => {
            reject(fileReader.error)
            fileReader.abort()
        }
    })
}

流程图

16659ae3e7f674.png

hash值+索引号命名切片

在切片上传uploadChunks方法中调用生成文件hash代码得到hash值,将hash值+索引号作为切片名字上传.

16659ae67cbe6e.png

并发控制切片上传

并发控制具体实现我们在"面试官:为什么网盘上传多个视频文件不能一起上传,80%人回答不清楚!"一文中有详细介绍,可以通过自己封装并发控制函数实现,也可以使用pLimit库实现。

/**
 * 限制多个并发任务,只能同时执行maxCount个
 * maxCount: 最大并发数
 */
function harexsLimit(maxCount) {
    let activeCount = 0 // 激活任务数
    let waitTask = [] // 任务队列

    const execute = (asyncFn, ...args) => {
        return new Promise((resolve, reject) => {
            const task = create(asyncFn, args, resolve, reject)
            if (activeCount >= maxCount) {
                waitTask.push(task)
            } else {
                task()
            }
        })
    }
    /**
     * 创建待执行任务
     */
    const create = (asyncFn, args, resolve, reject) => {
        return () => {
            asyncFn(...args).then(resolve).catch(reject).finally(() => {
                activeCount--
                // 每执行完一个任务启动任务任务队列下个任务
                if (waitTask.length) {
                    waitTask.shift()() //执行任务
                }
            })
            activeCount++
        }
    }

    return execute
}

16659aeab0c090.png

总结

大文件切片生成hash时,如果文件过大,hash值计算会比较慢,还有一种方式就是计算抽样 Hash,减少计算的字节数可以大幅度减少耗时;在前文的代码中,我们是将大文件切片后,全量传入 spark-md5.min.js 中来根据文件的二进制内容计算文件的 hash 的。那么,举个例子,我们可以这样优化: 文件切片以后,取第一个和最后一个切片全部内容,其他切片的取首中尾 三个地方各2各字节来计算 hash。这样来计算文件 hash 会快很多。

本文共 659 个字数,平均阅读时长 ≈ 2分钟
广告
0

海报

正在生成.....

评论 (0)

语录
取消
CC BY-NC-ND