Vue 3 中用组合式函数和 Shared Worker 实现后台分片上传(带哈希计算)

这篇具有很好参考价值的文章主要介绍了Vue 3 中用组合式函数和 Shared Worker 实现后台分片上传(带哈希计算)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

01. 背景

最近项目需求里有个文件上传功能,而客户需求里的文件基本上是比较大的,基本上得有 1 GiB 以上的大小,而上传大文件尤其是读大文件,可能会造成卡 UI 或者说点不动的问题。而用后台的 Worker 去实现是一个比较不错的解决办法。

02. 原理讲解

02.01. Shared Worker

Shared Worker 的好处是可以从几个浏览上下文中访问,例如几个窗口、iframe 或其他 worker。这样我们可以保证全局的页面上传任务都在我们的控制之下,甚至可以防止重复提交等功能。

02.02. 组合式函数

组合式函数的好处是在 Vue 3 是可以在任何 *.vue 文件中使用,并且是响应式方法,可以侦听 pinia 内 token 等的变化,传递给 Worker

02.03 简单流程设计

flowchart TB id1[用户选择文件] --> id2[创建上传任务] id2 --> id3[任务推送到 Worker] id3 --> id4[上传到服务器] id4 --> id5[Worker 返回任务状态] id5 --> id6[组合式函数拦截状态放到 Map 里]

03. 代码

upload-worker.ts 代码

import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex as toHex } from '@noble/hashes/utils';
interface SharedWorkerGlobalScope {
  onconnect: (event: MessageEvent<any>) => void;
}
const _self: SharedWorkerGlobalScope = self as any;
/**
 * 分片大小
 */
const pieceSize = 1024 * 1024;
/**
 * 消息参数
 */
interface MessageArg<T> {
  /**
   * 函数名
   */
  func: string;
  /**
   * 参数
   */
  arg: T;
}
/**
 * 上传任务信息
 */
interface UploadTaskInfo {
  /**
   * 文件名
   */
  fileName: string;
  /**
   * 上传路径
   */
  uploadPath: string;
  /**
   * 任务 id
   */
  id: string;
  /**
   * 文件大小
   */
  size: number;
  /**
   * 上传进度
   */
  progress: number;
  /**
   * 上传速度
   */
  speed?: number;
  /**
   * 任务状态
   */
  status: 'uploading' | 'paused' | 'canceled' | 'done' | 'error' | 'waiting';
  /**
   * 开始时间
   */
  startTime?: Date;
  /**
   * 结束时间
   */
  endTime?: Date;
  /**
   * 错误信息
   */
  errorMessage?: string;
}
/**
 * 上传任务
 */
interface UploadTask extends UploadTaskInfo {
  file: File;
  pieces: Array<boolean>;
  abort?: AbortController;
}
/**
 * 任务/哈希值映射
 */
const hashs = new Map();
/**
 * 上传任务列表
 */
const uploadTasks: Array<UploadTask> = [];
/**
 * 状态接收器
 */
const statusReceivers = new Map<string, MessagePort>();
/**
 * token 仓库
 */
const tokenStore = {
  /**
   * token
   */
  BearerToken: '',
};
/**
 * 返回上传状态
 * @param task 上传任务
 */
const updateStatus = (task: UploadTaskInfo) => {
  const taskInfo: UploadTaskInfo = {
    fileName: task.fileName,
    uploadPath: task.uploadPath,
    id: task.id,
    size: task.size,
    progress: task.progress,
    speed: task.speed,
    status: task.status,
    startTime: task.startTime,
    endTime: task.endTime,
    errorMessage: task.errorMessage,
  };
  statusReceivers.forEach((item) => {
    item.postMessage(taskInfo);
  });
};
/**
 * 运行上传任务
 * @param task 上传任务
 */
const runUpload = async (task: UploadTask) => {
  task.status = 'uploading';
  const hash = hashs.get(task.id) || sha256.create();
  hashs.set(task.id, hash);
  let retryCount = 0;
  const abort = new AbortController();
  task.abort = abort;
  while (task.status === 'uploading') {
    const startTime = Date.now();
    const index = task.pieces.findIndex((item) => !item);
    if (index === -1) {
      try {
        const response: { code: number; message: string } = await fetch(
          '/api/File/Upload',
          {
            method: 'PUT',
            headers: {
              Authorization: tokenStore.BearerToken,
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              id: task.id,
              fileHash: toHex(hash.digest()),
              filePath: task.uploadPath,
            }),
          }
        ).then((res) => res.json());
        if (response.code !== 200) {
          throw new Error(response.message);
        }
        task.status = 'done';
        task.endTime = new Date();
        updateStatus(task);
      } catch (e: any) {
        task.status = 'error';
        task.errorMessage = e.toString();
        task.endTime = new Date();
        deleteUpload(task.id);
        updateStatus(task);
      }
      break;
    }
    const start = index * pieceSize;
    const end = start + pieceSize >= task.size ? task.size : start + pieceSize;
    const buffer = task.file.slice(index * pieceSize, end);
    hash.update(new Uint8Array(await buffer.arrayBuffer()));
    const form = new FormData();
    form.append('file', buffer);
    let isTimeout = false;
    try {
      const timer = setTimeout(() => {
        isTimeout = true;
        abort.abort();
      }, 8000);
      const response: { code: number; message: string } = await fetch(
        `/api/File/Upload?id=${task.id}&offset=${start}`,
        {
          method: 'POST',
          body: form,
          headers: {
            Authorization: tokenStore.BearerToken,
          },
          signal: abort.signal,
        }
      ).then((res) => res.json());
      clearTimeout(timer);
      if (response.code !== 200) {
        throw new Error(response.message);
      }
      task.pieces[index] = true;
      task.progress =
        task.pieces.filter((item) => item).length / task.pieces.length;
      task.speed = (pieceSize / (Date.now() - startTime)) * 1000;
      updateStatus(task);
    } catch (e: any) {
      retryCount++;
      if (retryCount > 3) {
        task.status = 'error';
        if (isTimeout) {
          task.errorMessage = 'UploadTimeout';
        } else {
          task.errorMessage = e.toString();
        }
        task.endTime = new Date();
        deleteUpload(task.id);
        updateStatus(task);
      }
    }
    runNextUpload();
  }
};
/**
 * 运行下一个上传任务
 */
const runNextUpload = async () => {
  if (uploadTasks.filter((item) => item.status === 'uploading').length > 3) {
    return;
  }
  const task = uploadTasks.find((item) => item.status === 'waiting');
  if (task) {
    await runUpload(task);
  }
};
/**
 * 排队上传
 * @param e 消息事件
 */
const queueUpload = async (
  e: MessageEvent<
    MessageArg<{
      id: string;
      file: File;
      uploadPath: string;
    }>
  >
) => {
  uploadTasks.push({
    file: e.data.arg.file,
    fileName: e.data.arg.file.name,
    id: e.data.arg.id,
    uploadPath: e.data.arg.uploadPath,
    size: e.data.arg.file.size,
    progress: 0,
    speed: 0,
    status: 'waiting',
    pieces: new Array(Math.ceil(e.data.arg.file.size / pieceSize)).fill(false),
    errorMessage: undefined,
  });
  updateStatus(uploadTasks[uploadTasks.length - 1]);
  await runNextUpload();
};
/**
 * 注册状态接收器
 * @param e 消息事件
 * @param sender 发送者
 */
const registerStatusReceiver = (
  e: MessageEvent<MessageArg<string>>,
  sender?: MessagePort
) => {
  if (sender) statusReceivers.set(e.data.arg, sender);
};
/**
 * 注销状态接收器
 * @param e 消息事件
 */
const unregisterStatusReceiver = (e: MessageEvent<MessageArg<string>>) => {
  statusReceivers.delete(e.data.arg);
};
/**
 * 更新 token
 * @param e 消息事件
 */
const updateToken = (e: MessageEvent<MessageArg<string>>) => {
  tokenStore.BearerToken = 'Bearer ' + e.data.arg;
};
/**
 * 暂停上传
 * @param e 消息事件
 */
const pauseUpload = (e: MessageEvent<MessageArg<string>>) => {
  const task = uploadTasks.find((item) => item.id === e.data.arg);
  if (task) {
    task.status = 'paused';
    if (task.abort) {
      task.abort.abort();
    }
    updateStatus(task);
  }
};
/**
 * 取消上传
 * @param e 消息事件
 */
const cancelUpload = (e: MessageEvent<MessageArg<string>>) => {
  const task = uploadTasks.find((item) => item.id === e.data.arg);
  if (task) {
    task.status = 'canceled';
    if (task.abort) {
      task.abort.abort();
    }
    deleteUpload(task.id);
    updateStatus(task);
  }
};
/**
 * 删除上传
 * @param id 任务 id
 */
const deleteUpload = async (id: string) => {
  uploadTasks.splice(
    uploadTasks.findIndex((item) => item.id === id),
    1
  );
  hashs.delete(id);
  await fetch(`/api/File/Upload?id=${id}`, {
    method: 'DELETE',
    headers: {
      Authorization: tokenStore.BearerToken,
    },
  }).then((res) => res.json());
};
/**
 * 消息路由
 */
const messageRoute = new Map<
  string,
  (e: MessageEvent<MessageArg<any>>, sender?: MessagePort) => void
>([
  ['queueUpload', queueUpload],
  ['registerStatusReceiver', registerStatusReceiver],
  ['updateToken', updateToken],
  ['pauseUpload', pauseUpload],
  ['cancelUpload', cancelUpload],
  ['unregisterStatusReceiver', unregisterStatusReceiver],
]);
// 监听连接
_self.onconnect = (e) => {
  const port = e.ports[0];
  port.onmessage = async (e) => {
    // 调用函数
    const func = messageRoute.get(e.data.func);
    if (func) {
      func(e, port);
    }
  };
  port.start();
};

upload-service.ts 代码文章来源地址https://www.toymoban.com/news/detail-711276.html

import UploadWorker from './upload-worker?sharedworker';
import { onUnmounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useAuthStore } from 'src/stores/auth';
/**
 * 上传任务信息
 */
interface UploadTaskInfo {
  /**
   * 文件名
   */
  fileName: string;
  /**
   * 上传路径
   */
  uploadPath: string;
  /**
   * 任务 id
   */
  id: string;
  /**
   * 文件大小
   */
  size: number;
  /**
   * 上传进度
   */
  progress: number;
  /**
   * 上传速度
   */
  speed?: number;
  /**
   * 任务状态
   */
  status: 'uploading' | 'paused' | 'canceled' | 'done' | 'error' | 'waiting';
  /**
   * 开始时间
   */
  startTime?: Date;
  /**
   * 结束时间
   */
  endTime?: Date;
  /**
   * 错误信息
   */
  errorMessage?: string;
}
/**
 * 上传服务
 */
export const useUploadService = () => {
  const store = storeToRefs(useAuthStore());
  // 创建共享 worker
  const worker = new UploadWorker();
  /**
   * 上传任务列表
   */
  const uploadTasks = ref<Map<string, UploadTaskInfo>>(
    new Map<string, UploadTaskInfo>()
  );
  // 是否已注册状态接收器
  const isRegistered = ref(false);
  // 服务 id
  const serviceId = crypto.randomUUID();
  // 监听上传任务列表变化(只有在注册状态接收器后才会收到消息)
  worker.port.onmessage = (e: MessageEvent<UploadTaskInfo>) => {
    uploadTasks.value.set(e.data.id, e.data);
  };
  // 更新 token
  worker.port.postMessage({
    func: 'updateToken',
    arg: store.token.value,
  });
  watch(store.token, (token) => {
    worker.port.postMessage({
      func: 'updateToken',
      arg: token,
    });
  });
  /**
   * 排队上传
   * @param file 文件
   * @param uploadPath 上传路径
   */
  const queueUpload = (file: File, uploadPath: string) => {
    worker.port.postMessage({
      func: 'queueUpload',
      arg: {
        id: crypto.randomUUID(),
        file: file,
        uploadPath: uploadPath,
      },
    });
  };
  /**
   * 暂停上传
   * @param id 任务 id
   */
  const pauseUpload = (id: string) => {
    worker.port.postMessage({
      func: 'pauseUpload',
      arg: id,
    });
  };
  /**
   * 取消上传
   * @param id 任务 id
   */
  const cancelUpload = (id: string) => {
    worker.port.postMessage({
      func: 'cancelUpload',
      arg: id,
    });
  };
  /**
   * 注册状态接收器
   */
  const registerStatusReceiver = () => {
    worker.port.postMessage({
      func: 'registerStatusReceiver',
      arg: serviceId,
    });
    isRegistered.value = true;
  };
  /**
   * 注销状态接收器
   */
  const unregisterStatusReceiver = () => {
    worker.port.postMessage({
      func: 'unregisterStatusReceiver',
      arg: serviceId,
    });
    isRegistered.value = false;
  };
  onUnmounted(() => {
    unregisterStatusReceiver();
    worker.port.close();
  });
  return {
    uploadTasks,
    queueUpload,
    pauseUpload,
    cancelUpload,
    registerStatusReceiver,
    unregisterStatusReceiver,
  };
};

04. 用法

// 引入组合式函数
const uploadService = useUploadService();
// 注册状态接收器
uploadService.registerStatusReceiver();
// 表单绑定上传方法
const upload = (file: File, filePath: string) => {
  uploadService.queueUpload(file, filePath);
}
// 监听上传进度,当然也可以直接展示在界面,毕竟是 Ref
watch(uploadService.uploadTasks, console.log);

到了这里,关于Vue 3 中用组合式函数和 Shared Worker 实现后台分片上传(带哈希计算)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • Vue3的组合式API中如何使用setup()函数中的条件渲染和循环渲染

    首先,让我们来了解一下什么是Vue3的组合式API。组合式API是一种在Vue3中用于构建组件的方式,它使用函数式编程的方法来组织代码,让你的组件更加清晰、易于理解和维护。而setup()函数则是组合式API的核心,它用于初始化组件并返回包含组件数据的对象。 现在,让我们来看

    2024年02月11日
    浏览(10)
  • vue3组合式笔记

    dfdf {{count}} {{state.count}} {{computedValue}} // // 校验 submit 事件 // submit: ({ email, password }) = { // if (email password) { // return true // } else { // console.warn(‘Invalid submit event payload!’) // return false // } // } // }) // function submitForm(email, password) { // emit(‘submit’, { email, password }) // } // input // type=“

    2024年02月06日
    浏览(12)
  • Vue3组合式API

    Vue3组合式API

    目录 composition API vs options API 体验 composition API setup 函数 reactive 函数 ref 函数 script setup 语法 计算属性 computed 函数 监听器 watch 函数 生命周期 模板中 ref 的使用 组件通讯 - 父传子 组件通讯 - 子传父 依赖注入 - provide 和 inject 保持响应式 - toRefs 函数 vue2 采用的就是 options API (

    2024年02月07日
    浏览(11)
  • js继承的几种方式(原型链继承、构造函数继承、组合式继承、寄生组合式继承、ES6的Class类继承)

    js继承的几种方式(原型链继承、构造函数继承、组合式继承、寄生组合式继承、ES6的Class类继承)

    实现原理: 子类的原型指向父类实例 。子类在自身实例上找不到属性和方法时去它父类实例(父类实例和实例的原型对象)上查找,从而实现对父类属性和方法的继承 缺点: 子类创建时不能传参(即没有实现super()的功能); 父类实例的修改会影响子类所有实例 实现原理:

    2024年02月07日
    浏览(16)
  • vue3组合式API介绍

    根据官方的说法,vue3.0的变化包括性能上的改进、更小的 bundle 体积、对 TypeScript 更好的支持、用于处理大规模用例的全新 API,全新的api指的就是本文主要要说的组合式api。 在 vue3 版本之前,我们复用组件(或者提取和重用多个组件之间的逻辑),通常有以下几种方式: M

    2023年04月22日
    浏览(12)
  • 快速入门vue3组合式API

    快速入门vue3组合式API

    (创作不易,感谢有你,你的支持,就是我前行的最大动力,如果看完对你有帮助,请留下您的足迹) 目录 使用create-vue创建项目 熟悉项目目录和关键文件  组合式API  setup选项 setup选项的写法和执行时机 script setup 语法糖 reactive和ref函数 reactive() ref() computed watch 侦听单个数据

    2024年02月12日
    浏览(10)
  • 基于Vue组合式API的实用工具集

    基于Vue组合式API的实用工具集

    今天,给大家分享一个很实用的工具库 VueUse,它是基于 Vue Composition Api,也就是组合式API。支持在Vue2和Vue3项目中进行使用,据说是目前世界上Star最高的同类型库之一。 图片 官方地址: https://vueuse.org/ 中文地址: https://www.vueusejs.com/ github: https://github.com/vueuse/vueuse 图片 链接

    2024年01月23日
    浏览(7)
  • vue3 组合式 api 单文件组件写法

    Vue3 中的 Composition API 是一种新的编写组件逻辑的方式,它提供了更好的代码组织、类型推导、测试支持和复用性。相比于 Vue2 的 Options API,Composition API 更加灵活和可扩展。 在 Composition API 中,我们使用 setup 函数来定义组件的逻辑部分。setup 函数是一个特殊的函数,在创建组

    2024年02月12日
    浏览(11)
  • vue3组合式api单文件组件写法

    一,模板部分  二,js逻辑部分 

    2024年02月13日
    浏览(16)
  • vue3组合式写法在方法中出发点击事件

    vue3组合式写法在方法中出发点击事件

    问: 用vue3组合式写法,如何在一个方法中调用a标签的点击事件 回答: 在Vue 3的组合式API中,可以通过ref来获取DOM元素的引用,然后使用$el属性访问DOM元素并触发其点击事件。下面是示例代码: 在上述代码中,首先通过ref创建了一个名为linkRef的引用变量,并将其初始化为

    2024年02月15日
    浏览(9)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包