大文件切片
准备
<input type="file">
const inp = document.querySelector('input')
inp.addEventListener("change", (event) => {
const inputElement = event.target
const { files } = inputElement
if( files && files[0] ){
const file = files[0]
console.log(file)
}
});
或者我们想给一个button来设置点击这个选择文件
const inp = document.createElement('input');
inp.type = 'file';
inp.addEventListener("change", (event) => {
const inputElement = event.target
const { files } = inputElement
if( files && files[0] ){
const file = files[0]
console.log(file)
}
});
inp.click(); // 执行点击事件,具体的需要看你的项目需求
下面的具体以input文件选择为例,用原生js来实现大文件切片
切片
初始
import { cutFile } from './cutFile.js'
const inp = document.querySelector('input')
inp.addEventListener("change", async (event) => {
const inputElement = event.target
const { files } = inputElement
if( files && files[0] ){
const file = files[0]
console.log(file)
console.time('cutFile')
const chunks = await cutFile(file)
console.timeEnd('cutFile')
console.log(chunks)
}
});
第一步:返回切片结果
这个时候我们并不关心,我们切片的过程是怎么样的,我们先将切片的结果返回回去
// 调用 createChunk 方法
// 定义切片大小
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
export const cutFile = async (file) => {
// 获取到我们切片的数量:向上取整
const chunkCount = Math.ceil(file.size / CHUNK_SIZE)
const arr = []
// 开始分片
for (let i = 0; i < chunkCount; i++) {
const chunk = await createChunk(file, i, CHUNK_SIZE)
arr.push(chunk)
}
// 返回
return arr
}
第二步:切片的方法
// 切片方法:
import SparkMD5 from "spark-md5";
export const createChunk = async (file, chunkIndex, chunkSize) => {
return new Promise((resolve, reject) => {
const start = chunkIndex * chunkSize
const end = start + chunkSize >= file.size ? file.size : start + chunkSize
// MD5加密
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
const blob = file.slice(start, end)
// 以 ArrayBuffer 的形式读取文件。参数是一个表示文件的二进制数据块(Blob)。
// 先执行 readAsArrayBuffer 再执行onload
fileReader.readAsArrayBuffer(blob)
fileReader.onload = (e) => {
spark.append(e.target.result)
resolve({
start, end, index: chunkIndex, hash: spark.end(), blob
})
}
})
}
第三步:思考优化
// 切片方法:
import SparkMD5 from "spark-md5";
export const createChunk = async (file, chunkIndex, chunkSize) => {
return new Promise((resolve, reject) => {
const start = chunkIndex * chunkSize
const end = start + chunkSize >= file.size ? file.size : start + chunkSize
// MD5加密
const spark = new SparkMD5.ArrayBuffer()
const fileReader = new FileReader()
const blob = file.slice(start, end)
fileReader.onload = (e) => {
// 这里耗时长的原因:MD5编码
// 我们是在主线程上进行这步操作的,带来的问题就是会造成我们主线程的卡顿
// 讲解方法也很简单,我们开启web worker 多线程,让耗时的操作在其他线程上进行
// 那么开启多少线程合适呢,并不是越多越好,最好是我们计算机有CPU内核数就用多少个(如何拿到?)
const a = navigator.hardwareConcurrency || 4 // 做个兼容
spark.append(e.target.result)
resolve({
start, end, index: chunkIndex, hash: spark.end(), blob
})
}
// 以 ArrayBuffer 的形式读取文件。参数是一个表示文件的二进制数据块(Blob)。
fileReader.readAsArrayBuffer(blob)
})
}
第四步:进行优化
// 定义切片大小
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB
// 开启多线程数
const THREAD_COUNT = navigator.hardwareConcurrency || 4
export const cutFile = async (file) => {
return new Promise((resolve, reject)=>{
// 获取到我们切片的数量:向上取整
const chunkCount = Math.ceil(file.size / CHUNK_SIZE)
// 每个线程所分到的分片数量
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT)
// 汇总结果
let arr = []
// 定义一个变量来检测所有的线程是否结束
let finishCount = 0
// 循环线程数量
for (let i = 0; i < THREAD_COUNT; i++) {
// 创建一个线程并分配任务
const worker = new Worker('./worker.js', {
type:'module', // 告诉线程是一个模块线程,在线程里我们还需要导入别的东西
})
const start = i * threadChunkCount
let end = (i + 1) * threadChunkCount
if(end > chunkCount) end = chunkCount
// 线程传递消息进行分片
worker.postMessage({
file, // 文件
CHUNK_SIZE, // 分片大小
startChunkIndex: start, // 从哪个分片开始
endChunkIndex: end, // 到哪个分片结束
})
// 等分片结束拿到我们的分片
worker.onmessage = e =>{
// 这里不建议我们直接:arr.push(...e.data),因为他造成我们切片的顺序混乱
for(let i = start; i < end; i++){
arr[i] = e.data[i - start]
}
worker.terminate() // 结束线程
finishCount++
// 当线程完成数量等于线程数量的时候说明我们切片完成,返回结果
if( finishCount === THREAD_COUNT ){
resolve(arr)
}
}
}
})
}
第五步:线程中的操作
// work.js
// 在线程中我们调用我们写好的createChunk方法
import { createChunk } from './cutFile'
onmessage = async (e) => {
const { file, CHUNK_SIZE, startChunkIndex: start, endChunkIndex: end } = e.data
// 定义promise数组
const promiseArr = []
for (let i = start; i < end; i++) {
promiseArr.push(createChunk(file, i, CHUNK_SIZE))
}
// 拿到结果
const chunks = await Promise.all(promiseArr)
// 传递回去
postMessage(chunks)
}
补充
在涉及到大文件上传的时候我们都会进行切片上传,也有很多的方式
可以选择边切边上传,也可以选择切完再上传,当然还有当我们上传一半的时候断开了,要不要重新上传,这些都可以在切片中进行处理