> 技术文档 > 前端大文件断点续传完整实现指南:原理、安全策略与代码实战_前端断点续传

前端大文件断点续传完整实现指南:原理、安全策略与代码实战_前端断点续传


文章目录

    • 一、断点续传核心原理
      • 1.1 技术架构设计
      • 1.2 核心流程
    • 二、前端核心实现
    • 三、服务端关键实现(Node.js)
      • 3.1 分片接收接口
      • 3.2 文件合并接口
    • 四、安全增强方案
      • 4.1 全链路加密验证
      • 4.2 安全防护策略
    • 五、可靠性保障机制
      • 5.1 断点恢复实现
      • 5.2 分片校验流程
    • 六、性能优化方案
      • 6.1 智能分片策略
      • 6.2 并发控制优化
    • 七、完整测试方案
      • 7.1 测试用例设计
      • 7.2 自动化测试示例

一、断点续传核心原理

1.1 技术架构设计

#mermaid-svg-iv0ZVAGcgarPb5iU {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU .error-icon{fill:#552222;}#mermaid-svg-iv0ZVAGcgarPb5iU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iv0ZVAGcgarPb5iU .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-iv0ZVAGcgarPb5iU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iv0ZVAGcgarPb5iU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iv0ZVAGcgarPb5iU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iv0ZVAGcgarPb5iU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iv0ZVAGcgarPb5iU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iv0ZVAGcgarPb5iU .marker.cross{stroke:#333333;}#mermaid-svg-iv0ZVAGcgarPb5iU svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iv0ZVAGcgarPb5iU .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU .cluster-label text{fill:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU .cluster-label span{color:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU .label text,#mermaid-svg-iv0ZVAGcgarPb5iU span{fill:#333;color:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU .node rect,#mermaid-svg-iv0ZVAGcgarPb5iU .node circle,#mermaid-svg-iv0ZVAGcgarPb5iU .node ellipse,#mermaid-svg-iv0ZVAGcgarPb5iU .node polygon,#mermaid-svg-iv0ZVAGcgarPb5iU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iv0ZVAGcgarPb5iU .node .label{text-align:center;}#mermaid-svg-iv0ZVAGcgarPb5iU .node.clickable{cursor:pointer;}#mermaid-svg-iv0ZVAGcgarPb5iU .arrowheadPath{fill:#333333;}#mermaid-svg-iv0ZVAGcgarPb5iU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iv0ZVAGcgarPb5iU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iv0ZVAGcgarPb5iU .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-iv0ZVAGcgarPb5iU .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-iv0ZVAGcgarPb5iU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iv0ZVAGcgarPb5iU .cluster text{fill:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU .cluster span{color:#333;}#mermaid-svg-iv0ZVAGcgarPb5iU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iv0ZVAGcgarPb5iU :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 分片上传 前端 服务端 临时存储 分片校验 合并文件 永久存储 本地缓存 恢复上传

1.2 核心流程

#mermaid-svg-99uspBK04rNvpJsW {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-99uspBK04rNvpJsW .error-icon{fill:#552222;}#mermaid-svg-99uspBK04rNvpJsW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-99uspBK04rNvpJsW .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-99uspBK04rNvpJsW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-99uspBK04rNvpJsW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-99uspBK04rNvpJsW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-99uspBK04rNvpJsW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-99uspBK04rNvpJsW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-99uspBK04rNvpJsW .marker.cross{stroke:#333333;}#mermaid-svg-99uspBK04rNvpJsW svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-99uspBK04rNvpJsW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-99uspBK04rNvpJsW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-99uspBK04rNvpJsW .actor-line{stroke:grey;}#mermaid-svg-99uspBK04rNvpJsW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-99uspBK04rNvpJsW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-99uspBK04rNvpJsW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-99uspBK04rNvpJsW .sequenceNumber{fill:white;}#mermaid-svg-99uspBK04rNvpJsW #sequencenumber{fill:#333;}#mermaid-svg-99uspBK04rNvpJsW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-99uspBK04rNvpJsW .messageText{fill:#333;stroke:#333;}#mermaid-svg-99uspBK04rNvpJsW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-99uspBK04rNvpJsW .labelText,#mermaid-svg-99uspBK04rNvpJsW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-99uspBK04rNvpJsW .loopText,#mermaid-svg-99uspBK04rNvpJsW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-99uspBK04rNvpJsW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-99uspBK04rNvpJsW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-99uspBK04rNvpJsW .noteText,#mermaid-svg-99uspBK04rNvpJsW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-99uspBK04rNvpJsW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-99uspBK04rNvpJsW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-99uspBK04rNvpJsW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-99uspBK04rNvpJsW .actorPopupMenu{position:absolute;}#mermaid-svg-99uspBK04rNvpJsW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-99uspBK04rNvpJsW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-99uspBK04rNvpJsW .actor-man circle,#mermaid-svg-99uspBK04rNvpJsW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-99uspBK04rNvpJsW :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 用户 前端 服务端 选择文件 文件分片计算 查询文件状态 返回已上传分片 上传分片N 返回结果 loop [分片上传] 合并请求 校验合并文件 返回最终结果 用户 前端 服务端

二、前端核心实现

2.1 文件分片处理

class FileSplitter { constructor(file, chunkSize = 5 * 1024 * 1024) { this.file = file this.chunkSize = chunkSize this.totalChunks = Math.ceil(file.size / chunkSize) this.currentChunk = 0 } async* getChunk() { while (this.currentChunk < this.totalChunks) { const start = this.currentChunk * this.chunkSize const end = Math.min(start + this.chunkSize, this.file.size) const chunk = this.file.slice(start, end) yield { chunk, index: this.currentChunk, total: this.totalChunks, hash: await this.calculateHash(chunk) } this.currentChunk++ } } async calculateHash(chunk) { const buffer = await chunk.arrayBuffer() const hashBuffer = await crypto.subtle.digest(\'SHA-256\', buffer) return Array.from(new Uint8Array(hashBuffer)) .map(b => b.toString(16).padStart(2, \'0\')) .join(\'\') }}

2.2 上传控制器

class UploadController { private file: File private chunks: UploadChunk[] private concurrentLimit = 3 private retryLimit = 3 private uploadedChunks = new Set<number>() private progressCallbacks: Function[] = [] constructor(file: File) { this.file = file this.initChunks() this.loadProgress() } private initChunks() { const splitter = new FileSplitter(this.file) this.chunks = Array.from({length: splitter.totalChunks}, (_, i) => ({ index: i, status: \'pending\', retries: 0 })) } async start() { const queue = new AsyncQueue(this.concurrentLimit) for (const chunk of this.chunks) { if (!this.uploadedChunks.has(chunk.index)) { queue.add(() => this.uploadChunk(chunk)) } } await queue.complete() await this.mergeFile() } private async uploadChunk(chunk: UploadChunk) { try { const splitter = new FileSplitter(this.file) const chunkData = await splitter.getChunk(chunk.index) const formData = new FormData() formData.append(\'file\', chunkData.chunk) formData.append(\'index\', chunk.index.toString()) formData.append(\'total\', chunkData.total.toString()) formData.append(\'hash\', chunkData.hash) const response = await fetch(\'/api/upload\', { method: \'POST\', body: formData }) if (!response.ok) throw new Error(\'Upload failed\') this.uploadedChunks.add(chunk.index) this.saveProgress() this.emitProgress() } catch (error) { if (chunk.retries < this.retryLimit) { chunk.retries++ return this.uploadChunk(chunk) } throw error } }}

三、服务端关键实现(Node.js)

3.1 分片接收接口

const express = require(\'express\')const fs = require(\'fs-extra\')const multer = require(\'multer\')const upload = multer({ dest: \'temp/\' })const app = express()const activeUploads = new Map()app.post(\'/api/upload\', upload.single(\'file\'), async (req, res) => { const { index, total, hash } = req.body const fileKey = `${req.file.originalname}-${hash}` // 校验分片哈希 const chunkHash = await calculateHash(req.file.path) if (chunkHash !== hash) { fs.remove(req.file.path) return res.status(400).send(\'Invalid chunk hash\') } // 存储分片信息 if (!activeUploads.has(fileKey)) { activeUploads.set(fileKey, { totalChunks: parseInt(total), receivedChunks: new Set() }) } const uploadInfo = activeUploads.get(fileKey) uploadInfo.receivedChunks.add(parseInt(index)) // 返回已接收分片 res.json({ received: Array.from(uploadInfo.receivedChunks) })})

3.2 文件合并接口

app.post(\'/api/merge\', async (req, res) => { const { fileName, total, hash } = req.body const fileKey = `${fileName}-${hash}` const uploadInfo = activeUploads.get(fileKey) if (!uploadInfo || uploadInfo.receivedChunks.size < uploadInfo.totalChunks) { return res.status(400).send(\'Not all chunks received\') } // 合并文件 const finalPath = path.join(\'uploads\', fileName) const writeStream = fs.createWriteStream(finalPath) for (let i = 0; i < uploadInfo.totalChunks; i++) { const chunkPath = path.join(\'temp\', `${fileKey}-${i}`) const chunkBuffer = await fs.readFile(chunkPath) writeStream.write(chunkBuffer) await fs.remove(chunkPath) } writeStream.end() // 校验最终文件 const finalHash = await calculateHash(finalPath) if (finalHash !== hash) { await fs.remove(finalPath) return res.status(500).send(\'File verification failed\') } activeUploads.delete(fileKey) res.sendStatus(200)})

四、安全增强方案

4.1 全链路加密验证

// 前端加密配置const encryptChunk = async (chunk, publicKey) => { const encoder = new TextEncoder() const data = encoder.encode(chunk) const encrypted = await window.crypto.subtle.encrypt( { name: \'RSA-OAEP\' }, publicKey, data ) return new Blob([encrypted])}// 服务端解密const decryptChunk = async (encrypted, privateKey) => { const buffer = await encrypted.arrayBuffer() return crypto.subtle.decrypt( { name: \'RSA-OAEP\' }, privateKey, buffer )}

4.2 安全防护策略

// 客户端防御const validateFile = (file) => { const MAX_SIZE = 10 * 1024 * 1024 * 1024 // 10GB const ALLOW_TYPES = [\'video/mp4\', \'image/png\'] if (file.size > MAX_SIZE) throw new Error(\'文件过大\') if (!ALLOW_TYPES.includes(file.type)) throw new Error(\'文件类型不支持\')}// 服务端防御const antiVirusScan = async (filePath) => { const result = await clamscan.scanFile(filePath) if (result.viruses.length > 0) { throw new Error(\'发现恶意文件\') }}

五、可靠性保障机制

5.1 断点恢复实现

class UploadRecovery { static STORAGE_KEY = \'upload_progress\' static saveProgress(fileHash, chunks) { const progress = localStorage.getItem(STORAGE_KEY) || {} progress[fileHash] = chunks localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)) } static loadProgress(fileHash) { const progress = JSON.parse(localStorage.getItem(STORAGE_KEY)) return progress[fileHash] || [] } static clearProgress(fileHash) { const progress = JSON.parse(localStorage.getItem(STORAGE_KEY)) delete progress[fileHash] localStorage.setItem(STORAGE_KEY, JSON.stringify(progress)) }}

5.2 分片校验流程

#mermaid-svg-AFpW6CxaWaEzmX23 {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 .error-icon{fill:#552222;}#mermaid-svg-AFpW6CxaWaEzmX23 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AFpW6CxaWaEzmX23 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-AFpW6CxaWaEzmX23 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AFpW6CxaWaEzmX23 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AFpW6CxaWaEzmX23 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AFpW6CxaWaEzmX23 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AFpW6CxaWaEzmX23 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AFpW6CxaWaEzmX23 .marker.cross{stroke:#333333;}#mermaid-svg-AFpW6CxaWaEzmX23 svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AFpW6CxaWaEzmX23 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AFpW6CxaWaEzmX23 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-AFpW6CxaWaEzmX23 .actor-line{stroke:grey;}#mermaid-svg-AFpW6CxaWaEzmX23 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 .sequenceNumber{fill:white;}#mermaid-svg-AFpW6CxaWaEzmX23 #sequencenumber{fill:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 .messageText{fill:#333;stroke:#333;}#mermaid-svg-AFpW6CxaWaEzmX23 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AFpW6CxaWaEzmX23 .labelText,#mermaid-svg-AFpW6CxaWaEzmX23 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-AFpW6CxaWaEzmX23 .loopText,#mermaid-svg-AFpW6CxaWaEzmX23 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-AFpW6CxaWaEzmX23 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-AFpW6CxaWaEzmX23 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-AFpW6CxaWaEzmX23 .noteText,#mermaid-svg-AFpW6CxaWaEzmX23 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-AFpW6CxaWaEzmX23 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AFpW6CxaWaEzmX23 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AFpW6CxaWaEzmX23 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-AFpW6CxaWaEzmX23 .actorPopupMenu{position:absolute;}#mermaid-svg-AFpW6CxaWaEzmX23 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-AFpW6CxaWaEzmX23 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-AFpW6CxaWaEzmX23 .actor-man circle,#mermaid-svg-AFpW6CxaWaEzmX23 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-AFpW6CxaWaEzmX23 :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 前端 服务端 上传分片N 计算分片哈希 确认接收 要求重传 重新计算分片 重传分片N alt [哈希匹配] [哈希不匹配] 前端 服务端

六、性能优化方案

6.1 智能分片策略

function calculateChunkSize(fileSize) { const MIN_CHUNK = 1 * 1024 * 1024 // 1MB const MAX_CHUNK = 10 * 1024 * 1024 // 10MB const TARGET_CHUNKS = 100 const idealSize = Math.ceil(fileSize / TARGET_CHUNKS) return Math.min(MAX_CHUNK, Math.max(MIN_CHUNK, idealSize))}

6.2 并发控制优化

class AsyncQueue { constructor(concurrency = 3) { this.pending = [] this.inProgress = 0 this.concurrency = concurrency } add(task) { return new Promise((resolve, reject) => { this.pending.push({ task, resolve, reject }) this.run() }) } run() { while (this.inProgress < this.concurrency && this.pending.length) { const { task, resolve, reject } = this.pending.shift() this.inProgress++ task() .then(resolve) .catch(reject) .finally(() => { this.inProgress-- this.run() }) } } async complete() { while (this.pending.length || this.inProgress) { await new Promise(resolve => setTimeout(resolve, 100)) } }}

七、完整测试方案

7.1 测试用例设计

测试场景 验证目标 方法 网络中断恢复 自动续传能力 手动断开网络 分片哈希校验 数据完整性保障 修改分片内容 并发压力测试 服务器稳定性 同时发起100+上传 大文件测试 内存泄漏检查 上传10GB文件

7.2 自动化测试示例

def test_resume_upload(): # 初始化上传 file = generate_large_file(\'1GB.bin\') response = start_upload(file) assert response.status_code == 200 # 中断上传 interrupt_network() upload_chunk() assert_last_progress_saved() # 恢复上传 restore_network() resume_response = resume_upload(file) assert_file_complete(resume_response)

总结:本文从原理到实践详细讲解了前端断点续传的完整实现方案,包含文件分片、加密传输、进度恢复等核心技术点,并提供了生产环境可用的代码实现。
在这里插入图片描述