跳到主要内容

大文件切片

准备

<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)
}

补充

在涉及到大文件上传的时候我们都会进行切片上传,也有很多的方式

可以选择边切边上传,也可以选择切完再上传,当然还有当我们上传一半的时候断开了,要不要重新上传,这些都可以在切片中进行处理