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

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

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

概述

本文基于 Vue3 實現一個大文件上傳方案,完整覆蓋以下核心能力:

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

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

技術架構

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

整體上傳流程圖

目錄結構建議

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

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

類型定義(upload/types.ts)

統(tǒng)一、清晰的命名有助于維護與協(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';

/**
 * 分片上傳任務信息
 */
export interface UploadChunkTask {
  fileId: string;                // 所屬文件ID
  chunkId: string;               // 分片唯一標識,格式如 `${fileId}-${chunkIndex}`
  chunkIndex: number;            // 分片序號(從0開始)
  size: number;                  // 分片大?。ㄗ止?jié))
  blob: Blob;                    // 分片二進制數據

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

/**
 * 文件上傳任務信息
 */
export interface UploadFileTask {
  fileId: string;                // 文件唯一標識
  fileName: string;              // 文件名
  fileSize: number;              // 文件總大小(字節(jié))
  fileHash: string;              // 文件哈希值(用于整體校驗)

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

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

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

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

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

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

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

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

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

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

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

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

API 層(api/index.ts)

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

import axios from 'axios';

// 秒傳/斷點續(xù)傳 預檢查
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');

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

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

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

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

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

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

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

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

    <!-- 上傳控制參數 -->
    <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>

    <!-- 全局進度顯示 -->
    <div class="global-progress">
      全局進度:{{ 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)">恢復</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';

// 配置參數
const DEFAULT_CHUNK_SIZE_MB = 1; // 默認切片大小1MB
const DEFAULT_MAX_CONCURRENCY = 6; // 默認并發(fā)數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ā)控制(建議使用工廠模式動態(tài)重建)
const limiter = new ConcurrencyLimiter(maxGlobalConcurrency.value);

/* 核心方法 */

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

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

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

  // 初始化文件任務(響應式對象)
  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: 文件切片+計算hash(Worker線程)
    const { fileHash, chunkTasks } = await createChunksAndHashInWorker(file, chunkSize, fileId);
    task.fileHash = fileHash;
    task.chunkTasks = chunkTasks.map(ct => reactive(ct)); // 轉為響應式
    task.totalChunks = chunkTasks.length;

    // Step 2: 秒傳/斷點檢查
    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;
      }

      // 斷點續(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: 啟動上傳
    task.state = 'uploading';
    scheduleAllChunks(task);

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

/**
 * Web Worker 通信封裝
 * 職責:文件切片 + 計算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計算進度
      } 
      else if (type === 'done') {
        const { fileHash, fileChunkList } = e.data;
        
        // 構建分片任務數組
        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計算失敗'));
        worker.terminate();
      }
    };
  });
}

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

/**
 * 分片上傳任務封裝
 * 1. 加入并發(fā)隊列
 * 2. 處理重試邏輯
 * 3. 完成檢查
 */
function enqueueChunkUpload(fileTask: UploadFileTask, chunkTask: UploadChunkTask) {
  // 標記為進行中
  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);
  });
}

/**
 * 單個分片上傳實現
 * 關鍵點:
 * - 支持取消(AbortController)
 * - 進度上報(實際項目需實現)
 */
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';
  
  // 更新進度
  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';

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

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

// 恢復上傳
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í)行(指數退避)
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) {
      // 主動取消不重試
      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';
}

// 合并文件請求
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';
  }
}

// 更新文件進度
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)計
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
  );
}

// 標記文件上傳完成
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>

關鍵點詳解

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

小結

本文給出了一個基于 Vue3 的大文件上傳完整實現方案,重點在于:

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

到此這篇關于Vue3實現大文件上傳:切片上傳 / 斷點續(xù)傳 / 秒傳 / 暫停恢復 / 全局并發(fā)控制的文章就介紹到這了,更多相關Vue3實現大文件上傳內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • vue實現禁止瀏覽器記住密碼功能的示例代碼

    vue實現禁止瀏覽器記住密碼功能的示例代碼

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

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

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

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

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

    Vue無法對iframe進行緩存的解決方案

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

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

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

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

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

    vue中使用rem布局的兩種方法小結

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

    深入Vue-Router路由嵌套理解

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

    vue項目實現圖形驗證碼

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

    vue中el-table實現可拖拽移動列和動態(tài)排序字段

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

最新評論

天长市| 岑溪市| 同仁县| 定陶县| 开阳县| 金川县| 南安市| 冕宁县| 岑巩县| 徐汇区| 桃江县| 江北区| 昌图县| 木兰县| 漳州市| 常宁市| 宁南县| 株洲县| 海丰县| 南充市| 米易县| 织金县| 桑日县| 高台县| 雷州市| 蒲江县| 岑溪市| 瑞安市| 沭阳县| 察雅县| 阳东县| 会同县| 台南市| 卫辉市| 霍邱县| 吕梁市| 筠连县| 铜陵市| 清苑县| 崇仁县| 平谷区|