MD5
的核心是通过算法把任意长度的原始数据映射成128 bit 的数据。把一串数据经过处理,得到另一个固定长度的数据。是一种Hash
算法,全称为 消息摘要算法版本5(Message Digest Algorithm 5)。
不同原始数据会有不同的 MD5 值。 所以不同的文件MD5的值也不一样。
一般在上传文件的场景里,会根据MD5
实现续传、秒传的功能。
本文简单写下怎么生成 MD5,这里用到插件spark-md5
。
TL;DR
- 怕麻烦,10M以内生成MD5,直接用法一就行
- 怕麻烦,30M以内生成MD5,直接用法二就行
- 再大一点,用法三吧
- 但法三可以用在以上所有场景
法一:生成文件的 MD5
生成文件的 MD5,简单思路如下:
- 创建 FileReader 实例,读文件
- 读完之后,成功状态下,直接使用
SparkMD5.hashBinary
。
具体代码在文末。
缺陷
这种方式,当文件越大的时候,生成 md5 的速度也就越慢,比如 40M 的文件可能需要 1s 才生成 md5。当页面还有其他的交互的时候,将会堵塞其他交互,导致页面假死状态。
举个例子:加个按钮,写个点击事件。选择文件之后,立即点击按钮,会发现,当文件越大的时候,弹框的速度越来越迟钝。
<input id="upload" type="file" onchange="selectLocalFile" />
<!-- 这里加个按钮,选择文件完之后 -->
<button onclick="alert(1)">测试线程堵塞</button>
<script>
upload.onchange = async (e) => {
const file = e.target.files[0];
console.time("timeCreateMd5");
const md5 = await createFileMd5(file);
console.log(file.size);
// 会打印timeCreateMd5: 959.31396484375 ms
console.timeEnd("timeCreateMd5");
};
</script>
法二:在worker中生成md5
所以我们使用 web-worker
在 worker 线程
计算 hash
,这样用户仍可以在主界面正常的交互,不会引起堵塞。
当前页面的修改,如下:
新增在worker里执行的hash.js
文件如下:
self.importScripts("https://unpkg.com/spark-md5@3.0.1/spark-md5.min.js");
// 生成文件 hash
self.onmessage = async e => {
const file = e.data
const md5 = await createFileMd5(file)
self.postMessage(md5);
};
function createFileMd5(file){
// ...
// 同之前的,但以下需要修改,增加self的前缀
isSuccess
? resolve(self.SparkMD5.hashBinary(result))
: reject(new Error("读取出错了"));
}
到这,其实虽然生成大文件的md5耗费时间长,但起码不会堵塞页面主线程了。
也有缺陷
可以看到计算md5是将整个文件读完才看到,这样当文件过大是极其耗内存的。所以需要分片读取生成md5
。
法三:将文件分片生成md5
仔细看spark-md5,其实作者也是推荐分片读取,类似nodejs里面的流一样,这样不需要占据大量内存。
先将文件,按照一定大小分块chunk,这边直接使用File.slice
了。
然后将chunks传给另一个线程计算md5,这边大文件可能需要进度条,所以有一个进度,按需求使用。
附注:代码
代码:生成小文件的 MD5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input id="upload" type="file" onchange="selectLocalFile" />
<script src="https://unpkg.com/spark-md5@3.0.1/spark-md5.min.js"></script>
<script>
const upload = document.querySelector("#upload");
upload.onchange = async (e) => {
const file = e.target.files[0];
const md5 = await createFileMd5(file);
console.log(md5);
};
function createFileMd5(file) {
return new Promise((resolve, reject) => {
// 创建FileReader实例
const fileReader = new FileReader();
// 开始读文件
fileReader.readAsBinaryString(file);
// 文件读完之后,触发load事件
fileReader.onload = (e) => {
// e.target就是fileReader实例
console.log(e.target);
// result是fileReader读到的部分
const result = e.target.result;
// 如果读到的长度和文件长度一致,则读取成功
const isSuccess = file.size === result.length;
// 读取成功,则生成MD5,扔出去。失败就报错
isSuccess
? resolve(SparkMD5.hashBinary(result))
: reject(new Error("读取出错了"));
};
// 读取过程中出错也直接报错
fileReader.onerror = () => reject(new Error("读取出错了"));
});
}
</script>
</body>
</html>
代码:在worker中生成md5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input id="upload" type="file" onchange="selectLocalFile" />
<button onclick="alert(1)">测试线程堵塞</button>
<script>
const upload = document.querySelector("#upload");
upload.onchange = async (e) => {
const file = e.target.files[0];
console.time("timeCreateMd5");
const md5 = await createFileMd5InWorker(file);
console.log(file.size);
console.timeEnd("timeCreateMd5");
};
// 生成文件 md5(web-worker)
function createFileMd5InWorker(file) {
return new Promise((resolve) => {
// 新建worker线程,执行hash.js
const worker = new Worker("./hash.js");
// 给线程传file
worker.postMessage(file);
// 当线程传消息的时候,接受消息
worker.onmessage = (e) => {
const md5 = e.data;
md5 && resolve(md5)
};
});
}
</script>
</body>
</html>
// hash.js
self.importScripts("https://unpkg.com/spark-md5@3.0.1/spark-md5.min.js");
// 生成文件 md5
self.onmessage = async e => {
const file = e.data
const md5 = await createFileMd5(file)
self.postMessage(md5);
self.close()
};
function createFileMd5(file) {
return new Promise((resolve, reject) => {
// 创建FileReader实例
const fileReader = new FileReader();
// 开始读文件
fileReader.readAsBinaryString(file);
// 文件读完之后,触发load事件
fileReader.onload = (e) => {
// e.target就是fileReader实例,这里用this也是指fileReader实例
console.log(e.target);
// result是fileReader读到的部分
const result = e.target.result;
// 如果读到的长度和文件长度一致,则读取成功
const isSuccess = file.size === result.length;
// 读取成功,则生成MD5,扔出去。失败就报错
isSuccess
? resolve(self.SparkMD5.hashBinary(result))
: reject(new Error("读取出错了"));
};
// 读取过程中出错也直接报错
fileReader.onerror = () => reject(new Error("读取出错了"));
});
}
代码:分片读取生成md5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input id="upload" type="file" onchange="selectLocalFile" />
<button onclick="alert(1)">测试线程堵塞</button>
<script>
const upload = document.querySelector("#upload");
upload.onchange = async (e) => {
const file = e.target.files[0];
const chunks = createFileChunk(file)
console.time("timeCreateMd5");
// 这里注意放chunks
const {md5} = await createFileMd5InWorker(chunks);
console.log(file.size);
console.timeEnd("timeCreateMd5");
};
// 生成文件切片
function createFileChunk(file, size = 4 * 1024 * 1024) {
let chunks = [];
let cur = 0;
while (cur < file.size) {
chunks.push(file.slice(cur, cur + size));
cur += size;
}
return chunks;
}
// 生成文件 hash(web-worker)
function createFileMd5InWorker(fileChunks) {
return new Promise((resolve) => {
const worker = new Worker("./hash.js");
worker.postMessage({ fileChunks });
worker.onmessage = (e) => {
// 这边加了进度条 这里的进度条,看需要显示
const { percentage, hash } = e.data;
console.log(percentage)
// 计算出hash之后,扔出去
hash &&resolve(hash);
};
});
}
</script>
</body>
</html>
// 直接copy的 https://juejin.cn/post/6844904046436843527#heading-17
self.importScripts("./js/lib/spark-md5.min.js"); // 导入脚本
// 生成文件 hash
self.onmessage = e => {
const { fileChunks } = e.data;
console.log(fileChunks)
// const { fileChunks } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunks[index]);
reader.onload = e => {
count++;
spark.append(e.target.result);
if (count === fileChunks.length) {
self.postMessage({
percentage: 100,
hash: spark.end()
});
self.close();
} else {
percentage += 100 / fileChunks.length;
self.postMessage({
percentage
});
loadNext(count);
}
};
};
loadNext(0);
};