生成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-worker
在 worker
线程计算 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()
}
})
}
流程图
hash值+索引号命名切片
在切片上传uploadChunks方法中调用生成文件hash代码得到hash值,将hash值+索引号作为切片名字上传.
并发控制切片上传
并发控制具体实现我们在"面试官:为什么网盘上传多个视频文件不能一起上传,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
}
总结
大文件切片生成hash时,如果文件过大,hash值计算会比较慢,还有一种方式就是计算抽样 Hash,减少计算的字节数可以大幅度减少耗时;在前文的代码中,我们是将大文件切片后,全量传入 spark-md5.min.js 中来根据文件的二进制内容计算文件的 hash 的。那么,举个例子,我们可以这样优化: 文件切片以后,取第一个和最后一个切片全部内容,其他切片的取首中尾 三个地方各2各字节来计算 hash。这样来计算文件 hash 会快很多。
本文共 659 个字数,平均阅读时长 ≈ 2分钟