本文基于 Vue3、TypeScript 和 Setup 语法糖,实现文件断点续传功能,支持网络中断或浏览器关闭后从上次上传位置继续上传,并在浏览器重新打开时通过用户确认自动续传。使用 IndexedDB 存储文件元数据和分片状态,确保上传过程可靠,支持暂停/恢复以及跨浏览器会话的自动续传。
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
在 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')],
},
},
})
无额外运行时依赖,使用浏览器原生 IndexedDB API。
断点续传允许用户在网络中断或浏览器关闭后,从上次上传位置继续上传。在浏览器重新打开时,系统检测未完成上传任务,通过用户确认后自动续传。IndexedDB 存储文件元数据(如文件名、大小、最后修改时间)和分片状态(已上传、待上传)。
src/App.vue
)
文件断点续传(支持自动续传)
上传进度: {{ progress }}%
检测到未完成任务,正在自动续传 {{ fileName }}...
检测到未完成的文件:{{ pendingFile.fileName }} ({{ formatSize(pendingFile.fileSize) }})
上次修改时间:{{ new Date(pendingFile.lastModified).toLocaleString() }}
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;
}
};
onMounted
通过 getPendingFile
检查未完成任务。pendingFile
存储元数据,显示确认界面(文件名、大小、最后修改时间)。handleFileChange
和 confirmResume
验证文件名、大小和最后修改时间,防止错误续传。clearDB
清空 chunks
和 metadata
存储。/upload
接口接收分片,实际需实现后端分片存储和合并逻辑。File
对象无法跨会话持久化。可考虑 FileSystem API(但支持度较低)。CHUNK_SIZE
(1MB)平衡内存和网络开销,可根据需求调整。onMounted
中检查 IndexedDB 支持:if (!window.indexedDB) {
alert('浏览器不支持 IndexedDB');
}
Dexie.js
简化 IndexedDB 操作。useFileUpload
)。FileMetadata
,增强一致性校验。通过 IndexedDB 实现可靠的断点续传功能,支持浏览器关闭后经用户确认自动续传。Tailwind CSS 优化了界面,TypeScript 确保类型安全,完善的错误处理提升了可靠性。代码适用于云存储、视频上传等场景,开发者可根据需求调整分片大小或扩展多文件支持。