Web Worker 切片上传

37 阅读2分钟

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发达,依然希望这篇文章能够帮到需要的人