Vue 3 中用組合式函數和 Shared Worker 實現後臺分片上傳(帶哈希計算)

来源:https://www.cnblogs.com/aobaxu/archive/2023/10/26/17790793.html
-Advertisement-
Play Games

01. 背景 最近項目需求里有個文件上傳功能,而客戶需求里的文件基本上是比較大的,基本上得有 1 GiB 以上的大小,而上傳大文件尤其是讀大文件,可能會造成卡 UI 或者說點不動的問題。而用後臺的 Worker 去實現是一個比較不錯的解決辦法。 02. 原理講解 02.01. Shared Work ...


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 代碼

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);

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 電腦數據的表示 1. 數值數據的表示 1.1 各種進位數的表示 二進位 (Binary) :以 0b 或 0B 開頭,字元僅含0和1. 用下標2或者數字後面加B表示。如 $(1011)_2$ 或 $1011B$ 八進位 (Octal) :以 0o 或 0O 開頭,字元含0-7. 用下標8或者數字後 ...
  • 1. 圖論 1.1. 起源於萊昂哈德·歐拉在1736年發表的一篇關於“哥尼斯堡七橋問題”的論文 1.2. 要解決這個問題,該圖需要零個或兩個具有奇數連接的節點 1.3. 任何滿足這一條件的圖都被稱為歐拉圖 1.4. 如果路徑只訪問每條邊一次,則該圖具有歐拉路徑 1.5. 如果路徑起點和終點相同,則該 ...
  • 天下武功,無堅不摧,唯快不破!我的名字叫 Redis,全稱是 Remote Dictionary Server。 有人說,組 CP,除了要瞭解她外,還要給機會讓她瞭解你。 那麼,作為開發工程師的你,是否願意認真閱讀此心法抓住機會來瞭解我,運用到你的系統中提升性能。 我遵守 BSD 協議,由義大利人 ...
  • 一、背景 公元2023-10-12(周四)上午,組內的亞梅反饋,用戶生成標簽報死鎖異常 二、排查異常日誌 查到當時報錯的日誌 具體異常信息如下 server-provider-info-2023-10-12.0.log:2023-10-12 09:40:50.593 [TID:bf623bded18 ...
  • 在資料庫中,對象的創建者將成為該對象的所有者,具有對該對象進行查詢、修改和刪除等操作的許可權。同時,系統管理員也擁有與所有者相同的許可權。 ...
  • 本文介紹了視頻黑屏的可能原因和解決方案。主要原因包括用戶主動關閉視頻、網路問題和渲染問題。解決方案包括優化網路穩定性、確保視頻渲染視圖設置正確、提供清晰的提示、實時監測網路質量、使用詳細的日誌系統、開啟視頻預覽功能、使用視頻流回調、處理編解碼問題、處理許可權問題、自定義視頻渲染邏輯和使用實時反饋系統。... ...
  • 頁面路由指在應用程式中實現不同頁面之間的跳轉和數據傳遞。HarmonyOS提供了Router模塊,通過不同的url地址,可以方便地進行頁面路由,輕鬆地訪問不同的頁面。 一、基礎使用 Router模塊提供了兩種跳轉模式,分別是router.pushUrl()和router.replaceUrl()。這 ...
  • ArkTS是HarmonyOS優選的主力應用開發語言。ArkTS圍繞應用開發在TypeScript(簡稱TS)生態基礎上做了進一步擴展,繼承了TS的所有特性,是TS的超集。 ArkTS在TS的基礎上主要擴展瞭如下能力: 基本語法:ArkTS定義了聲明式UI描述、自定義組件和動態擴展UI元素的能力,再 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...