md5 文件上传
当用户在操作文件上传功能时,某些文件特别大,比如:100M,1G ?G 。网速慢,浏览器卡顿,可使用文件切片方式上传。
html 页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>md5 文件上传</title>
<style>
.loading {
300px;
border: 1px solid #333;
height: 20px;
}
.loading div {
0%;
background: #F00;
height: 20px;
}
</style>
</head>
<body>
<input type="file">
<div id="loading" class="loading"><div></div></div>
<!-- md5 文件 网上可下载 -->
<script src="./md5.js"></script>
<script>
const fileEl = document.querySelector('input')
const loading = document.querySelector('#loading > div')
const chunkSize = 1024 * 1024* 2;
fileEl.onchange = async (e) => {
const ele = e.target;
const file = ele.files[0];
// 文件分片
const chunks = createFileChunk(file, chunkSize)
// 判断文件是否重复
const hash = await calculateHashSample(file);
// 1、文件上传
await calculateHashWorker(chunks)
// 2、利用浏览器空闲时间上传
// await calculateHashIdle(chunks)
const data = chunks.map((chunk, index)=> {
return {
hash,
index,
chunk: chunk.file,
progress: 0
}
})
console.log(data)
}
</script>
</body>
</html>
文件切片
function createFileChunk(file, size) {
const chunks = [];
let cur = 0;
while (cur < file.size) {
chunks.push({
index: cur,
file: file.slice(cur, cur + size) // 文件切割
})
cur += size;
}
// 返回切分完成后的文件
return chunks;
}
文件上传
FileReader()
对象允许Web应用程序异步读取存储在用户计算机上的文件或原始数据缓冲区)的内容,使用 File
或 Blob
对象指定要读取的文件或数据。
async function calculateHashWorker(chunks) {
return new Promise(reslove => {
let count = 0;
const spark = new SparkMD5.ArrayBuffer();
let progress = 0;
const loadNext = index => {
const reader = new FileReader();
reader.onload = e => {
count++;
spark.append(e.target.result)
if(count === chunks.length) {
spark.end();
console.log('完成')
loading.style.width = (100) + '%';
reslove();
} else {
progress += 100 / chunks.length; // 进度条
console.log('上传中...',progress)
loading.style.width = (progress) + '%';
// 进行下一步上传
loadNext(count);
}
}
reader.readAsArrayBuffer(chunks[count].file)
}
loadNext(0);
})
}
利用空闲时间上传
window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。
async function calculateHashIdle(chunks) {
return new Promise(reslove => {
const spark = new SparkMD5.ArrayBuffer();
let count = 0;
const appendToSpark = async file => {
return new Promise(reslove => {
const reader = new FileReader();
reader.readAsArrayBuffer(file)
reader.onload = e => {
spark.append(e.target.result)
reslove();
}
})
}
const workLoop = async deadline => {
// 判断当前文件片段是否上传完成 && 当前剩余时间大于 1
// deadline.timeRemaining() 获取当前帧的剩余时间
while (count < chunks.length && deadline.timeRemaining() > 1) {
await appendToSpark(chunks[count].file)
count++;
if(count < chunks.length) {
// 加载动画
console.log('加载动画...',((100 * count) / chunks.length).toFixed(2) + '%');
loading.style.width = ((100 * count) / chunks.length).toFixed(2) + '%';
} else {
console.log('加载完毕')
loading.style.width = '100%';
reslove(spark.end())
}
}
window.requestIdleCallback(workLoop)
}
// 浏览器一旦空闲,调用 workLoop
window.requestIdleCallback(workLoop)
})
}
判断文件是否存在
new Blob()
对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作
// 采用抽样逻辑 比如: 1G 的文件,抽样后5M以内
// 两个文件hash 一样,可能文件不一样,hash 不一样,文件一定不一样。
async function calculateHashSample(file) {
// 抽样
return new Promise(reslove => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
const size = file.size;
let offset = 1024 * 1024* 2;
let chunks = [file.slice(0, offset)];//第一个2M,最后一个区块数据全要
let cur = offset;
while(cur < size) {
if(cur + offset >= size) {
// 最后一个区块
chunks.push(file.slice(cur, cur+offset))
} else {
// 中间区块
const mid = cur + offset / 2;
const end = cur + offset;
chunks.push(file.slice(cur, cur + 2)) // 起始位置,取两个字节
chunks.push(file.slice(mid, mid + 2)) // 中间位置,取两个字节
chunks.push(file.slice(end - 2, end)) // 最后位置,取两个字节
}
cur += offset;
}
reader.onload = e => {
spark.append(e.target.result)
console.log('完成')
reslove(spark.end());
}
reader.readAsArrayBuffer(new Blob(chunks))
})
}
注意:坏处:抽样逻辑可能会出现误判概率低,,好处:可以提高效率