大文件分片
大家好, 我今天是华为冲锋战神遥遥领先瑜, 在网上看了大半天mate60 现在耳朵里都是遥遥领先, 所以一鼓作气给大家分享一篇大文件分片+webWorker的文章, 通过写作这篇文章, 自己也学习到了不少知识, 比如通过js获取电脑cpu线程, 那么话不多说直接开始
1. 初始化, 搭设架子
js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>大文件分片</title>
<style></style>
</head>
<body>
<input type="file" id="fileRef" />
<script type="module" src="./main.js">
</script>
</body>
</html>
2. 读取单个文件的分片, 并进行加密
这里使用的是SparkMD5进行加密, 可自行下载并导入
js
import { createChunks } from './createChunks'
// 规定每次切片的文件大小
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
export async function cutFile (file) {
// 生成每一个切片, 分片是耗时的所以是异步操作
const chunk = await createChunks(file, 1, CHUNK_SIZE)
// 等待分片完成, 就可以拿到这一个分片的信息
console.log(chunk)
}
定义cutFile函数来进行文件切片, 并且返回该切片的开始结束, 第几个和hash值
js
import SparkMD5 from "./md5.js";
/**
* @param {*} file 文件
* @param {*} index 第几个分片
* @param {*} chunkSize 每一个分片的大小
*/
export function createChunks (file, index, chunkSize) {
return new Promise((resolve, reject) => {
// 开始第几个*分片的大小
const start = index * chunkSize
// 结束时start + 分片的大小
const end = start + chunkSize
const fileReader = new FileReader()
spark.append(e.target.result);
const files = file.slice(start, end);
// 读取文件的分片 读取完成后触发onload事件
fileReader.onload = e => {
console.log(e)
const spark = Md5(e.target.result)
resolve({
start,
end,
index,
hash: spark.end(),
files,
})
}
// 读取文件的分片
fileReader.readAsArrayBuffer(file.slice(start, end))
})
}
现在我们只读到了一个分片, 但是我们需要读取所有的分片, 所以需要计算目前这个文件需要分成多少个分片, 然后一个一个去读
所以可以利用文件的总大小 / 自定义定义文件切片的大小 就可以得到总切片数量
注意这里需要向上去整, 出现任何的小数点都需要包含一片内
js
export async function cutFile (file) {
// 计算文件的切片数量
const chunks = Math.ceil(file.size / CHUNK_SIZE)
console.log(chunks, 'chunks')
// 生成每一个切片, 分片是耗时的所以是异步操作
const chunk = await createChunks(file, 1, CHUNK_SIZE)
// 等待分片完成, 就可以拿到这一个分片的信息
console.log(chunk)
}
3. 将文件进行全部切片
已经拿到了文件切片的总数量, 这里就可以循环, 并存储到一个数组中即可
js
export async function cutFile (file) {
const result = []
// 计算文件的切片数量
const chunks = Math.ceil(file.size / CHUNK_SIZE)
// 生成每一个切片, 分片是耗时的所以是异步操作
for (let i = 0; i < chunks; i++) {
const chunk = await createChunks(file, i, CHUNK_SIZE)
// 等待分片完成, 就可以拿到这一个分片的信息
result.push(chunk)
}
return result
}
此时我们根据入口函数来打印看一下结果
文件被切割成了103分并且都保存在了一个数组中, 消耗了2.3秒
4. 分析如何对分片进行优化
这里我上传的文件是500m大小, 但是我上传是好几个g 一定会更加的延迟, 造成线程长时间的阻塞, 原因是这里使用到了MD5, 进行了大量的计算
所以如何避免线程的阻塞就是优化的关键, 在js中有Web Worker
mdn的概念说: 使得在一个独立于 Web 应用程序主执行线程的后台线程中运行脚本操作成为可能。这样做的好处是可以在独立线程中执行费时的处理任务,使主线程(通常是 UI 线程)的运行不会被阻塞/放慢。
关于webWoker的使用, 推荐大家看一下BEFE团队 的一文彻底学会使用web worker
5. 使用webWoker进行优化
1. 搭设架子
首先需要定义开始线程的数量, 并且按照数量去开启新的线程, 并搭设架子
至于要向worker发送什么消息, 以及接收返回的值, 我们稍后分析
js
// 定义线程数量
const THREAD_COUNT = 4 // 4个线程
export async function cutFile (file) {
const result = []
// 计算文件的切片数量
const chunks = Math.ceil(file.size / CHUNK_SIZE)
// 生成每一个切片, 分片是耗时的所以是异步操作
// for (let i = 0; i < chunks; i++) {
// const chunk = await createChunks(file, 1, CHUNK_SIZE)
// // 等待分片完成, 就可以拿到这一个分片的信息
// result.push(chunk)
// }
// 创建新的线程
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker('./worker.js', { type: 'module' })
worker.postMessage(???)// 向 worker 线程发送消息
worker.onmessage = e => {
// 接收到 worker 线程返回的消息
console.log(e)
}
}
return result
}
2. 计算每一个线程需要处理的切片数量
- 首先必须是文件file, 需要处理的文件
- 其次是文件的每一个尺寸, 就是定义好的CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
- 要处理的分片区间(也就是开始下标和结束的下标) 例如 103个文件 , 这里一共开启了4个线程, 那么就需要这四个线程分别去处理103个分片文件
以上三个条件已知的是file文件以及每一个文件的chunksize, 只有第三个是不知道的, 也就是不知道开始和结束值的分片期间
首先需要计算每一个线程需要处理的切片数量
js
const workerChunkCount = Math.ceil(切片数量 / 定义线程数量)
开启线程, 并计算每个线程的开始索引和结束索引, 这里需要在遍历中通过i下标进行计算
js
// 这里的worker稍后解释如何定义, 注意这里要写type: module 因为引入了其他模块需要使用
const worker = new Worker('./worker.js', { type: 'module' })
// 计算每个线程的开始索引和结束索引
const startIndex = i * workerChunkCount
let endIndex = startIndex + workerChunkCount
// 防止最后一个线程结束索引大于文件的切片数量的总数量
if (endIndex > chunks) {
endIndex = chunks
}
最后进行发送
js
// 向 worker 线程发送消息
worker.postMessage({
file, // 文件
CHUNK_SIZE, // 文件大小
startIndex, // 开始下标
endIndex // 结束下标
})
完整代码
js
// 规定每次切片的文件大小
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
// 定义线程数量
const THREAD_COUNT = 4 // 4个线程
export async function cutFile (file) {
const result = []
// 计算文件的切片数量
const chunks = Math.ceil(file.size / CHUNK_SIZE)
// 计算每一个线程需要处理的切片数量
const workerChunkCount = Math.ceil(chunks / THREAD_COUNT)
// 生成每一个切片, 分片是耗时的所以是异步操作
// for (let i = 0; i < chunks; i++) {
// const chunk = await createChunks(file, 1, CHUNK_SIZE)
// // 等待分片完成, 就可以拿到这一个分片的信息
// result.push(chunk)
// }
// 创建新的线程
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker('./worker.js', { type: 'module' })
// 计算每个线程的开始索引和结束索引
const startIndex = i * workerChunkCount
let endIndex = startIndex + workerChunkCount
// 防止最后一个线程结束索引大于文件的切片数量的总数量
if (endIndex > chunks) {
endIndex = chunks
}
// 向 worker 线程发送消息
worker.postMessage({
file, // 文件
CHUNK_SIZE, // 文件大小
startIndex, // 开始下标
endIndex // 结束下标
})
worker.onmessage = e => {
// 接收到 worker 线程返回的消息
console.log(e)
}
}
return result
}
3. 接收到 worker 线程返回的消息
- 接收worker返回的信息, 我们需要将信息按照顺序添加到result数组中,
- 并且每次接收到一个就需要关闭掉一个线程
- 如果线程全部完成了, 需要将result数组进行抛出
js
let finishCount = 0 // 记录线程开启的次数
worker.onmessage = (e) => {
// 接收到 worker 线程返回的消息
for (let i = startIndex; i < endIndex; i++) {
result[i] = e.data[i - startIndex];
}
worker.terminate();
finishCount++;
// 如果记录的开启线程 = 定义的开启线程次数
if (finishCount === THREAD_COUNT) {
// 通知主线程, 并返回结果
resolve(result);
}
};
这里使用resolve 因为需要等待, 所以需要是异步操作
完整代码
js
export const cutFile = async (file) => {
return new Promise((resolve, reject) => {
// 规定每次切片的文件大小
const CHUNK_SIZE = 1024 * 1024 * 5; // 5MBå
// 定义线程数量
const THREAD_COUNT = 4; // 4个线程
let result = [];
// 计算文件的切片数量
const chunks = Math.ceil(file.size / CHUNK_SIZE);
// 计算每一个线程需要处理的切片数量
const workerChunkCount = Math.ceil(chunks / THREAD_COUNT);
let finishCount = 0; // 完成的线程数量
// 生成每一个切片, 分片是耗时的所以是异步操作
// for (let i = 0; i < chunks; i++) {
// const chunk = await createChunks(file, i, CHUNK_SIZE);
// // 等待分片完成, 就可以拿到这一个分片的信息
// result.push(chunk);
// }
// return result;
// 创建新的线程
for (let i = 0; i < THREAD_COUNT; i++) {
const worker = new Worker("./worker.js", {
type: "module",
});
// const worker = new Worker(worderPath)
// 计算每个线程的开始索引和结束索引
const startIndex = i * workerChunkCount;
let endIndex = startIndex + workerChunkCount;
// 防止最后一个线程结束索引大于文件的切片数量的总数量
if (endIndex > chunks) {
endIndex = chunks;
}
worker.postMessage({
file,
CHUNK_SIZE,
startIndex,
endIndex,
});
worker.onmessage = (e) => {
// 接收到 worker 线程返回的消息
for (let i = startIndex; i < endIndex; i++) {
result[i] = e.data[i - startIndex];
}
worker.terminate();
finishCount++;
if (finishCount === THREAD_COUNT) {
// 所有线程都完成了
// 通知主线程
// console.log(result);
resolve(result);
}
};
}
});
};
4. worker文件
因为这里开启了worker线程, 所以需要执行./worker的逻辑
js
const worker = new Worker("./worker.js", {
type: "module",
});
很显然 这里就是将file,CHUNK_SIZE,startIndex,endIndex 这几个有用的数据拿出来, 并且执行读取单个分片的方法 createChunks 这里之前已经定义好了, 所以直接调用
js
// 之前 添加worker的信息
worker.postMessage({
file,
CHUNK_SIZE,
startIndex,
endIndex,
});
- 那么, 需要调用几次createChunks 方法呢? 可以通过for循环startIndex,endIndex 来操作
- 并且createChunks 中读取文件是需要异步操作的, 我们希望同时全部读取完毕后在返回, 不然就是上一个读取完, 才会读取下一个, 浪费了时间
按照以上这两点思路, 就可以写逻辑了
js
import { createChunks } from "./createChunks.js";
onmessage = async (e) => {
const arr = [];
const { file, CHUNK_SIZE, startIndex, endIndex } = e.data;
// console.log(file, CHUNK_SIZE, startIndex, endIndex);
for (let i = startIndex; i < endIndex; i++) {
arr.push(createChunks(file, i, CHUNK_SIZE));
}
// Promise.all=> 同时进行异步操作
const chunks = await Promise.all(arr);
// 提交线程信息
postMessage(chunks);
};
看一下结果吧
很明显, 从之前的2秒多 多现在的0.2米, 速度快了10倍.
6. Js 获取电脑cpu的线程
突发奇想, js能不能获取电脑cpu的最大线程数量呢, 这样就可以每次追求最快的速度, 现在已经是晚上1点多了, 偷懒直接问了chat, 哈哈 搜了一下还真有
这里果断再优化一下, 最求卓越
js
// 获取核心线程的数量
const THREAD_COUNT = navigator.hardwareConcurrency || 4;
console.log(navigator.hardwareConcurrency);
果然又快了一倍多, 美滋滋~手动撒花~~✿✿ヽ(°▽°)ノ✿
这里附上git链接, 有需要的可以直接下载源码查看
彩蛋
最近华为mate60的突然发布, 是让很多人包括我在内都欣喜不已!华为伴着遥遥领先和麒麟芯片再次回来了! 近年来,华为面临了来自美国政府的技术制裁,但这并没有阻止华为继续前行,如同一叶轻舟,穿越千山万水,创造着无尽可能. 有网友拍摄深夜的华为总部, 总是灯火通明, 里面的人都是顶级的优秀人才. 不仅优秀更是勤劳付出.
年底争取给 给自己换一台华为手机. #华为 #麒麟芯片 #科技强国 🚀🇨🇳