Web worker可以使JS开启多线程模式,把繁重的计算放到worker线程里,可以防止主应用卡顿。
限制
- web worker不能读取dom,windows等全局属性,也无法任意引入全局文件(引入文件需通过importScript)。
- http网络请求,不能通过axios等第三方库发送网络请求,只能通过fetch或原生xhr。
通信
web worker只能通过postMessage与主线程进行通信。
const myWork = new Worder('worker.js')
// 主线程接发信息
myWork.postMessage(...)
myWork.onmessage(e => {
const data = e.data
})
// Worker接收信息 worker.js
onmessage = function(e){
// worker 业务代码
....
// worker 发送消息
postMessage(...)
}
文件切片
将切片的代码写成服务,单独放到一个文件,作为worker脚本
interface ImageUploadParams {
image_name: string;
image_tag: string;
chunk_size: number;
chunk_id: number;
task_id: string;
total_chunks: number;
image_file: any;
}
const conCurrentLimit = 10;
const retryLimit = 3;
const chunkSize = 10 * 1024 * 1024; // 10MB
let lastInstance = null as unknown as UpdateService;
interface UploadParams {
image_name: string;
image_tag: string;
image_file: File;
base_url: string;
reset?: boolean;
}
type UploadTask = ImageUploadParams & {
retryCount: number;
filename: string;
status: "pending" | "uploading" | "success" | "failed";
};
class UpdateService {
// 文件信息
private image_name = "";
private image_tag = "";
private total_chunks = 0;
readonly chunk_size = chunkSize;
image_file: File = null as unknown as File;
// 上传队列
private index = 0;
readonly conCurrentLimit = conCurrentLimit;
readonly retryLimit = retryLimit;
base_url = "";
queue: UploadTask[] = [];
taskId: string | null = null;
isRunning = false;
onComplete: (() => void) | null = null;
constructor({ image_name, image_tag, image_file, base_url }: UploadParams) {
this.image_name = image_name;
this.image_tag = image_tag;
this.image_file = image_file;
this.base_url = base_url;
this.taskId = "task" + Date.now();
if (!(this.image_file instanceof File)) {
throw new Error("image_file must be a File instance");
}
this.toSliceFile();
}
toSliceFile() {
this.total_chunks = Math.ceil(this.image_file.size / this.chunk_size);
for (let chunkId = 0; chunkId < this.total_chunks; chunkId++) {
const start = chunkId * this.chunk_size;
const end = Math.min(this.image_file.size, start + this.chunk_size);
const chunk = this.image_file.slice(start, end);
const params: UploadTask = {
image_name: this.image_name,
image_tag: this.image_tag,
chunk_size: this.chunk_size,
chunk_id: chunkId,
task_id: this.taskId as string,
total_chunks: this.total_chunks,
image_file: chunk,
status: "pending",
retryCount: 0,
filename: this.image_file.name,
};
this.queue.push(params);
}
}
startUpload() {
if (this.isRunning) return;
this.isRunning = true;
for (let i = 0; i < this.conCurrentLimit; i++) {
this.index = i;
this.uploadFile(this.queue[this.index]);
if (this.index >= this.total_chunks) break;
}
}
nextUpload() {
this.index++;
if (this.index < this.total_chunks) {
this.uploadFile(this.queue[this.index]);
}
}
private uploadFile(params: UploadTask) {
params.status = "uploading";
fetch(`${this.base_url}/image.upload_image`, {
method: "POST",
credentials: "include",
body: (() => {
const formData = new FormData();
formData.append("image_name", params.image_name + "");
formData.append("image_tag", params.image_tag + "");
formData.append("chunk_size", params.chunk_size + "");
formData.append("chunk_id", params.chunk_id + "");
formData.append("task_id", params.task_id);
formData.append("total_chunks", params.total_chunks + "");
formData.append("image_file", params.image_file);
formData.append("filename", params.filename);
return formData;
})(),
})
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(() => {
params.status = "success";
if (this.getProgress() === 100 && this.onComplete) {
this.onComplete();
} else {
this.isRunning = false;
postMessage({ progress: this.getProgress() });
this.nextUpload();
}
})
.catch(() => {
params.status = "failed";
if (params.retryCount < this.retryLimit) {
params.retryCount++;
this.uploadFile(params);
}
});
}
getProgress() {
const successCount = this.queue.filter(
(item) => item.status === "success",
).length;
return +(successCount / this.total_chunks)?.toFixed(2) * 100;
}
reset() {
this.index = 0;
this.queue = [];
this.isRunning = false;
this.taskId = "task" + Date.now();
this.toSliceFile();
}
}
onmessage = function (e) {
const { image_name, image_tag, image_file, base_url, reset } =
e.data as UploadParams;
if (reset && lastInstance) {
lastInstance.reset();
return;
}
const updateService = (lastInstance = new UpdateService({
image_name,
image_tag,
image_file,
base_url,
}));
updateService.onComplete = () => {
postMessage({ progress: 100 });
};
};
引入worker脚本
引入worker脚本时,需要注意构建之后,你的worker脚本需要被打包成是一个单独的js文件,我的项目用的是vite,参考vite.dev/guide/featu…
import { useWebWorker } from "@vueuse/core";
// 实时显示上传进度
const uploadProgress = ref(0);
const myWorker = new Worker(
new URL("/worker.ts", import.meta.url),
);
const { data, post, terminate } = useWebWorker(myWorker);
// 提交表单
const handleConfirm = () => {
formRef.value?.validate((valid) => {
if (valid) {
const params = {
image_name: form.value.image_name,
image_tag: form.value.image_tag,
image_file: form.value.image_file[0]?.raw as File,
base_url: import.meta.env.VITE_APP_BASE_API,
};
loading.value = true;
console.log(params);
post(params);
}
});
};
// 监听 workerData 实时更新进度
watch(data, (data) => {
if (data?.progress !== undefined) {
uploadProgress.value = data.progress;
if (data.progress === 100) {
ElMessage.success("上传成功");
}
}
});
寄语
尽管AI发达,依然希望这篇文章能够帮到需要的人