最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Vue3實(shí)現(xiàn)大文件上傳:切片上傳 / 斷點(diǎn)續(xù)傳 / 秒傳 / 暫?;謴?fù) / 全局并發(fā)控制

 更新時(shí)間:2026年02月20日 12:03:08   作者:前端百草閣  
本文詳細(xì)介紹了基于Vue3實(shí)現(xiàn)的大文件上傳方案,涵蓋切片上傳、斷點(diǎn)續(xù)傳、秒傳、暫停/恢復(fù)和全局并發(fā)控制等功能,通過全局并發(fā)限流器、WebWorker進(jìn)行切片和Hash計(jì)算、以及精確的進(jìn)度統(tǒng)計(jì),該方案在提高上傳效率和用戶體驗(yàn)方面表現(xiàn)出色

概述

本文基于 Vue3 實(shí)現(xiàn)一個(gè)大文件上傳方案,完整覆蓋以下核心能力:

  • 切片上傳
  • 斷點(diǎn)續(xù)傳
  • 秒傳(存在即返回)
  • 暫停 / 恢復(fù)
  • 全局并發(fā)控制與實(shí)時(shí)進(jìn)度

本文采用全局并發(fā)限流器作為上傳調(diào)度核心,讓多個(gè)文件、多個(gè)切片總并發(fā)可控的前提下高效上傳。

技術(shù)架構(gòu)

  • 前端:Vue3(Composition API)
  • 并發(fā)控制:全局 ConcurrencyLimiter(信號(hào)量/隊(duì)列)
  • 進(jìn)度統(tǒng)計(jì):切片級(jí)別累加 → 文件級(jí)別 → 全局
  • 傳輸庫(kù):axios(支持上傳進(jìn)度與取消)
  • 切片與 Hash:Web Worker + SparkMD5

整體上傳流程圖

目錄結(jié)構(gòu)建議

僅供參考,便于組織代碼:

src/
  api/
    index.ts           // axios 實(shí)例與 API 函數(shù)
  worker/
    hash-worker.js     // 切片與文件Hash計(jì)算
  upload/
    limiter.ts         // ConcurrencyLimiter
    types.ts           // 類型定義
    helpers.ts         // 進(jìn)度、重試、工具函數(shù)
  views/
    Uploader.vue       // 上傳頁(yè)面(UI + 調(diào)度)

類型定義(upload/types.ts)

統(tǒng)一、清晰的命名有助于維護(hù)與協(xié)作。

/**
 * 文件上傳狀態(tài)枚舉
 * - idle: 初始狀態(tài),未開始上傳
 * - uploading: 上傳中
 * - paused: 已暫停
 * - completed: 上傳完成
 * - failed: 上傳失敗
 */
export type FileState = 'idle' | 'uploading' | 'paused' | 'completed' | 'failed';

/**
 * 分片上傳狀態(tài)枚舉
 * - idle: 初始狀態(tài),未開始上傳
 * - uploading: 上傳中
 * - completed: 上傳完成
 * - failed: 上傳失敗
 */
export type ChunkState = 'idle' | 'uploading' | 'completed' | 'failed';

/**
 * 分片上傳任務(wù)信息
 */
export interface UploadChunkTask {
  fileId: string;                // 所屬文件ID
  chunkId: string;               // 分片唯一標(biāo)識(shí),格式如 `${fileId}-${chunkIndex}`
  chunkIndex: number;            // 分片序號(hào)(從0開始)
  size: number;                  // 分片大小(字節(jié))
  blob: Blob;                    // 分片二進(jìn)制數(shù)據(jù)

  chunkHash: string;             // 分片哈希值,格式如 `${fileHash}-${chunkIndex}`,用于斷點(diǎn)續(xù)傳識(shí)別
  uploadedBytes: number;         // 已上傳字節(jié)數(shù)
  attemptCount: number;          // 當(dāng)前重試次數(shù)
  maxAttempts: number;           // 最大重試次數(shù)
  abortController: AbortController; // 用于取消分片上傳的中止控制器
  state: ChunkState;             // 當(dāng)前分片狀態(tài)
}

/**
 * 文件上傳任務(wù)信息
 */
export interface UploadFileTask {
  fileId: string;                // 文件唯一標(biāo)識(shí)
  fileName: string;              // 文件名
  fileSize: number;              // 文件總大?。ㄗ止?jié))
  fileHash: string;              // 文件哈希值(用于整體校驗(yàn))

  chunkSize: number;             // 每個(gè)分片的大?。ㄗ止?jié))
  totalChunks: number;           // 總分片數(shù)
  chunkTasks: UploadChunkTask[]; // 所有分片任務(wù)數(shù)組

  state: FileState;              // 文件整體上傳狀態(tài)
  isPaused: boolean;             // 是否手動(dòng)暫停

  inflightChunks: Set<UploadChunkTask>; // 正在上傳中的分片集合
  uploadedBytes: number;         // 文件級(jí)已上傳字節(jié)數(shù)(累計(jì)所有分片)
  totalBytes: number;            // 文件級(jí)總字節(jié)數(shù)(等于fileSize)
  percent: number;               // 文件級(jí)上傳進(jìn)度百分比(0-100)
}

/**
 * 全局上傳進(jìn)度信息
 */
export interface GlobalProgress {
  uploadedBytes: number;         // 全局已上傳字節(jié)數(shù)(累計(jì)所有文件)
  totalBytes: number;            // 全局總字節(jié)數(shù)(累計(jì)所有文件)
  percent: number;               // 全局上傳進(jìn)度百分比(0-100)
}

并發(fā)限流器(upload/limiter.ts)

核心目標(biāo):嚴(yán)格限制“所有文件的切片總并發(fā)”不超過上限;支持暫停時(shí)移除某文件的待執(zhí)行項(xiàng)。

/**
 * 并發(fā)限制器,用于控制同時(shí)執(zhí)行的任務(wù)數(shù)量。
 * 當(dāng)達(dá)到最大并發(fā)數(shù)時(shí),新任務(wù)會(huì)進(jìn)入隊(duì)列等待。
 */
export class ConcurrencyLimiter {
  // 最大并發(fā)任務(wù)數(shù)
  private maxConcurrent: number;
  // 當(dāng)前正在執(zhí)行的任務(wù)數(shù)
  private activeCount = 0;
  // 等待隊(duì)列,存儲(chǔ)尚未執(zhí)行的任務(wù)
  private pendingQueue: Array<{
    runTask: () => Promise<unknown>; // 任務(wù)執(zhí)行函數(shù)(返回 Promise)
    resolve: (v: unknown) => void;   // 任務(wù)成功時(shí)的回調(diào)
    reject: (e: unknown) => void;    // 任務(wù)失敗時(shí)的回調(diào)
    fileId: string;                  // 任務(wù)關(guān)聯(lián)的文件ID(用于取消)
  }> = [];

  constructor(maxConcurrent: number) {
    this.maxConcurrent = maxConcurrent;
  }

  /**
   * 將任務(wù)加入隊(duì)列,并返回一個(gè) Promise。
   * 當(dāng)任務(wù)開始執(zhí)行時(shí),Promise 會(huì)根據(jù)任務(wù)結(jié)果 resolve/reject。
   * @param runTask 待執(zhí)行的任務(wù)函數(shù)
   * @param fileId 關(guān)聯(lián)的文件ID(用于后續(xù)取消)
   */
  enqueue(runTask: () => Promise<unknown>, fileId: string) {
    return new Promise((resolve, reject) => {
      // 將任務(wù)推入等待隊(duì)列
      this.pendingQueue.push({ runTask, resolve, reject, fileId });
      // 嘗試啟動(dòng)下一個(gè)任務(wù)(如果未達(dá)到并發(fā)上限)
      this.tryStartNext();
    });
  }

  /**
   * 根據(jù)文件ID移除隊(duì)列中的待執(zhí)行任務(wù)(例如取消上傳)。
   * @param fileId 要移除的任務(wù)關(guān)聯(lián)的文件ID
   */
  removePendingByFileId(fileId: string) {
    this.pendingQueue = this.pendingQueue.filter(item => item.fileId !== fileId);
  }

  /**
   * 嘗試從隊(duì)列中啟動(dòng)下一個(gè)任務(wù)(如果未達(dá)到并發(fā)上限)。
   */
  private tryStartNext() {
    // 只要當(dāng)前活躍任務(wù)數(shù)未達(dá)上限且隊(duì)列非空,就繼續(xù)啟動(dòng)任務(wù)
    while (this.activeCount < this.maxConcurrent && this.pendingQueue.length > 0) {
      const next = this.pendingQueue.shift()!; // 從隊(duì)列頭部取出任務(wù)
      this.activeCount++; // 增加活躍任務(wù)計(jì)數(shù)

      // 立即執(zhí)行任務(wù)(包裝在 Promise.resolve 中以捕獲同步錯(cuò)誤)
      Promise.resolve()
        .then(() => next.runTask()) // 執(zhí)行任務(wù)函數(shù)
        .then(res => {
          this.activeCount--;       // 任務(wù)完成,減少活躍計(jì)數(shù)
          next.resolve(res);        // 通知外部任務(wù)成功
          this.tryStartNext();      // 遞歸檢查是否能啟動(dòng)新任務(wù)
        })
        .catch(err => {
          this.activeCount--;       // 任務(wù)失敗,減少活躍計(jì)數(shù)
          next.reject(err);         // 通知外部任務(wù)失敗
          this.tryStartNext();      // 遞歸檢查是否能啟動(dòng)新任務(wù)
        });
    }
  }
}

API 層(api/index.ts)

根據(jù)你服務(wù)端接口適配。示例包含:checkFile、uploadChunk、mergeChunk。axios 支持 signal 取消。

import axios from 'axios';

// 秒傳/斷點(diǎn)續(xù)傳 預(yù)檢查
export async function checkFile(params: {
  fileHash: string;
  fileName: string;
}) {
}

// 上傳切片
export async function uploadChunk(data: FormData, signal: AbortSignal) {
}

// 合并切片
export async function mergeChunk(params: {
  fileHash: string;
  fileName: string;
  chunkSize: number;
}) {
}

Web Worker:切片 + 文件 Hash(worker/hash-worker.js)

  • 切片:Blob.slice 按 chunkSize 切分
  • 文件 Hash:SparkMD5(按切片依次讀取并 append)
  • 返回:fileHash + fileChunkList
// worker/hash-worker.js
self.importScripts('https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js');

/**
 * 將文件切割為多個(gè)分片
 * @param {File} file - 待分片的文件對(duì)象
 * @param {number} chunkSize - 每個(gè)分片的大?。ㄗ止?jié))
 * @returns {Array<{blob: Blob, size: number}>} 分片數(shù)組,包含每個(gè)分片的Blob對(duì)象和大小
 */
function createFileChunks(file, chunkSize) {
  const chunks = [];
  let offset = 0; // 當(dāng)前分片的起始字節(jié)位置
  
  // 循環(huán)切割直到覆蓋整個(gè)文件
  while (offset < file.size) {
    // 使用slice方法切割文件(兼容大文件)
    const blob = file.slice(offset, offset + chunkSize);
    chunks.push({ 
      blob,          // 分片二進(jìn)制數(shù)據(jù)
      size: blob.size // 記錄分片實(shí)際大小(最后一片可能小于chunkSize)
    });
    offset += chunkSize; // 移動(dòng)切割位置
  }
  return chunks;
}

/**
 * 計(jì)算文件哈希(基于所有分片的增量計(jì)算)
 * @param {Array<{blob: Blob}>} chunks - 分片數(shù)組
 * @returns {Promise<string>} 文件的MD5哈希值
 */
async function calcFileHash(chunks) {
  // 使用SparkMD5庫(kù)進(jìn)行增量哈希計(jì)算
  const spark = new self.SparkMD5.ArrayBuffer();
  let processedCount = 0; // 已處理分片計(jì)數(shù)

  for (let i = 0; i < chunks.length; i++) {
    const { blob } = chunks[i];
    // 將Blob轉(zhuǎn)換為ArrayBuffer進(jìn)行哈希計(jì)算
    const buf = await blob.arrayBuffer();
    spark.append(buf); // 增量更新哈希
    processedCount++;

    // 計(jì)算并上報(bào)當(dāng)前進(jìn)度
    const percentage = Math.round((processedCount / chunks.length) * 100);
    self.postMessage({ 
      type: 'progress', 
      percentage       // 進(jìn)度百分比(0-100)
    });
  }
  return spark.end(); // 返回最終哈希值
}

// WebWorker消息處理器
self.onmessage = async (e) => {
  const { file, chunkSize } = e.data; // 從主線程接收的參數(shù)
  
  try {
    // 1. 文件分片
    const fileChunkList = createFileChunks(file, chunkSize);
    
    // 2. 計(jì)算文件哈希(包含進(jìn)度上報(bào))
    const fileHash = await calcFileHash(fileChunkList);
    
    // 處理成功,返回結(jié)果給主線程
    self.postMessage({ 
      type: 'done',
      fileHash,        // 文件完整哈希值
      fileChunkList    // 分片結(jié)果數(shù)組
    });
    self.close();      // 關(guān)閉Worker
  } catch (err) {
    // 錯(cuò)誤處理
    self.postMessage({ 
      type: 'error', 
      error: String(err) // 錯(cuò)誤信息轉(zhuǎn)字符串
    });
    self.close();
  }
};

Vue3 視圖與調(diào)度(views/Uploader.vue)

選擇文件 → 準(zhǔn)備任務(wù) → 秒傳/斷點(diǎn)續(xù)傳 → 全局并發(fā)上傳 → 合并

<template>
  <!-- 上傳器主界面 -->
  <div class="uploader">
    <!-- 文件選擇區(qū)域 -->
    <label class="btn">
      選擇文件
      <input type="file" multiple @change="onSelectFiles" hidden />
    </label>

    <!-- 上傳控制參數(shù) -->
    <div class="controls">
      <label>
        切片大小(MB):
        <input type="number" v-model.number="chunkSizeMB" min="1" max="16" />
      </label>
      <label>
        全局并發(fā):
        <input type="number" v-model.number="maxGlobalConcurrency" min="1" max="12" />
      </label>
    </div>

    <!-- 全局進(jìn)度顯示 -->
    <div class="global-progress">
      全局進(jìn)度:{{ globalProgress.percent }}%({{ formatBytes(globalProgress.uploadedBytes) }} / {{ formatBytes(globalProgress.totalBytes) }})
    </div>

    <!-- 文件列表展示 -->
    <ul class="file-list">
      <li v-for="f in fileTasks" :key="f.fileId">
        <div class="row">
          <div class="name">{{ f.fileName }}</div>
          <div class="state">{{ f.state }}</div>
          <div class="progress">
            {{ f.percent }}%({{ formatBytes(f.uploadedBytes) }} / {{ formatBytes(f.totalBytes) }})
          </div>
          <div class="actions">
            <button v-if="f.state !== 'paused' && f.state !== 'completed'" @click="pauseFileUpload(f.fileId)">暫停</button>
            <button v-if="f.state === 'paused'" @click="resumeFileUpload(f.fileId)">恢復(fù)</button>
          </div>
        </div>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { checkFile, uploadChunk as apiUploadChunk, mergeChunk } from '@/api';
import { ConcurrencyLimiter } from '@/upload/limiter';
import type { UploadFileTask, UploadChunkTask, GlobalProgress } from '@/upload/types';

// 配置參數(shù)
const DEFAULT_CHUNK_SIZE_MB = 1; // 默認(rèn)切片大小1MB
const DEFAULT_MAX_CONCURRENCY = 6; // 默認(rèn)并發(fā)數(shù)6

const chunkSizeMB = ref<number>(DEFAULT_CHUNK_SIZE_MB);
const maxGlobalConcurrency = ref<number>(DEFAULT_MAX_CONCURRENCY);

// 狀態(tài)管理
const fileTasks = reactive<UploadFileTask[]>([]);
const globalProgress = reactive<GlobalProgress>({ 
  uploadedBytes: 0, 
  totalBytes: 0, 
  percent: 0 
});

// 并發(fā)控制(建議使用工廠模式動(dòng)態(tài)重建)
const limiter = new ConcurrencyLimiter(maxGlobalConcurrency.value);

/* 核心方法 */

/**
 * 文件選擇處理
 * 1. 獲取文件列表
 * 2. 為每個(gè)文件創(chuàng)建上傳任務(wù)
 */
async function onSelectFiles(e: Event) {
  const input = e.target as HTMLInputElement;
  if (!input?.files?.length) return;

  // 并行處理多個(gè)文件(注意瀏覽器并發(fā)限制)
  await Promise.all(
    Array.from(input.files).map(file => 
      prepareAndStartFileTask(file)
    )
  );
  
  input.value = ''; // 重置input
}

/**
 * 文件上傳預(yù)處理(核心流程)
 * 1. 創(chuàng)建文件任務(wù)對(duì)象
 * 2. 切片+計(jì)算hash(Worker線程)
 * 3. 檢查秒傳/斷點(diǎn)續(xù)傳
 * 4. 啟動(dòng)上傳調(diào)度
 */
async function prepareAndStartFileTask(file: File) {
  // 生成唯一文件ID(實(shí)際項(xiàng)目建議使用更可靠的生成方式)
  const fileId = `${file.name}-${file.size}-${Date.now()}`;
  const chunkSize = Math.max(1, chunkSizeMB.value) * 1024 * 1024;

  // 初始化文件任務(wù)(響應(yīng)式對(duì)象)
  const task: UploadFileTask = reactive({
    fileId,
    fileName: file.name,
    fileSize: file.size,
    fileHash: '',
    chunkSize,
    totalChunks: 0,
    chunkTasks: [],
    state: 'idle',
    isPaused: false,
    inflightChunks: new Set(),
    uploadedBytes: 0,
    totalBytes: file.size,
    percent: 0
  });

  fileTasks.push(task); // 立即加入列表顯示

  try {
    // Step 1: 文件切片+計(jì)算hash(Worker線程)
    const { fileHash, chunkTasks } = await createChunksAndHashInWorker(file, chunkSize, fileId);
    task.fileHash = fileHash;
    task.chunkTasks = chunkTasks.map(ct => reactive(ct)); // 轉(zhuǎn)為響應(yīng)式
    task.totalChunks = chunkTasks.length;

    // Step 2: 秒傳/斷點(diǎn)檢查
    const check = await checkFile({ 
      fileHash: `${fileHash}${file.name}`, 
      fileName: file.name 
    });

    if (check?.code === 0) {
      const { shouldUpload, uploadedList = [] } = check.data || {};

      // 秒傳處理
      if (!shouldUpload) {
        completeFileTask(task);
        return;
      }

      // 斷點(diǎn)續(xù)傳:過濾已上傳切片
      if (uploadedList.length > 0) {
        task.chunkTasks = task.chunkTasks.filter(ct => 
          !uploadedList.includes(ct.chunkHash)
        );
        
        // 所有切片已上傳,嘗試合并
        if (task.chunkTasks.length === 0) {
          await tryMerge(task);
          return;
        }
      }
    }

    // Step 3: 啟動(dòng)上傳
    task.state = 'uploading';
    scheduleAllChunks(task);

  } catch (err) {
    console.error('文件預(yù)處理失敗:', file.name, err);
    task.state = 'failed';
  }
}

/**
 * Web Worker 通信封裝
 * 職責(zé):文件切片 + 計(jì)算MD5
 */
function createChunksAndHashInWorker(file: File, chunkSize: number, fileId: string) {
  return new Promise<{ fileHash: string; chunkTasks: UploadChunkTask[] }>((resolve, reject) => {
    const worker = new Worker(new URL('@/worker/hash-worker.js', import.meta.url));
    
    worker.postMessage({ file, chunkSize });
    
    worker.onmessage = (e: MessageEvent) => {
      const { type } = e.data || {};
      
      if (type === 'progress') {
        // 可在此處更新hash計(jì)算進(jìn)度
      } 
      else if (type === 'done') {
        const { fileHash, fileChunkList } = e.data;
        
        // 構(gòu)建分片任務(wù)數(shù)組
        const chunkTasks: UploadChunkTask[] = fileChunkList.map((c: any, index: number) => ({
          fileId,
          chunkId: `${fileId}-${index}`,
          chunkIndex: index,
          size: c.size,
          blob: c.blob,
          chunkHash: `${fileHash}-${index}`,
          uploadedBytes: 0,
          attemptCount: 0,
          maxAttempts: 3,
          abortController: new AbortController(),
          state: 'idle'
        }));
        
        resolve({ fileHash, chunkTasks });
        worker.terminate();
      } 
      else if (type === 'error') {
        reject(new Error(e.data?.error || 'hash計(jì)算失敗'));
        worker.terminate();
      }
    };
  });
}

/**
 * 調(diào)度文件的所有分片上傳
 * 注意:實(shí)際項(xiàng)目建議使用全局交錯(cuò)調(diào)度(buildInterleavedChunks)
 */
function scheduleAllChunks(fileTask: UploadFileTask) {
  fileTask.chunkTasks
    .filter(ct => ct.state !== 'completed')
    .forEach(ct => enqueueChunkUpload(fileTask, ct));
  
  updateGlobalTotals();
}

/**
 * 分片上傳任務(wù)封裝
 * 1. 加入并發(fā)隊(duì)列
 * 2. 處理重試邏輯
 * 3. 完成檢查
 */
function enqueueChunkUpload(fileTask: UploadFileTask, chunkTask: UploadChunkTask) {
  // 標(biāo)記為進(jìn)行中
  fileTask.inflightChunks.add(chunkTask);
  
  limiter.enqueue(
    async () => {
      try {
        await runWithRetry(
          () => uploadOneChunk(fileTask, chunkTask),
          chunkTask
        );
      } finally {
        fileTask.inflightChunks.delete(chunkTask);
      }
    },
    fileTask.fileId
  ).then(async () => {
    // 檢查文件是否全部完成
    if (isFileUploadComplete(fileTask)) {
      await tryMerge(fileTask);
    }
  }).catch(err => {
    console.error('分片上傳失敗:', chunkTask.chunkId, err);
  });
}

/**
 * 單個(gè)分片上傳實(shí)現(xiàn)
 * 關(guān)鍵點(diǎn):
 * - 支持取消(AbortController)
 * - 進(jìn)度上報(bào)(實(shí)際項(xiàng)目需實(shí)現(xiàn))
 */
async function uploadOneChunk(fileTask: UploadFileTask, chunkTask: UploadChunkTask) {
  chunkTask.state = 'uploading';

  const formData = new FormData();
  formData.append('fileHash', `${fileTask.fileHash}${fileTask.fileName}`);
  formData.append('fileName', fileTask.fileName);
  formData.append('index', String(chunkTask.chunkIndex));
  formData.append('chunkFile', chunkTask.blob);
  formData.append('chunkHash', chunkTask.chunkHash);
  formData.append('chunkSize', String(fileTask.chunkSize));
  formData.append('chunkNumber', String(fileTask.totalChunks));

  // 執(zhí)行上傳(帶取消支持)
  await apiUploadChunk(formData, chunkTask.abortController.signal);
  
  // 更新狀態(tài)
  chunkTask.uploadedBytes = chunkTask.size;
  chunkTask.state = 'completed';
  
  // 更新進(jìn)度
  updateFileProgress(fileTask);
  updateGlobalProgress();
}

/* 上傳控制方法 */

// 暫停上傳
function pauseFileUpload(fileId: string) {
  const fileTask = fileTasks.find(f => f.fileId === fileId);
  if (!fileTask || fileTask.state === 'completed') return;

  fileTask.isPaused = true;
  fileTask.state = 'paused';

  // 取消隊(duì)列中的任務(wù)
  limiter.removePendingByFileId(fileId);

  // 中止進(jìn)行中的上傳
  fileTask.inflightChunks.forEach(ct => {
    ct.abortController.abort();
  });
}

// 恢復(fù)上傳
function resumeFileUpload(fileId: string) {
  const fileTask = fileTasks.find(f => f.fileId === fileId);
  if (!fileTask) return;

  fileTask.isPaused = false;
  fileTask.state = 'uploading';

  // 重置未完成切片的狀態(tài)
  fileTask.chunkTasks
    .filter(ct => ct.state !== 'completed')
    .forEach(ct => {
      ct.abortController = new AbortController();
      enqueueChunkUpload(fileTask, ct);
    });
}

/* 工具方法 */

// 帶重試的執(zhí)行(指數(shù)退避)
async function runWithRetry(
  taskFn: () => Promise<unknown>,
  chunkTask: UploadChunkTask,
  maxAttempts = 3,
  baseDelay = 500
) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    chunkTask.attemptCount = attempt;
    
    try {
      return await taskFn();
    } catch (err: any) {
      // 主動(dòng)取消不重試
      if (chunkTask.abortController.signal.aborted) throw err;
      
      // 最后一次嘗試失敗
      if (attempt === maxAttempts) {
        chunkTask.state = 'failed';
        throw err;
      }
      
      // 延遲重試
      await sleep(baseDelay * Math.pow(2, attempt - 1));
    }
  }
}

// 文件上傳完成檢查
function isFileUploadComplete(fileTask: UploadFileTask) {
  return fileTask.chunkTasks.every(c => c.state === 'completed') && 
         fileTask.state !== 'paused';
}

// 合并文件請(qǐng)求
async function tryMerge(fileTask: UploadFileTask) {
  const res = await mergeChunk({
    fileHash: fileTask.fileHash,
    fileName: fileTask.fileName,
    chunkSize: fileTask.chunkSize
  }).catch(() => null);

  if (res?.code === 0) {
    completeFileTask(fileTask);
  } else {
    fileTask.state = 'failed';
  }
}

// 更新文件進(jìn)度
function updateFileProgress(fileTask: UploadFileTask) {
  const uploaded = fileTask.chunkTasks.reduce((s, c) => s + c.uploadedBytes, 0);
  fileTask.uploadedBytes = uploaded;
  fileTask.percent = Math.round((uploaded / fileTask.totalBytes) * 100);
}

// 更新全局統(tǒng)計(jì)
function updateGlobalTotals() {
  globalProgress.totalBytes = fileTasks.reduce((s, f) => s + f.totalBytes, 0);
}

function updateGlobalProgress() {
  globalProgress.uploadedBytes = fileTasks.reduce((s, f) => s + f.uploadedBytes, 0);
  globalProgress.percent = Math.round(
    (globalProgress.uploadedBytes / globalProgress.totalBytes) * 100
  );
}

// 標(biāo)記文件上傳完成
function completeFileTask(fileTask: UploadFileTask) {
  fileTask.state = 'completed';
  fileTask.percent = 100;
  fileTask.uploadedBytes = fileTask.totalBytes;
  updateGlobalProgress();
}

/* 輔助工具 */
function sleep(ms: number) { 
  return new Promise(res => setTimeout(res, ms)); 
}

function formatBytes(n: number) {
  if (!n) return '0 B';
  const units = ['B','KB','MB','GB','TB']; 
  const i = Math.floor(Math.log(n)/Math.log(1024));
  return `${(n/Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
}
</script>

關(guān)鍵點(diǎn)詳解

  1. 全局并發(fā)控制優(yōu)先于“按文件分配”
    將所有切片視為統(tǒng)一資源池,由 ConcurrencyLimiter 控制啟動(dòng)。
    防止多文件時(shí)各自“自增并發(fā)”導(dǎo)致總并發(fā)超標(biāo)。
    通過交錯(cuò)入隊(duì)(interleaved)實(shí)現(xiàn)公平性,避免某個(gè)大文件占滿資源。
  2. 進(jìn)度統(tǒng)計(jì)
    切片 onUploadProgress 事件能獲得 e.loaded,但 axios 對(duì)同一請(qǐng)求是累積的,簡(jiǎn)化起見以“完成即視為 size”統(tǒng)計(jì)也可滿足絕大多數(shù) UI 需求。
    若需更精確的實(shí)時(shí)數(shù)值,可在 api 層把 e.loaded 透?jìng)魃蠈樱瑢?duì) chunkTask.uploadedBytes 動(dòng)態(tài)賦值并觸發(fā) updateFileProgress / updateGlobalProgress。
  3. 斷點(diǎn)續(xù)傳與秒傳
    chunkHash 建議采用 f i l e H a s h − {fileHash}- fileHash{chunkIndex},服務(wù)端據(jù)此判斷某切片是否已存在。
    文件 Hash 可用 SparkMD5;若需更強(qiáng),考慮 SHA-256(但更耗時(shí))。
    秒傳:checkFile 返回 shouldUpload=false 直接完成。
  4. 暫停 / 恢復(fù)
    暫停:removePendingByFileId + abort inflight,一次到位。
    恢復(fù):為未完成切片重建 AbortController 并重新入隊(duì)。
    注意暫停后不要丟失切片狀態(tài),resume 時(shí)應(yīng)按 chunkIndex 順序重排可選。
  5. 錯(cuò)誤重試
    建議指數(shù)退避:500ms, 1s, 2s…
    區(qū)分可重試(網(wǎng)絡(luò)錯(cuò)誤、5xx、超時(shí))與不可重試(4xx 參數(shù)錯(cuò)誤、鑒權(quán)失?。?br />超過最大重試次數(shù)可標(biāo)記文件 failed 并提示用戶。
  6. 配置與兼容
    HTTP/2 多路復(fù)用下“同域6并發(fā)”限制不再剛性,但客戶端/服務(wù)端處理能力仍有限,maxGlobalConcurrency 保持可配置。
    Safari 對(duì) fetch 上傳進(jìn)度支持欠佳,推薦 axios/XHR。
    chunkSize 一般 1–4MB 權(quán)衡較好;過小請(qǐng)求數(shù)過多,過大重試成本高。
    服務(wù)端對(duì)接要點(diǎn)(簡(jiǎn)述)
    checkFile(fileHash, fileName)
    返回 shouldUpload 和已存在的 uploadedList(切片 hash 列表)。
    upload 接口
    校驗(yàn)必要字段:fileHash、fileName、index、chunkHash、chunkSize、chunkNumber。
    存儲(chǔ)到臨時(shí)目錄:/upload_tmp/{fileHash}/{index}
    merge 接口
    校驗(yàn)切片數(shù)量完整性與哈希一致性(可選)。
    合并為最終文件并清理臨時(shí)目錄。
    冪等:重復(fù)合并時(shí)應(yīng)安全返回成功。

小結(jié)

本文給出了一個(gè)基于 Vue3 的大文件上傳完整實(shí)現(xiàn)方案,重點(diǎn)在于:

  • 使用 Web Worker 進(jìn)行切片和文件 Hash 計(jì)算,主線程不卡頓;
  • 通過全局并發(fā)限流器 ConcurrencyLimiter 嚴(yán)格控制總并發(fā),支持暫停/恢復(fù),且多文件之間相對(duì)公平;
  • 利用秒傳與斷點(diǎn)續(xù)傳節(jié)省帶寬與時(shí)間;
  • 細(xì)粒度進(jìn)度統(tǒng)計(jì)與指數(shù)退避重試,提升穩(wěn)定性與用戶體驗(yàn)。

到此這篇關(guān)于Vue3實(shí)現(xiàn)大文件上傳:切片上傳 / 斷點(diǎn)續(xù)傳 / 秒傳 / 暫?;謴?fù) / 全局并發(fā)控制的文章就介紹到這了,更多相關(guān)Vue3實(shí)現(xiàn)大文件上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • vue實(shí)現(xiàn)禁止瀏覽器記住密碼功能的示例代碼

    vue實(shí)現(xiàn)禁止瀏覽器記住密碼功能的示例代碼

    這篇文章主要介紹了vue實(shí)現(xiàn)禁止瀏覽器記住密碼功能的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-02-02
  • vue項(xiàng)目使用jszip和file-saver批量打包壓縮圖片或附件方式

    vue項(xiàng)目使用jszip和file-saver批量打包壓縮圖片或附件方式

    這篇文章主要介紹了vue項(xiàng)目使用jszip和file-saver批量打包壓縮圖片或附件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • VUE腳手架的下載和配置步驟詳解

    VUE腳手架的下載和配置步驟詳解

    這篇文章主要介紹了VUE腳手架下載和配置,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-04-04
  • Vue無法對(duì)iframe進(jìn)行緩存的解決方案

    Vue無法對(duì)iframe進(jìn)行緩存的解決方案

    項(xiàng)目采用的若依框架,但系統(tǒng)中會(huì)嵌入大屏、報(bào)表頁(yè)面,是使用iframe來實(shí)現(xiàn)的,若依框架的菜單管理中提供了緩存功能,是使用keep-alive實(shí)現(xiàn)的,但對(duì)于iframe頁(yè)面并不生效,所以本文介紹了關(guān)于Vue無法對(duì)iframe進(jìn)行緩存的解決方案記錄,需要的朋友可以參考下
    2024-12-12
  • Vue?監(jiān)視屬性之天氣案例實(shí)現(xiàn)

    Vue?監(jiān)視屬性之天氣案例實(shí)現(xiàn)

    這篇文章主要介紹了Vue?監(jiān)視屬性之天氣案例實(shí)現(xiàn),文章以天氣為例展開介紹Vue?監(jiān)視屬性?的相關(guān)內(nèi)容,需要的小伙伴可以參考一下
    2022-05-05
  • VUE簽字組件vue-esign安裝使用教程

    VUE簽字組件vue-esign安裝使用教程

    在我們開發(fā)項(xiàng)目中,特別是流程審批類的項(xiàng)目,最后一步會(huì)提交審核,審核員看完相應(yīng)信息以后,沒問題就會(huì)簽字通過審批,這篇文章主要給大家介紹了關(guān)于VUE簽字組件vue-esign安裝使用的相關(guān)資料,需要的朋友可以參考下
    2023-09-09
  • vue中使用rem布局的兩種方法小結(jié)

    vue中使用rem布局的兩種方法小結(jié)

    這篇文章主要介紹了vue中使用rem布局的兩種方法小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07
  • 深入Vue-Router路由嵌套理解

    深入Vue-Router路由嵌套理解

    這篇文章主要介紹了深入Vue-Router路由嵌套理解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-08-08
  • vue項(xiàng)目實(shí)現(xiàn)圖形驗(yàn)證碼

    vue項(xiàng)目實(shí)現(xiàn)圖形驗(yàn)證碼

    這篇文章主要為大家詳細(xì)介紹了vue項(xiàng)目實(shí)現(xiàn)圖形驗(yàn)證碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-04-04
  • vue中el-table實(shí)現(xiàn)可拖拽移動(dòng)列和動(dòng)態(tài)排序字段

    vue中el-table實(shí)現(xiàn)可拖拽移動(dòng)列和動(dòng)態(tài)排序字段

    最近公司需要做個(gè)項(xiàng)目,需要拖拽表格和自定義表格字段,本文主要介紹了vue中el-table實(shí)現(xiàn)可拖拽移動(dòng)列和動(dòng)態(tài)排序字段,具有一定吃參考價(jià)值,感興趣的可以了解一下
    2023-12-12

最新評(píng)論

桂东县| 望都县| 土默特左旗| 山阳县| 阆中市| 泽库县| 德格县| 富平县| 通城县| 安徽省| 阿拉善右旗| 西吉县| 泰兴市| 八宿县| 高要市| 泊头市| 巨鹿县| 元阳县| 尤溪县| 牙克石市| 克什克腾旗| 格尔木市| 大宁县| 中山市| 临泉县| 彰化县| 公主岭市| 延长县| 太仆寺旗| 温州市| 商河县| 凤凰县| 合山市| 乌海市| 武乡县| 牟定县| 会东县| 晋宁县| 麻江县| 郁南县| 布拖县|