如果没有看过分析篇的铁铁可以先去看看分析篇: 功能分析篇
BigFile
一个BigFile通常会由当前文件对应的TaskQueue,由Chunk组成的数组,当前文件的hash以及对应事件的处理组成,BIgFile也是我们在整个流程唯二需要手动操作的类
核心成员
- cutFile
将获得的文件对象分割成指定大小的分片并构造Chunk对象存入chunks中 - fileExist
在获得当前文件的hash之后需要向后端询问此文件是否已经存在服务器上,如果存在则需要终止本次文件上传 - upload
开始或继续上传任务 - pause
暂停本次上传 - finish
文件分片是否上传完成 - emerge
文件是否已经合并 - fileUploadPath
文件上传路径 - fileExistPath
文件校验是否存在服务器路径 - chunks
Chunk对象数组 - tasks
该文件的TaskQueue对象
相关实现
class BigFile {
constructor({ file, maxRunning = 1, chunkSize = 1 * 1024 * 1024, fileUploadPath, fileExistPath } = opts) {
this.file = file;
this.filename = this.file.name;
this.filesize = this.file.size;
this.tasks = new TaskQueue(maxRunning);
this.chunks = [];
this.chunkSize = chunkSize;
this.chunksCount = Math.ceil(this.filesize / this.chunkSize);
this.fileHash = "";
this.finish = false;
this.emerge = false;
this.fileDOM = new FileDOM(this.filename);
this.fileUploadPath = fileUploadPath;
this.fileExistPath = fileExistPath;
this.cutFile();
}
cutFile() {
for (let i = 0; i < this.chunksCount; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, this.filesize);
const chunk = this.file.slice(start, end);
this.chunks.push(new Chunk(chunk, i));
const task = new Task(this.chunks[i], this.fileUploadPath, this.fileExistPath);
this.tasks.add(task);
}
}
setFileHash(hash) {
this.fileHash = hash;
if (this.finish) return;
this.fileExist()
}
fileExist() {
const xhr = new XMLHttpRequest();
xhr.open('GET', `${this.fileExistPath}?hash=${hash}&isFile=${true}&filename=${encodeURIComponent(this.filename)}`);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
try {
const response = JSON.parse(xhr.response);
if (response.code === 300) {
this.finish = true;
this.emerge = true;
this.tasks.finish();
}
} catch (e) {
console.error("Failed to parse JSON response.", e);
}
};
xhr.onerror = () => {
console.error("Network error occurred.");
};
xhr.send();
}
upload() {
this.tasks.start();
}
stop() {
this.tasks.pause();
}
onProgress(cb) {
const _upload = (total) => {
cb(this.filesize, total)
}
this.tasks.setOnUpload(_upload);
}
onFinished(cb) {
this.tasks.setOnFinished(cb);
}
}
BigFileList
我们并不能设想在同一时间内用户只会上传一个文件,在同一时间时如果用户上传多个文件那么我们该怎么实现控制多个文件的上传顺序,启停单独的文件,使所有文件同时暂停开始的功能,我们通过构造BigFileList类来实现上述功能
核心方法
- startItem
传入一个下标,用于启动当前下标位置的文件上传任务 - stopItem
同startItem - getFileIndex
传入一个BigFile对象,获取当前对象在BigFileList中的下标 - start
根据MaxRunning来开启对应数量的文件上传任务 - next
功能同TaskQueue中的next - stop
停止所有的文件上传 - clear
在停止所有的文件上传任务的同时清除所有BigFile对象,清空页面中的文件列表 - maxRunning
同一时间内文件的最大上传数量 - status
当前文件列表的状态,同TaskQueue中的status
相关实现
class BigFileList {
constructor(fileMaxRunning = 3) {
this.list = [];
this.maxRunning = fileMaxRunning;
this.running = 0;
this.status = "waiting";
}
add(bigFile) {
this.list.push(bigFile);
}
getFileIndex(file) {
return this.list.findIndex(item => item === file);
}
get listCount() {
return this.list.length;
}
start(cb) {
this.status = "running";
if (this.running > this.maxRunning) return;
for (let i = 0; i < this.listCount; i++) {
this.startItem(i);
cb && cb(this.list[i]);
}
}
startItem(index) {
this.status = "running";
if (this.running >= this.maxRunning) return;
if (this.list[index].finish) return;
this.next(index);
}
stop(cb) {
if (this.status === "waiting") return;
this.status = "waiting";
this.running = 0;
for (const file of this.list) {
file.stop();
cb && cb(file);
}
}
stopItem(index) {
if (this.status === "waiting") return;
const file = this.list[index];
file.stop();
this.running--;
if (this.running === 0) this.status = "waiting";
}
deleteItem(index) {
this.stopItem(index);
this.list[index].fileDOM.onDeleteEvent();
this.list = this.list.filter((v, i) => i !== index)
}
clear(cb) {
this.stop()
for (const file of this.list) {
file.fileDOM.onDeleteEvent();
}
this.list = [];
this.running = 0;
this.status = "waiting";
cb && cb();
}
next(index) {
if (index >= this.listCount) {
return;
}
if (this.list[index].finish) {
this.next(index + 1);
return;
}
const file = this.list[index];
this.running++;
file.upload();
file.onFinished(() => {
this.running--;
this.next(index + 1);
}, (error) => {
console.error("Error:", error);
});
}
}
其他代码
至此我们所有的功能都封装完毕,我们只需在现有的基础上调用暴露出来的接口就能完整整个文件上传的所有功能,以下是具体代码
import { BigFile, BigFileList } from "./File.js";
import { DOM } from "./render.js";
const dom = new DOM();
const bigFiles = new BigFileList(3);
dom.onFileChange((files) => {
for (const file of files) {
const bigFile = new BigFile({
file: file,
maxRunning: 3,
fileUploadPath: "http://localhost:3000/file",
fileExistPath: "http://localhost:3000/exist",
chunkSize: 1024 * 1024 * 1,
});
bigFiles.add(bigFile);
dom.addFile(bigFile);
bigFile.onProgress(createProgressHandler(bigFile));
bigFile.onFinished(createFinishHandler(bigFile));
updateStatusIfNotFinished(bigFile, "暂停中");
setupButtons(bigFile);
createWorker(bigFile);
}
bigFiles.start();
});
dom.fileListStopButton.addEventListener("click", () => {
bigFiles.stop((file) => {
updateStatusIfNotFinished(file, "暂停中");
});
})
dom.fileListStartButton.addEventListener("click", () => {
bigFiles.start((file) => {
updateStatusIfNotFinished(file, "上传中");
})
})
dom.fileListClearButton.addEventListener("click", () => {
bigFiles.clear();
})
function emerge(bigFile, cb) {
if (!bigFile.finish || bigFile.emerge) return;
const hashs = bigFile.chunks.map((v) => v.hash);
const xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:3000/emerge");
const infos = {
name: bigFile.filename,
hashs: hashs,
fileHash: bigFile.fileHash,
};
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(infos));
xhr.onload = () => {
if (xhr.status === 200) {
bigFile.emerge = true;
cb && cb();
} else {
cb && cb(xhr.responseText);
}
};
xhr.onerror = () => {
cb && cb("网络请求失败");
};
}
function createProgressHandler(bigFile) {
let startDate = new Date();
return (filesize, total) => {
const newDate = new Date();
bigFile.fileDOM.updatePercent(`${(total / filesize * 100).toFixed(2)}%`);
bigFile.fileDOM.updateSpeed(`${(total / ((newDate - startDate) / 1000) / 1000000).toFixed(1)}MB/s`);
updateStatusIfNotFinished(bigFile, "上传中");
};
}
function createFinishHandler(bigFile) {
return () => {
bigFile.finish = true;
bigFile.fileDOM.updateStatus("上传完成");
bigFile.fileDOM.updatePercent("100%");
bigFile.fileDOM.updateSpeed("0MB/s");
emerge(bigFile);
};
}
function setupButtons(bigFile) {
bigFile.fileDOM.startButton.addEventListener("click", () => {
if (bigFile.finish) return;
const index = bigFiles.getFileIndex(bigFile);
bigFiles.startItem(index);
});
bigFile.fileDOM.stopButton.addEventListener("click", () => {
if (bigFile.finish) return;
const index = bigFiles.getFileIndex(bigFile);
bigFiles.stopItem(index);
updateStatusIfNotFinished(bigFile, "暂停中")
});
bigFile.fileDOM.deleteButton.addEventListener("click", () => {
const index = bigFiles.getFileIndex(bigFile);
bigFiles.deleteItem(index);
});
}
function createWorker(bigFile) {
const myWorker = new Worker("./js/worker.js");
myWorker.postMessage(bigFile.chunks);
myWorker.onmessage = (e) => {
const hash = e.data;
bigFile.setFileHash(hash);
emerge(bigFile);
};
myWorker.onerror = (error) => {
console.error("Worker error:", error);
};
bigFile.onFinished(() => {
myWorker.terminate();
});
}
function updateStatusIfNotFinished(bigFile, status) {
if (bigFile.finish) return;
bigFile.fileDOM.updateStatus(status);
}
webWorker
webWorker中的代码
importScripts("../untils/spark-md5.js")
function hash(chunks) {
return new Promise((resolve, reject) => {
const sparkMD5 = new SparkMD5()
function _read(i) {
if (i >= chunks.length) {
resolve(sparkMD5.end())
return
}
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(chunks[i].chunk)
fileReader.onload = () => {
sparkMD5.append(fileReader.result)
_read(i + 1)
}
}
_read(0)
})
}
onmessage = async function (e) {
const chunks = e.data
const md5 = await hash(chunks)
postMessage(md5)
}