前端开发怎么实现文件的断点续传

发布时间:2021-09-09 10:02:30 作者:chen
来源:亿速云 阅读:181
# 前端开发怎么实现文件的断点续传

## 目录
1. [引言](#引言)
2. [断点续传技术原理](#断点续传技术原理)
   - 2.1 [HTTP范围请求](#http范围请求)
   - 2.2 [文件分块策略](#文件分块策略)
   - 2.3 [断点记录机制](#断点记录机制)
3. [前端实现方案](#前端实现方案)
   - 3.1 [基础实现流程](#基础实现流程)
   - 3.2 [核心API使用](#核心api使用)
   - 3.3 [进度监控设计](#进度监控设计)
4. [完整代码实现](#完整代码实现)
   - 4.1 [HTML结构](#html结构)
   - 4.2 [JavaScript核心逻辑](#javascript核心逻辑)
   - 4.3 [服务端配合示例](#服务端配合示例)
5. [优化与扩展](#优化与扩展)
   - 5.1 [并发上传控制](#并发上传控制)
   - 5.2 [失败重试机制](#失败重试机制)
   - 5.3 [本地存储优化](#本地存储优化)
6. [实际应用场景](#实际应用场景)
7. [常见问题解决方案](#常见问题解决方案)
8. [未来发展趋势](#未来发展趋势)
9. [结语](#结语)

## 引言

在现代Web应用中,大文件上传是常见的需求场景。传统的文件上传方式在面对网络不稳定或大文件传输时存在明显缺陷:

1. 网络中断导致整个文件重新上传
2. 上传进度不可控
3. 服务器资源浪费

断点续传技术通过将文件分块、记录传输状态,实现了:
- 网络恢复后从中断处继续传输
- 精确的上传进度控制
- 更高的传输可靠性

本文将深入探讨前端实现断点续传的完整技术方案。

## 断点续传技术原理

### HTTP范围请求

```javascript
// 典型Range头示例
headers: {
  'Range': 'bytes=0-102399' // 请求前102400字节
}

HTTP/1.1定义的Range请求允许客户端请求资源的特定部分。关键要点:

  1. 请求头:

    • Range: bytes=start-end
    • 支持多范围请求:bytes=0-50, 100-150
  2. 响应头:

    • Accept-Ranges: bytes(服务器支持范围请求)
    • Content-Range: bytes start-end/total(实际返回的范围)
  3. 状态码:

    • 206 Partial Content(成功返回部分内容)
    • 416 Requested Range Not Satisfiable(范围请求不合法)

文件分块策略

分块方式 优点 缺点 适用场景
固定大小 实现简单 可能影响效率 常规文件
动态分块 网络适配性好 实现复杂 不稳定网络
哈希分块 重复检测 计算开销大 云存储系统

推荐分块大小: - 小文件(<10MB):单块上传 - 中等文件(10MB-1GB):1-5MB/块 - 大文件(>1GB):5-10MB/块

断点记录机制

实现方案对比:

graph TD
    A[记录方式] --> B[LocalStorage]
    A --> C[IndexedDB]
    A --> D[服务端存储]
    B --> E[容量限制5MB]
    C --> F[支持大数据量]
    D --> G[最可靠但需网络]

推荐组合方案: 1. 前端使用IndexedDB存储分块状态 2. 服务端记录最终上传状态 3. 恢复时进行状态校验

前端实现方案

基础实现流程

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    
    User->>Frontend: 选择文件
    Frontend->>Frontend: 计算文件哈希(可选)
    Frontend->>Backend: 查询上传状态
    Backend-->>Frontend: 返回已上传块列表
    loop 分块上传
        Frontend->>Backend: 上传文件块(带Range头)
        Backend-->>Frontend: 返回上传结果
        Frontend->>Frontend: 更新本地进度
    end
    Frontend->>Backend: 合并文件请求
    Backend-->>Frontend: 返回完整文件URL

核心API使用

  1. File API获取文件信息:
const file = input.files[0];
const chunkSize = 5 * 1024 * 1024; // 5MB
const totalChunks = Math.ceil(file.size / chunkSize);
  1. Blob.slice方法分块:
function getChunk(file, index) {
  const start = index * chunkSize;
  const end = Math.min(file.size, start + chunkSize);
  return file.slice(start, end);
}
  1. Fetch API带进度上传:
async function uploadChunk(url, chunk, headers = {}) {
  const xhr = new XMLHttpRequest();
  return new Promise((resolve, reject) => {
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable) {
        console.log(`进度: ${Math.round((e.loaded / e.total) * 100)}%`);
      }
    };
    
    xhr.onload = () => resolve(xhr.response);
    xhr.onerror = reject;
    
    xhr.open('POST', url, true);
    Object.entries(headers).forEach(([k, v]) => {
      xhr.setRequestHeader(k, v);
    });
    xhr.send(chunk);
  });
}

进度监控设计

复合进度计算模型:

class UploadProgress {
  constructor(total) {
    this.total = total;
    this.loaded = 0;
    this.chunkProgress = {};
  }
  
  update(chunkId, percent) {
    this.chunkProgress[chunkId] = percent;
    this.recalculate();
  }
  
  recalculate() {
    this.loaded = Object.values(this.chunkProgress)
      .reduce((sum, p) => sum + p, 0) / Object.keys(this.chunkProgress).length;
  }
  
  get percent() {
    return Math.min(100, Math.round(this.loaded * 100));
  }
}

可视化方案建议: 1. 整体进度条(总文件) 2. 分块进度网格(每个块的状态) 3. 实时传输速率显示 4. 预估剩余时间计算

完整代码实现

HTML结构

<div class="upload-container">
  <input type="file" id="fileInput" />
  <button id="uploadBtn">开始上传</button>
  <button id="pauseBtn" disabled>暂停</button>
  <button id="resumeBtn" disabled>恢复</button>
  
  <div class="progress-container">
    <div class="progress-bar" id="totalProgress"></div>
    <span id="progressText">0%</span>
  </div>
  
  <div class="chunk-grid" id="chunkGrid"></div>
  
  <div class="status-info">
    <div>速度: <span id="speed">0 KB/s</span></div>
    <div>剩余时间: <span id="remaining">--</span></div>
  </div>
</div>

JavaScript核心逻辑

class ResumableUploader {
  constructor(options) {
    this.file = null;
    this.chunkSize = options.chunkSize || 5 * 1024 * 1024;
    this.uploadUrl = options.uploadUrl;
    this.statusUrl = options.statusUrl;
    this.mergeUrl = options.mergeUrl;
    this.concurrent = options.concurrent || 3;
    this.chunks = [];
    this.activeUploads = new Set();
    this.paused = false;
    this.fileHash = '';
    
    // 初始化IndexedDB
    this.dbPromise = this.initDB();
  }
  
  async initDB() {
    return new Promise((resolve) => {
      const request = indexedDB.open('UploadManager', 1);
      
      request.onupgradeneeded = (e) => {
        const db = e.target.result;
        if (!db.objectStoreNames.contains('uploadRecords')) {
          db.createObjectStore('uploadRecords', { keyPath: 'fileHash' });
        }
      };
      
      request.onsuccess = (e) => resolve(e.target.result);
    });
  }
  
  async selectFile(file) {
    this.file = file;
    this.fileHash = await this.calculateHash(file);
    this.chunks = Array(Math.ceil(file.size / this.chunkSize)).fill(false);
    
    // 检查本地存储
    const record = await this.getRecord(this.fileHash);
    if (record) {
      this.chunks = record.chunks;
      updateUI();
    }
    
    // 查询服务端状态
    await this.checkServerStatus();
  }
  
  async checkServerStatus() {
    const response = await fetch(`${this.statusUrl}?hash=${this.fileHash}`);
    const data = await response.json();
    data.uploadedChunks.forEach(chunk => {
      this.chunks[chunk] = true;
    });
    updateUI();
  }
  
  async startUpload() {
    this.paused = false;
    const pendingChunks = this.chunks
      .map((done, index) => done ? null : index)
      .filter(i => i !== null);
    
    while (pendingChunks.length >  && !this.paused) {
      const availableSlots = this.concurrent - this.activeUploads.size;
      if (availableSlots <= 0) {
        await new Promise(r => setTimeout(r, 500));
        continue;
      }
      
      const chunkIds = pendingChunks.splice(0, availableSlots);
      chunkIds.forEach(chunkId => this.uploadChunk(chunkId));
    }
    
    if (this.chunks.every(Boolean)) {
      await this.mergeChunks();
    }
  }
  
  async uploadChunk(chunkId) {
    this.activeUploads.add(chunkId);
    
    try {
      const chunk = getChunk(this.file, chunkId);
      const formData = new FormData();
      formData.append('chunk', chunk);
      formData.append('chunkId', chunkId);
      formData.append('totalChunks', this.chunks.length);
      formData.append('fileHash', this.fileHash);
      
      await fetch(this.uploadUrl, {
        method: 'POST',
        body: formData
      });
      
      this.chunks[chunkId] = true;
      await this.saveRecord();
      updateUI();
    } catch (err) {
      console.error(`块${chunkId}上传失败:`, err);
    } finally {
      this.activeUploads.delete(chunkId);
    }
  }
  
  // 其他辅助方法...
}

服务端配合示例(Node.js)

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');

const app = express();
const upload = multer({ dest: 'temp/' });

// 存储上传记录
const uploadRecords = new Map();

app.post('/upload', upload.single('chunk'), (req, res) => {
  const { chunkId, totalChunks, fileHash } = req.body;
  
  // 创建文件记录
  if (!uploadRecords.has(fileHash)) {
    uploadRecords.set(fileHash, {
      total: parseInt(totalChunks),
      uploaded: new Set()
    });
  }
  
  const record = uploadRecords.get(fileHash);
  
  // 移动临时文件
  const chunkPath = path.join('chunks', fileHash, `${chunkId}`);
  fs.mkdirSync(path.dirname(chunkPath), { recursive: true });
  fs.renameSync(req.file.path, chunkPath);
  
  // 更新记录
  record.uploaded.add(parseInt(chunkId));
  
  res.json({ success: true, chunkId });
});

app.get('/status', (req, res) => {
  const { hash } = req.query;
  const record = uploadRecords.get(hash) || { uploaded: [] };
  res.json({
    uploadedChunks: Array.from(record.uploaded)
  });
});

app.post('/merge', async (req, res) => {
  const { fileHash, fileName } = req.body;
  const record = uploadRecords.get(fileHash);
  
  if (!record || record.uploaded.size !== record.total) {
    return res.status(400).json({ error: '上传未完成' });
  }
  
  // 合并文件
  const mergedPath = path.join('uploads', fileName);
  const writeStream = fs.createWriteStream(mergedPath);
  
  for (let i = 0; i < record.total; i++) {
    const chunkPath = path.join('chunks', fileHash, `${i}`);
    await new Promise(resolve => {
      fs.createReadStream(chunkPath)
        .pipe(writeStream, { end: false })
        .on('finish', resolve);
    });
  }
  
  writeStream.end();
  
  // 清理临时文件
  fs.rmdirSync(path.join('chunks', fileHash), { recursive: true });
  uploadRecords.delete(fileHash);
  
  res.json({ url: `/uploads/${fileName}` });
});

app.listen(3000);

优化与扩展

并发上传控制

智能并发调整算法:

class ConcurrencyController {
  constructor(maxConcurrent) {
    this.max = maxConcurrent;
    this.current = 0;
    this.history = [];
    this.adjustInterval = setInterval(() => this.adjust(), 5000);
  }
  
  async acquire() {
    while (this.current >= this.calculateOptimal()) {
      await new Promise(r => setTimeout(r, 100));
    }
    this.current++;
  }
  
  release() {
    this.current--;
  }
  
  calculateOptimal() {
    // 基于历史吞吐量动态计算
    const avgSpeed = this.history.length 
      ? this.history.reduce((s, v) => s + v, 0) / this.history.length
      : 0;
    
    if (avgSpeed < 100 * 1024) { // <100KB/s
      return Math.max(1, this.max / 2);
    }
    return this.max;
  }
  
  adjust() {
    this.history.push(currentSpeed);
    if (this.history.length > 10) this.history.shift();
  }
}

失败重试机制

async uploadChunkWithRetry(chunkId, maxRetries = 3) {
  let attempt = 0;
  while (attempt < maxRetries) {
    try {
      await this.uploadChunk(chunkId);
      return;
    } catch (err) {
      attempt++;
      if (attempt >= maxRetries) throw err;
      
      // 指数退避
      await new Promise(r => 
        setTimeout(r, 1000 * Math.pow(2, attempt)));
    }
  }
}

本地存储优化

IndexedDB高级用法:

async saveRecord() {
  const db = await this.dbPromise;
  const tx = db.transaction('uploadRecords', 'readwrite');
  const store = tx.objectStore('uploadRecords');
  
  await store.put({
    fileHash: this.fileHash,
    chunks: this.chunks,
    fileName: this.file.name,
    lastModified: this.file.lastModified,
    timestamp: Date.now()
  });
  
  // 清理过期记录
  const allRecords = await store.getAll();
  if (allRecords.length > 20) {
    const oldRecords = allRecords
      .sort((a, b) => a.timestamp - b.timestamp)
      .slice(0, -20);
    
    oldRecords.forEach(r => 
      store.delete(r.fileHash));
  }
}

实际应用场景

云存储服务

企业应用

特殊场景

常见问题解决方案

问题1:文件哈希计算导致界面卡顿

解决方案:

// 使用Web Worker后台计算
function calculateHashInWorker(file) {
  return new Promise((resolve) => {
    const worker = new Worker('hash-worker.js');
    worker.postMessage(file);
    worker.onmessage = (e) => resolve(e.data);
  });
}

// hash-worker.js
self.onmessage = async (e) => {
  const file = e.data;
  const buffer = await file.arrayBuffer();
  const hash = await crypto.subtle.digest('SHA-256', buffer);
  const hashArray = Array.from(new Uint8Array(hash));
  const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  self.postMessage(hashHex);
};

问题2:Safari浏览器兼容性问题

处理方案: 1. 检测浏览器支持情况:

const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
  1. 备用方案:
    • 使用XHR代替Fetch API
    • 减小默认分块大小(Safari对内存使用更敏感)
    • 增加进度上报频率

问题3:服务端存储空间浪费

优化策略: 1. 设置自动清理机制(24小时未完成的临时文件) 2. 实现分块去重(相同哈希值的块只存一份) 3. 使用LRU缓存策略管理临时文件

未来发展趋势

  1. WebTransport协议

    • 基于QUIC的传输协议
    • 原生支持多路复用和可靠传输
  2. WebRTC数据通道

    • 点对点文件传输
    • 降低服务器带宽压力
  3. 增量哈希算法

    • 流式哈希计算
    • 实时重复数据检测
  4. 预测优化

    • 基于网络状况
推荐阅读:
  1. 如何实现大文件的上传和断点续传
  2. H5如何实现文件断点续传

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

前端开发

上一篇:Java并发之AbstractQueuedSynchronizer源码的示例分析

下一篇:怎么通过重启路由的方法切换IP地址

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》