前言
本文将使用 Web Workers 和 MinIO 实现文件上传的Demo。如果您发现任何错误或有更优的解决方案,欢迎在评论区提出宝贵意见。
资源
目录结构
- utils
- aws-sdk.min.js
- domparser_bundle.js
- workers
- upload.js
- index.html
1. 创建目录和文件
在根目录下创建utils和worker目录,index.html。
将资源文件放在utils目录下。
worker目录下创建upload.js。
index.html如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>初体验 web worker</title>
</head>
<body>
<!-- 文档地址 https://developer.mozilla.org/zh-CN/docs/Web/API/File/webkitRelativePath -->
<input id="upload-directory" type="file" webkitdirectory multiple />
<button id="cancel">取消</button>
<script>
const S3Worker = new Worker("/workers/upload.js");
const uploadDirectoryInput = document.getElementById("upload-directory");
uploadDirectoryInput?.addEventListener("change", (e) => {
S3Worker.postMessage({ action: "uploads", payload: e.target.files });
});
const cancel = document.getElementById("cancel");
cancel?.addEventListener("click", () => {
S3Worker.postMessage({ action: "abort" });
});
</script>
</body>
</html>
upload.js如下:
// worker内不能使用window对象
var window = self;
// domparser
importScripts("/utils/domparser_bundle.js");
// worker内没有DOMParser
var DOMParser = xmldom.DOMParser;
// aws-sdk
importScripts("/utils/aws-sdk.min.js");
// 文件状态
const FileStatus = {
waiting: "waiting", //等待上传
success: "success", //上传成功
failed: "failed", // 上传失败
cancel: "cancel", // 取消
uploading: "uploading", // 上传中
};
/**
* @description uuid
* @returns {string}
*/
const getCryptoUuid = () => {
if (typeof crypto === "object") {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
if (
typeof crypto.getRandomValues === "function" &&
typeof Uint8Array === "function"
) {
const callback = (c) => {
const num = Number(c);
return (
num ^
(crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))
).toString(16);
};
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, callback);
}
}
let timestamp = new Date().getTime();
let perforNow =
(typeof performance !== "undefined" &&
performance.now &&
performance.now() * 1000) ||
0;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
let random = Math.random() * 16;
if (timestamp > 0) {
random = (timestamp + random) % 16 | 0;
timestamp = Math.floor(timestamp / 16);
} else {
random = (perforNow + random) % 16 | 0;
perforNow = Math.floor(perforNow / 16);
}
return (c === "x" ? random : (random & 0x3) | 0x8).toString(16);
});
};
/**
* @description 创建S3实例
* @returns {AWS.S3}
*/
const createS3 = () => {
const accessKeyId = ""; // 账号
const secretAccessKey = ""; // 密码
const endpoint = ""; // minio上传地址
const region = ""; // AWS 服务端点 文档地址 https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html
const s3ForcePathStyle = true; // 是否强制使用 S3 对象的路径样式 URL。
const signatureVersion = "v4"; //用于对请求进行签名的签名版本(覆盖 API 置)。可能的值: 'v2'|'v3'|'v4'
return new AWS.S3({
accessKeyId,
secretAccessKey,
endpoint,
region,
s3ForcePathStyle,
signatureVersion,
});
};
/**
* @description 上传管理
*/
class UploadManage {
#bucket; // 桶名
#s3 = createS3(); // s3 实例
#fileList = []; // 文件列表
#events = []; // 事件响应
#uploader = new Map(); // 上传实例
/**
* @param {{bucket:string;}} param0
*/
constructor({ bucket }) {
this.#bucket = bucket;
}
/**
* @description
* @param {FileList} fileList
*/
setFileList(fileList) {
this.#fileList = [];
for (let index = 0; index < fileList.length; index++) {
const file = fileList.item(index);
if (file) {
this.#fileList.push({
originFile: file,
status: FileStatus.waiting,
percent: 0,
});
}
}
}
/**
* @description 监听事件
* @param {"onSucess" | "onError" | "onFailed" | "onChange" | "onLoading" | "onProgress" | "onAbort"} event
* @param {(e:any)=>void} listener
*/
on(event, listener) {
this.#events.push({
listener,
event,
});
}
/**
* @description 关闭事件
* @param {"onSucess" | "onError" | "onFailed" | "onChange" | "onLoading" | "onProgress" | "onAbort"} event
*/
off(event) {
this.#events = this.#events.filter((item) => item.event !== event);
}
/**
* @description 触发事件
* @param {"onSucess" | "onError" | "onFailed" | "onChange" | "onLoading" | "onProgress" | "onAbort"} event
* @param {any} data
*/
#emit(event, data) {
this.#events.forEach((item) => {
if (item.event === event) {
item.listener(data);
}
});
}
/**
* @description 上传文件
* @param {{originFile:File;status:string;percent:number;abort:()=>void|null;}} file
*/
#upload(file) {
const params = {
Bucket: this.#bucket,
Key: file.originFile.webkitRelativePath,
Body: file.originFile,
ContentType: file.originFile.type,
};
const uuid = getCryptoUuid();
const uploader = this.#s3.upload(params);
this.#uploader.set(uuid, uploader);
file.abort = uploader.abort;
uploader.on("httpUploadProgress", (p) => {
file.percent = Number(((p.loaded / p.total) * 100).toFixed(0));
});
uploader.send((err, data) => {
if (err) {
file.status = FileStatus.failed;
this.#emit("onFailed", err);
} else {
file.status = FileStatus.success;
this.#emit("onSucess", data);
}
file.abort = null;
this.#uploader.delete(uuid);
this.#pushUpload();
});
}
/**
* @description 取消全部上传
*/
abort() {
this.#fileList.forEach((file) => {
if (file.status === FileStatus.waiting) {
file.status = FileStatus.cancel;
}
});
for (const [_, uploader] of this.#uploader.entries()) {
uploader.abort();
}
this.#uploader.clear();
}
/**
* @description 推入上传
*/
#pushUpload() {
for (let index = 0; index < this.#fileList.length; index++) {
const file = this.#fileList[index];
if (file.status === FileStatus.waiting) {
file.status = FileStatus.uploading;
this.#upload(file);
break;
}
}
}
/**
* @description 开始上传
*/
start() {
if (this.#fileList.length === 0) {
this.#emit("onError", "没有可上传的文件");
} else {
this.#fileList.slice(0, 3).forEach((file) => {
file.status = FileStatus.uploading;
this.#upload(file);
});
}
}
}
const actions = {
uploads: "uploads",
abort: "abort",
};
// 检测有无S3
if (AWS && AWS.S3) {
// 上传管理实例
const uploadManage = new UploadManage({ bucket: "你的桶名" });
// 上传成功
uploadManage.on("onSucess", (e) => {
console.log(e, "onSucess");
});
// 上传失败
uploadManage.on("onFailed", (e) => {
console.log(e, "onFailed");
});
// 监听通信
onmessage = (e) => {
const { action, payload } = e.data;
switch (action) {
case actions.uploads:
uploadManage.setFileList(payload);
uploadManage.start();
break;
case actions.abort:
uploadManage.abort();
break;
default:
break;
}
};
}
2. 启动项目
使用http-server
npx http-server