欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

IndexedDB?實現(xiàn)斷點續(xù)傳、分片上傳功能

 更新時間:2025年06月21日 11:06:41   作者:菜喵007  
本文基于Vue3、TypeScript和Setup語法糖,實現(xiàn)文件斷點續(xù)傳功能,支持網(wǎng)絡(luò)中斷或瀏覽器關(guān)閉后從上次上傳位置繼續(xù)上傳,并在瀏覽器重新打開時通過用戶確認自動續(xù)傳,感興趣的朋友跟隨小編一起看看吧

IndexedDB 斷點續(xù)傳

本文基于 Vue3、TypeScript 和 Setup 語法糖,實現(xiàn)文件斷點續(xù)傳功能,支持網(wǎng)絡(luò)中斷或瀏覽器關(guān)閉后從上次上傳位置繼續(xù)上傳,并在瀏覽器重新打開時通過用戶確認自動續(xù)傳。使用 IndexedDB 存儲文件元數(shù)據(jù)和分片狀態(tài),確保上傳過程可靠,支持暫停/恢復以及跨瀏覽器會話的自動續(xù)傳。

1. 項目環(huán)境準備

1.1 技術(shù)棧

  • Vue3:使用 Composition API 和 Setup 語法糖。
  • TypeScript:提供類型安全。
  • IndexedDB:存儲文件元數(shù)據(jù)和分片狀態(tài)。
  • Vite:作為構(gòu)建工具。
  • Tailwind CSS:優(yōu)化界面樣式。

1.2 項目初始化

npm create vite@latest indexeddb-upload -- --template vue-ts
cd indexeddb-upload
npm install
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
npm run dev

1.3 配置 Tailwind CSS

在 src/style.css 中添加:

@tailwind base;
@tailwind components;
@tailwind utilities;

更新 vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
  plugins: [vue()],
  css: {
    postcss: {
      plugins: [require('tailwindcss'), require('autoprefixer')],
    },
  },
})

1.4 依賴

無額外運行時依賴,使用瀏覽器原生 IndexedDB API。

2. 大批量文件斷點續(xù)傳(支持自動續(xù)傳)

2.1 場景描述

斷點續(xù)傳允許用戶在網(wǎng)絡(luò)中斷或瀏覽器關(guān)閉后,從上次上傳位置繼續(xù)上傳。在瀏覽器重新打開時,系統(tǒng)檢測未完成上傳任務(wù),通過用戶確認后自動續(xù)傳。IndexedDB 存儲文件元數(shù)據(jù)(如文件名、大小、最后修改時間)和分片狀態(tài)(已上傳、待上傳)。

2.2 實現(xiàn)思路

  • 使用 IndexedDB 存儲文件元數(shù)據(jù)和分片狀態(tài)。
  • 頁面加載時,檢查 IndexedDB 中的未完成任務(wù),顯示確認界面。
  • 用戶確認后,驗證文件一致性并繼續(xù)上傳。
  • 使用 Vue3 響應式 API 管理狀態(tài)和進度。
  • 支持暫停/繼續(xù)功能,實時更新 UI。
  • TypeScript 確保類型安全。
  • 使用 Tailwind CSS 優(yōu)化界面。

2.3 完整示例代碼

2.3.1 主組件 (src/App.vue)

<template>
  <div class="p-6 max-w-2xl mx-auto">
    <h1 class="text-3xl font-bold mb-6">文件斷點續(xù)傳(支持自動續(xù)傳)</h1>
    <input
      type="file"
      ref="fileInput"
      @change="handleFileChange"
      class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
    />
    <div class="mt-6 flex space-x-4">
      <button
        class="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
        @click="startUpload"
        :disabled="isUploading"
      >
        開始上傳
      </button>
      <button
        class="bg-red-600 text-white px-6 py-2 rounded-md hover:bg-red-700 disabled:bg-gray-400"
        @click="pauseUpload"
        :disabled="!isUploading"
      >
        暫停上傳
      </button>
    </div>
    <div class="mt-6">
      <p class="text-lg">上傳進度: {{ progress }}%</p>
      <div class="w-full bg-gray-200 rounded-full h-4 mt-2">
        <div
          class="bg-blue-600 h-4 rounded-full"
          :style="{ width: `${progress}%` }"
        ></div>
      </div>
    </div>
    <p v-if="autoUploading" class="text-green-600 mt-4">
      檢測到未完成任務(wù),正在自動續(xù)傳 {{ fileName }}...
    </p>
    <div
      v-if="pendingFile"
      class="mt-4 p-4 bg-yellow-100 border border-yellow-400 rounded-md"
    >
      <p>檢測到未完成的文件:{{ pendingFile.fileName }} ({{ formatSize(pendingFile.fileSize) }})</p>
      <p>上次修改時間:{{ new Date(pendingFile.lastModified).toLocaleString() }}</p>
      <div class="mt-4 flex space-x-4">
        <button
          class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700"
          @click="confirmResume"
        >
          繼續(xù)上傳
        </button>
        <button
          class="bg-gray-600 text-white px-4 py-2 rounded-md hover:bg-gray-700"
          @click="cancelResume"
        >
          取消
        </button>
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { initDB, saveChunkStatus, getChunkStatus, uploadChunk, getPendingFile, clearDB } from './utils/upload';
const CHUNK_SIZE = 1024 * 1024; // 1MB
const fileInput = ref<HTMLInputElement | null>(null);
const selectedFile = ref<File | null>(null);
const isUploading = ref(false);
const isPaused = ref(false);
const autoUploading = ref(false);
const uploadedChunks = ref(new Set<number>());
const totalChunks = ref(0);
const fileName = ref('');
const db = ref<IDBDatabase | null>(null);
const pendingFile = ref<FileMetadata | null>(null);
const progress = computed(() =>
  totalChunks.value ? ((uploadedChunks.value.size / totalChunks.value) * 100).toFixed(2) : '0'
);
const formatSize = (bytes: number): string => {
  const units = ['B', 'KB', 'MB', 'GB'];
  let size = bytes;
  let unitIndex = 0;
  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex++;
  }
  return `${size.toFixed(2)} ${units[unitIndex]}`;
};
const handleFileChange = async (event: Event) => {
  const input = event.target as HTMLInputElement;
  if (input.files?.length) {
    const file = input.files[0];
    // 驗證文件一致性
    if (pendingFile.value && (file.name !== pendingFile.value.fileName || file.size !== pendingFile.value.fileSize || file.lastModified !== pendingFile.value.lastModified)) {
      alert('所選文件與未完成任務(wù)不匹配,請取消未完成任務(wù)或選擇正確文件');
      input.value = '';
      return;
    }
    selectedFile.value = file;
    fileName.value = file.name;
    totalChunks.value = Math.ceil(file.size / CHUNK_SIZE);
    uploadedChunks.value.clear();
    pendingFile.value = null;
    await saveFileMetadata();
  }
};
const saveFileMetadata = async () => {
  if (!db.value || !selectedFile.value) return;
  const transaction = db.value.transaction(['metadata'], 'readwrite');
  const store = transaction.objectStore('metadata');
  const metadata: FileMetadata = {
    fileName: selectedFile.value.name,
    fileSize: selectedFile.value.size,
    totalChunks: totalChunks.value,
    lastModified: selectedFile.value.lastModified,
  };
  await new Promise((resolve, reject) => {
    const request = store.put(metadata);
    request.onsuccess = () => resolve(undefined);
    request.onerror = () => reject(request.error);
  });
};
const startUpload = async (auto = false) => {
  if (!selectedFile.value && !auto) {
    alert('請選擇文件');
    return;
  }
  isUploading.value = true;
  isPaused.value = false;
  if (auto) autoUploading.value = true;
  if (!db.value) {
    db.value = await initDB('FileUploadDB', 1, (db) => {
      db.createObjectStore('chunks', { keyPath: 'chunkId' });
      db.createObjectStore('metadata', { keyPath: 'fileName' });
    });
  }
  try {
    for (let i = 0; i < totalChunks.value; i++) {
      if (isPaused.value) break;
      const chunkStatus = await getChunkStatus(db.value, i);
      if (chunkStatus?.status === 'uploaded') {
        uploadedChunks.value.add(i);
        continue;
      }
      const start = i * CHUNK_SIZE;
      const end = Math.min(start + CHUNK_SIZE, selectedFile.value!.size);
      const chunk = selectedFile.value!.slice(start, end);
      await uploadChunk(db.value, chunk, i, fileName.value, totalChunks.value);
      uploadedChunks.value.add(i);
    }
    if (!isPaused.value) {
      await clearDB(db.value);
      alert('上傳完成');
      resetState();
    }
  } catch (error) {
    alert(`上傳失敗: ${error instanceof Error ? error.message : '未知錯誤'}`);
    isUploading.value = false;
    autoUploading.value = false;
  }
};
const pauseUpload = () => {
  isPaused.value = true;
  isUploading.value = false;
  autoUploading.value = false;
  alert('上傳已暫停,可重新點擊“開始上傳”繼續(xù)');
};
const confirmResume = async () => {
  if (!fileInput.value!.files?.length) {
    alert('請重新選擇文件以繼續(xù)上傳');
    return;
  }
  const file = fileInput.value!.files[0];
  if (
    file.name !== pendingFile.value!.fileName ||
    file.size !== pendingFile.value!.fileSize ||
    file.lastModified !== pendingFile.value!.lastModified
  ) {
    alert('所選文件與未完成任務(wù)不匹配');
    return;
  }
  selectedFile.value = file;
  fileName.value = file.name;
  totalChunks.value = pendingFile.value!.totalChunks;
  pendingFile.value = null;
  await startUpload(true);
};
const cancelResume = async () => {
  await clearDB(db.value!);
  pendingFile.value = null;
  resetState();
  alert('已取消未完成任務(wù)');
};
const resetState = () => {
  isUploading.value = false;
  autoUploading.value = false;
  selectedFile.value = null;
  fileName.value = '';
  totalChunks.value = 0;
  uploadedChunks.value.clear();
  if (fileInput.value) fileInput.value.value = '';
};
onMounted(async () => {
  if (!window.indexedDB) {
    alert('瀏覽器不支持 IndexedDB');
    return;
  }
  try {
    db.value = await initDB('FileUploadDB', 1, (db) => {
      db.createObjectStore('chunks', { keyPath: 'chunkId' });
      db.createObjectStore('metadata', { keyPath: 'fileName' });
    });
    const pending = await getPendingFile(db.value);
    if (pending) {
      pendingFile.value = pending;
    }
  } catch (error) {
    alert(`初始化數(shù)據(jù)庫失敗: ${error instanceof Error ? error.message : '未知錯誤'}`);
  }
});
</script>

2.3.2 工具函數(shù) (src/utils/upload.ts)

export interface ChunkStatus {
  chunkId: number;
  fileName: string;
  status: 'pending' | 'uploaded';
}
export interface FileMetadata {
  fileName: string;
  fileSize: number;
  totalChunks: number;
  lastModified: number;
}
export const initDB = (
  dbName: string,
  version: number,
  onUpgrade: (db: IDBDatabase) => void
): Promise<IDBDatabase> => {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(dbName, version);
    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result;
      onUpgrade(db);
    };
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
};
export const saveChunkStatus = (
  db: IDBDatabase,
  chunkId: number,
  fileName: string,
  status: 'pending' | 'uploaded'
): Promise<void> => {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['chunks'], 'readwrite');
    const store = transaction.objectStore('chunks');
    const request = store.put({ chunkId, fileName, status });
    request.onsuccess = () => resolve();
    request.onerror = () => reject(request.error);
  });
};
export const getChunkStatus = (db: IDBDatabase, chunkId: number): Promise<ChunkStatus | undefined> => {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['chunks'], 'readonly');
    const store = transaction.objectStore('chunks');
    const request = store.get(chunkId);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
};
export const getPendingFile = (db: IDBDatabase): Promise<FileMetadata | undefined> => {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['metadata'], 'readonly');
    const store = transaction.objectStore('metadata');
    const request = store.getAll();
    request.onsuccess = () => {
      const files = request.result as FileMetadata[];
      resolve(files.length > 0 ? files[0] : undefined);
    };
    request.onerror = () => reject(request.error);
  });
};
export const clearDB = async (db: IDBDatabase): Promise<void> => {
  const stores = ['chunks', 'metadata'];
  for (const storeName of stores) {
    const transaction = db.transaction([storeName], 'readwrite');
    const store = transaction.objectStore(storeName);
    await new Promise((resolve, reject) => {
      const request = store.clear();
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }
};
export const uploadChunk = async (
  db: IDBDatabase,
  chunk: Blob,
  chunkId: number,
  fileName: string,
  totalChunks: number
): Promise<void> => {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('chunkId', chunkId.toString());
  formData.append('fileName', fileName);
  try {
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData,
    });
    if (!response.ok) {
      throw new Error(`上傳失敗,狀態(tài)碼: ${response.status}`);
    }
    await saveChunkStatus(db, chunkId, fileName, 'uploaded');
  } catch (error) {
    console.error(`分片 ${chunkId} 上傳失敗:`, error);
    throw error;
  }
};

2.4 代碼說明

  • 自動續(xù)傳
    • 頁面加載時,onMounted 通過 getPendingFile 檢查未完成任務(wù)。
    • 若存在未完成任務(wù),pendingFile 存儲元數(shù)據(jù),顯示確認界面(文件名、大小、最后修改時間)。
    • 用戶需選擇相同文件并點擊“繼續(xù)上傳”,確保文件一致性。
  • 文件一致性校驗
    • handleFileChange 和 confirmResume 驗證文件名、大小和最后修改時間,防止錯誤續(xù)傳。
  • Tailwind CSS
    • 添加進度條、樣式化按鈕和響應式確認對話框,提升用戶體驗。
  • 錯誤處理
    • 數(shù)據(jù)庫初始化、文件不匹配和上傳失敗均提供用戶友好的提示。
  • 清理數(shù)據(jù)
    • 上傳完成或取消后,clearDB 清空 chunks 和 metadata 存儲。
  • 后端接口
    • 假設(shè) /upload 接口接收分片,實際需實現(xiàn)后端分片存儲和合并邏輯。

2.5 應用場景

  • 大文件上傳(如視頻、壓縮包)在網(wǎng)絡(luò)不穩(wěn)定或瀏覽器意外關(guān)閉的場景。
  • 云存儲客戶端需要無縫恢復上傳。
  • 用戶希望最小化手動干預的上傳流程。

2.6 局限性

  • 用戶需重新選擇文件以續(xù)傳,因 File 對象無法跨會話持久化??煽紤] FileSystem API(但支持度較低)。
  • 僅支持單文件未完成任務(wù),多個文件需擴展 UI 選擇邏輯。

3. 注意事項與優(yōu)化

3.1 錯誤處理

  • 所有 IndexedDB 和網(wǎng)絡(luò)操作均包含 try-catch 塊,提供用戶提示。
  • 文件不匹配時提示用戶取消任務(wù)或選擇正確文件。

3.2 性能優(yōu)化

  • CHUNK_SIZE(1MB)平衡內(nèi)存和網(wǎng)絡(luò)開銷,可根據(jù)需求調(diào)整。
  • 上傳完成或取消后清理 IndexedDB 數(shù)據(jù),釋放存儲空間。

3.3 瀏覽器兼容性

  • 在 onMounted 中檢查 IndexedDB 支持:
if (!window.indexedDB) {
  alert('瀏覽器不支持 IndexedDB');
}

3.4 改進建議

  • 使用 Dexie.js 簡化 IndexedDB 操作。
  • 封裝上傳邏輯為自定義 Hook(如 useFileUpload)。
  • 添加文件哈希(如 MD5)到 FileMetadata,增強一致性校驗。
  • 支持多文件未完成任務(wù),增加文件選擇 UI。

4. 總結(jié)

通過 IndexedDB 實現(xiàn)可靠的斷點續(xù)傳功能,支持瀏覽器關(guān)閉后經(jīng)用戶確認自動續(xù)傳。Tailwind CSS 優(yōu)化了界面,TypeScript 確保類型安全,完善的錯誤處理提升了可靠性。代碼適用于云存儲、視頻上傳等場景,開發(fā)者可根據(jù)需求調(diào)整分片大小或擴展多文件支持。

到此這篇關(guān)于IndexedDB 實現(xiàn)斷點續(xù)傳、分片上傳 的文章就介紹到這了,更多相關(guān)IndexedDB 斷點續(xù)傳、分片上傳 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論