IndexedDB 实现断点续传、分片上传

IndexedDB 断点续传

本文基于 Vue3、TypeScript 和 Setup 语法糖,实现文件断点续传功能,支持网络中断或浏览器关闭后从上次上传位置继续上传,并在浏览器重新打开时通过用户确认自动续传。使用 IndexedDB 存储文件元数据和分片状态,确保上传过程可靠,支持暂停/恢复以及跨浏览器会话的自动续传。


1. 项目环境准备

1.1 技术栈

  • Vue3:使用 Composition API 和 Setup 语法糖。
  • TypeScript:提供类型安全。
  • IndexedDB:存储文件元数据和分片状态。
  • Vite:作为构建工具。
  • Tailwind CSS:优化界面样式。

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. 大批量文件断点续传(支持自动续传)

2.1 场景描述

断点续传允许用户在网络中断或浏览器关闭后,从上次上传位置继续上传。在浏览器重新打开时,系统检测未完成上传任务,通过用户确认后自动续传。IndexedDB 存储文件元数据(如文件名、大小、最后修改时间)和分片状态(已上传、待上传)。

2.2 实现思路

  1. 使用 IndexedDB 存储文件元数据和分片状态。
  2. 页面加载时,检查 IndexedDB 中的未完成任务,显示确认界面。
  3. 用户确认后,验证文件一致性并继续上传。
  4. 使用 Vue3 响应式 API 管理状态和进度。
  5. 支持暂停/继续功能,实时更新 UI。
  6. TypeScript 确保类型安全。
  7. 使用 Tailwind CSS 优化界面。

2.3 完整示例代码

2.3.1 主组件 (src/App.vue)



2.3.2 工具函数 (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(`上传失败,状态码: ${response.status}`);
    }
    await saveChunkStatus(db, chunkId, fileName, 'uploaded');
  } catch (error) {
    console.error(`分片 ${chunkId} 上传失败:`, error);
    throw error;
  }
};

2.4 代码说明

  • 自动续传
    • 页面加载时,onMounted 通过 getPendingFile 检查未完成任务。
    • 若存在未完成任务,pendingFile 存储元数据,显示确认界面(文件名、大小、最后修改时间)。
    • 用户需选择相同文件并点击“继续上传”,确保文件一致性。
  • 文件一致性校验
    • handleFileChangeconfirmResume 验证文件名、大小和最后修改时间,防止错误续传。
  • Tailwind CSS
    • 添加进度条、样式化按钮和响应式确认对话框,提升用户体验。
  • 错误处理
    • 数据库初始化、文件不匹配和上传失败均提供用户友好的提示。
  • 清理数据
    • 上传完成或取消后,clearDB 清空 chunksmetadata 存储。
  • 后端接口
    • 假设 /upload 接口接收分片,实际需实现后端分片存储和合并逻辑。

2.5 应用场景

  • 大文件上传(如视频、压缩包)在网络不稳定或浏览器意外关闭的场景。
  • 云存储客户端需要无缝恢复上传。
  • 用户希望最小化手动干预的上传流程。

2.6 局限性

  • 用户需重新选择文件以续传,因 File 对象无法跨会话持久化。可考虑 FileSystem API(但支持度较低)。
  • 仅支持单文件未完成任务,多个文件需扩展 UI 选择逻辑。

3. 注意事项与优化

3.1 错误处理

  • 所有 IndexedDB 和网络操作均包含 try-catch 块,提供用户提示。
  • 文件不匹配时提示用户取消任务或选择正确文件。

3.2 性能优化

  • CHUNK_SIZE(1MB)平衡内存和网络开销,可根据需求调整。
  • 上传完成或取消后清理 IndexedDB 数据,释放存储空间。

3.3 浏览器兼容性

  • onMounted 中检查 IndexedDB 支持:
if (!window.indexedDB) {
  alert('浏览器不支持 IndexedDB');
}

3.4 改进建议

  • 使用 Dexie.js 简化 IndexedDB 操作。
  • 封装上传逻辑为自定义 Hook(如 useFileUpload)。
  • 添加文件哈希(如 MD5)到 FileMetadata,增强一致性校验。
  • 支持多文件未完成任务,增加文件选择 UI。

4. 总结

通过 IndexedDB 实现可靠的断点续传功能,支持浏览器关闭后经用户确认自动续传。Tailwind CSS 优化了界面,TypeScript 确保类型安全,完善的错误处理提升了可靠性。代码适用于云存储、视频上传等场景,开发者可根据需求调整分片大小或扩展多文件支持。

你可能感兴趣的:(前端,性能优化,前端,typescript,vue)