跳到主要内容

HTML

2024年10月17日
柏拉文
越努力,越幸运

一、认识


二、细节


2.1 获取文件

获取文件

function getFile(e) {
const {
target: { files },
} = e;
file = files[0];
}

2.2 获取文件的 MD5

获取文件的 MD5

fileMD5.js
import SparkMD5 from "spark-md5";

export function calcFileMD5(file, chunkSize) {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = (e) => {
reject(fileReader.error);
fileReader.abort();
};
function loadNext() {
const start = currentChunk * chunkSize;
const end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}

2.3 检测服务端是否含有文件名、文件 MD5 列表

检测服务端是否含有文件名、文件 MD5 列表

async function checkFileExist() {
const { name, type } = file;
const { data } = await get("http://localhost:4000/upload/exists", {
name,
type: type.split("/")[0],
md5: fileMD5,
});
return data;
}

2.4 如果服务端不存在该文件名,开始文件分片

如果服务端不存在该文件名,开始文件分片

function uploadChunks() {
const { size } = file;
const chunks = Math.ceil(size / chunkSize);
return asyncPool(poolLimit, [...new Array(chunks).keys()], (i) => {
if (chunkFinishList.indexOf(i + "") !== -1) {
return Promise.resolve();
}
const start = i * chunkSize;
const end = i + 1 === chunks ? size : (i + 1) * chunkSize;
const chunk = file.slice(start, end);
return uploadChunk({
chunk,
chunkIndex: i,
});
});
}

2.5 检测文件片段在服务端是否存在,如果不存在,并发请求

检测文件片段在服务端是否存在,如果不存在,并发请求

asyncPool.js
export default async function (poolLimit, array, iteratorFn) {
const taskList = [];
const currentTaskList = [];
for (const item of array) {
const promise = Promise.resolve().then(() => iteratorFn(item, array));
taskList.push(promise);
if (poolLimit <= array.length) {
const currentPromise = promise.then(() =>
currentTaskList.splice(currentTaskList.indexOf(currentPromise), 1)
);
currentTaskList.push(currentPromise);
if (currentTaskList.length >= poolLimit) {
await Promise.race(currentTaskList);
}
}
}
return Promise.all(taskList);
}

2.6 所有片段发送成功,服务端开始合并片段

所有片段发送成功,服务端开始合并片段

function concatChunks() {
const { name, type } = file;
return get("http://localhost:4000/upload/concatFragment", {
name,
type: type.split("/")[0],
md5: fileMD5,
});
}

三、实现


入口文件 index.js
import asyncPool from "./asyncPool";
import { calcFileMD5 } from "./fileMD5.js";
import { get, post, upload } from "./request.js";

let file;
let fileMD5;
const poolLimit = 2;
const chunkSize = 2 * 1024 * 1024;
let chunkFinishList = [];

start();

function start() {
const input = document.getElementById("file");
const btn = document.getElementById("btn");
input.addEventListener("change", getFile);
btn.addEventListener("click", uploadFile);
}

function getFile(e) {
const {
target: { files },
} = e;
file = files[0];
}
async function uploadFile() {
fileMD5 = await calcFileMD5(file, chunkSize);
const result = await checkFileExist();
if (result.isExists) {
console.log("该文件已上传成功!");
return;
}
chunkFinishList = result.chunkFinishList;
await uploadChunks();
await concatChunks();
}

async function checkFileExist() {
const { name, type } = file;
const { data } = await get("http://localhost:4000/upload/exists", {
name,
type: type.split("/")[0],
md5: fileMD5,
});
return data;
}

function uploadChunks() {
const { size } = file;
const chunks = Math.ceil(size / chunkSize);
return asyncPool(poolLimit, [...new Array(chunks).keys()], (i) => {
if (chunkFinishList.indexOf(i + "") !== -1) {
return Promise.resolve();
}
const start = i * chunkSize;
const end = i + 1 === chunks ? size : (i + 1) * chunkSize;
const chunk = file.slice(start, end);
return uploadChunk({
chunk,
chunkIndex: i,
});
});
}

function uploadChunk({ chunk, chunkIndex }) {
const { name } = file;
const formData = new FormData();
formData.set("file", chunk, fileMD5 + "-" + chunkIndex);
formData.set("fileName", name);
formData.set("timestamp", Date.now());
return upload("http://localhost:4000/upload/fragmentUpload", formData);
}

function concatChunks() {
const { name, type } = file;
return get("http://localhost:4000/upload/concatFragment", {
name,
type: type.split("/")[0],
md5: fileMD5,
});
}
请求方法 ajax.js
export function get(url, params) {
return new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
const newUrl = Object.keys(params).reduce((prev, curr) => {
return `${prev}${curr}=${params[curr]}&`;
}, `${url}?`);
ajax.open("get", newUrl.slice(0, -1), true);
ajax.onreadystatechange = () => {
if (ajax.readyState === 4 && [200, 304].includes(ajax.status)) {
const { response } = ajax;
resolve(JSON.parse(response));
}
};
ajax.send();
});
}
export function post(url, data) {
const ajax = new XMLHttpRequest();
}
export function upload(url, formData) {
return new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.open("post", url, true);
ajax.onreadystatechange = () => {
if (ajax.readyState === 4 && [200, 304].includes(ajax.status)) {
const { response } = ajax;
resolve(JSON.parse(response));
}
};
ajax.send(formData);
});
}
fileMD5.js
import SparkMD5 from "spark-md5";

export function calcFileMD5(file, chunkSize) {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = (e) => {
reject(fileReader.error);
fileReader.abort();
};
function loadNext() {
const start = currentChunk * chunkSize;
const end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(file.slice(start, end));
}
loadNext();
});
}
asyncPool.js
export default async function (poolLimit, array, iteratorFn) {
const taskList = [];
const currentTaskList = [];
for (const item of array) {
const promise = Promise.resolve().then(() => iteratorFn(item, array));
taskList.push(promise);
if (poolLimit <= array.length) {
const currentPromise = promise.then(() =>
currentTaskList.splice(currentTaskList.indexOf(currentPromise), 1)
);
currentTaskList.push(currentPromise);
if (currentTaskList.length >= poolLimit) {
await Promise.race(currentTaskList);
}
}
}
return Promise.all(taskList);
}

参考资料


阿宝哥-JavaScript 中如何实现大文件并发上传?