如果没有看过分析篇的铁铁可以先去看看分析篇: 功能分析篇
Chunk
Chunk类用于构造一个文件分片对象,通过这个类我们能获取当前分片的hash,lob对象,大小以及当前分片是处于整个文件中的第几个等属性
核心方法
- chunkHash
chunkHash属性用于获取当前分片的hash
相关实现
class Chunk {
constructor(chunk, index) {
this.chunk = chunk;
this.index = index;
this.hash = "";
this.size = this.chunk.size;
this.reader = null;
}
get chunkHash() {
return hash
}
}
计算文件hash
文件hash是一个CPU密集型任务,所以只会在任务被启动时才进行计算,确保主线程不会被阻塞,,因为计算hash涉及了IO操作,所以是异步的,为了防止在读取文件的过程中又一次的对reader进行重新赋值,我们可以对reader进行一个缓存处理,如果后续还需要hash,则可以将之前已经计算好的hash直接返回,而不需要重新计算,具体实现如下
get chunkHash() {
return new Promise((resolve, reject) => {
if (this.hash) resolve(this.hash);
if (!this.reader) {
this.reader = new FileReader();
}
this.reader.readAsArrayBuffer(this.chunk);
this.reader.onload = () => {
if (this.reader.result) {
this.hash = SparkMD5.ArrayBuffer.hash(this.reader.result);
resolve(this.hash);
} else {
reject(new Error('Failed to read the chunk.'));
}
};
this.reader.onerror = (event) => {
reject(event.target.error);
};
this.reader.onloadend = () => {
this.reader = null;
};
});
}
Task
每一个分片都需要上传到服务器上,每一个上传都是一个任务——即Task类,我们通过Task实例来执行上传任务,包括当前分片上传进度的监控,取消当前分片的上传等功能
核心成员
- start
开始分片上传,在上传之前会先询问后端此分片是否已经上传过了如果已经上传过了则会跳过此次上传,返回一个Promise,用于TaskQueue处理任务并行调度 - changeExist
用于向后端请求是否需要发送本次分片 - stop
取消本次上传 - onUpload
监听本次分片的上传进度 - uploadCallBack
传入的监听进度的回调函数,调用时会传入当前上传的数据量,当前分片的index - path
文件上传的路径 - existPath
验证文件是否存在的路径 - finish
此任务是否已经完成
相关实现
class Task {
constructor(chunk, path, existPath) {
this.chunk = chunk;
this.size = this.chunk.size;
this.xhr = new XMLHttpRequest();
this.formData = new FormData();
this.path = path;
this.existPath = existPath;
this.finish = false;
this.hash = "";
this.uploadCallback = null;
}
start() {
return new Promise((resolve, reject) => {
this.changeExist().then(exist => {
if (exist) {
this.uploadCallback(this.size, this.chunk.index);
this.finish = true;
resolve();
} else {
this.xhr.open("POST", this.path, true);
this.formData.set("hash", this.hash);
this.formData.set("file", this.chunk.chunk);
this.xhr.onload = () => {
if (this.xhr.status === 200) {
this.finish = true;
resolve(this.xhr.response);
} else {
reject(this.xhr.response);
}
};
this.xhr.onerror = () => reject(new Error('Network error'));
this.xhr.send(this.formData);
}
}).catch(reject);
});
}
changeExist() {
return new Promise(async (resolve, reject) => {
this.hash = await this.chunk.chunkHash;
const xhr = new XMLHttpRequest();
xhr.open("GET", `${this.existPath}?hash=${encodeURIComponent(this.hash)}`, true);
xhr.onload = () => {
const response = JSON.parse(xhr.response);
if (response.code === 300) {
resolve(true);
} else {
resolve(false);
}
}
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
})
}
stop() {
this.xhr.abort();
this.formData = new FormData();
}
onUpload(cb) {
this.uploadCallback = cb;
this.xhr.upload.onprogress = (e) => {
this.uploadCallback(e.loaded, this.chunk.index)
};
}
}
TaskQueue
当多个Task实例组合到一起,就是整个文件上传的流程,但我们不能通过直接控制每个Task实例来达成整个文件的开始暂停任务,所以我们通过TaskQueue类来执行包括整个任务队列的启停,整个文件上传进度的监听等功能
核心成员
- start
按顺序开始本次文件上传,同一时间最大上传数量由maxRunning决定 - pause
暂停本次文件上传活动,将当前正在执行的任务全部暂停,重新放回队列等待调用 - setOnUpload
添加文件上传的进度监控事件 - next
每次调用next都会从任务队列中取出任务执行,并在任务执行完后自动循环调用自身 - tasks
任务队列 - runTask
当前正在执行的任务 - onFinished
文件上传结束后的回调函数数组 - status
当前文件上传的状态,分为waiting,paused,running,fulfilled四种
相关实现
class TaskQueue {
constructor(maxRunning = 1) {
this.tasks = [];
this.status = "waiting";
this.maxRunning = maxRunning;
this.running = 0;
this.runTask = [];
this.onFinished = [];
}
get taskCount() {
return this.tasks.length;
}
add(...tasks) {
this.tasks.push(...tasks)
}
start() {
if (this.status === "running") return;
this.status = "running";
while (this.running < this.maxRunning && this.taskCount > 0) {
this.next()
this.running++;
}
}
pause() {
if (["waiting", "fullfilled", "paused"].includes(this.status)) return;
this.status = "paused"
this.add(...this.runTask);
this.runTask.forEach(task => task.stop());
this.running = 0;
this.runTask = [];
}
setOnFinished(cb) {
this.onFinished.push(cb);
}
finish() {
this.pause();
this.status = "fullfilled";
this.tasks = [];
this.runTask = [];
this.running = 0;
this.onFinished.forEach(cb => cb());
}
setOnUpload(cb) {
cb()
}
next() {
if (this.taskCount <= 0) {
return;
}
const task = this.tasks.shift();
this.runTask.push(task);
task.start().then(() => {
this.runTask = this.runTask.filter(task => !task.finish);
if (this.runTask.length <= 0) {
this.status = "fullfilled";
if (this.onFinished.length > 0) {
this.onFinished.forEach(cb => cb());
}
}
this.next();
}).catch(error => console.error('Error in task:', error));
}
}
文件上传的进度监听
我们的Task本身将当前请求的相关事件暴露了出来,同时还提供了上传数据量以及是第几个分片,通过构造一个和分片数量一致的数组,每次触发这个事件时将得到的数据量写入数组对应下标中,将整个数组中的数字相加就是整个文件上传的数据量,具体实现如下
setOnUpload(cb) {
const totals = Array(this.taskCount).fill(0)
function _upload(loaded, index) {
totals[index] = loaded;
cb(totals.reduce((a, b) => a + b, 0))
}
this.tasks.forEach(task => task.onUpload(_upload))
}