如何实现前端大文件上传(切片上传+断点续传)?

应用背景

在现代Web应用中,文件上传是一个常见的功能需求,尤其是大文件上传(如视频、高清图片、大型文档等)。传统的文件上传方式在处理大文件时存在以下问题:

  1. 网络不稳定:大文件上传时间长,网络波动可能导致上传失败,用户需要重新上传。
  2. 服务器压力:一次性上传大文件会占用大量服务器资源,可能导致服务器性能下降甚至崩溃。
  3. 用户体验差:上传失败后需要重新上传,用户等待时间长,体验不佳。

为了解决这些问题,前端大文件上传通常采用分片上传断点续传的技术:

  • 分片上传:将大文件分割成多个小文件(分片),逐个上传,减少单次上传的压力。
  • 断点续传:如果上传中断,可以从断点处继续上传,避免重新上传整个文件。

为什么需要大文件上传?

  1. 提高上传成功率:分片上传可以减少单次上传的文件大小,降低因网络波动导致的上传失败概率。
  2. 减轻服务器压力:分片上传可以分散服务器负载,避免一次性处理大文件。
  3. 提升用户体验:断点续传功能可以让用户在上传中断后继续上传,减少等待时间。
  4. 支持大文件上传:传统上传方式对文件大小有限制,分片上传可以突破这一限制。

代码实现

以下是基于前端技术(如 Vue.js 和 Axios)实现大文件上传的代码示例。

1. 文件分片

将大文件分割成多个小文件(分片)。

typescript 复制代码
const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片大小为 5MB

function createFileChunks(file: File, chunkSize: number = CHUNK_SIZE) {
  const chunks = [];
  let start = 0;
  while (start < file.size) {
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);
    chunks.push(chunk);
    start = end;
  }
  return chunks;
}

2. 上传分片

使用 Axios 上传每个分片。

typescript 复制代码
import axios from "axios";

async function uploadChunk(chunk: Blob, index: number, fileHash: string) {
  const formData = new FormData();
  formData.append("chunk", chunk);
  formData.append("index", index.toString());
  formData.append("fileHash", fileHash);

  try {
    const response = await axios.post("/api/upload-chunk", formData, {
      headers: { "Content-Type": "multipart/form-data" },
    });
    return response.data;
  } catch (error) {
    console.error("上传分片失败:", error);
    throw error;
  }
}

3. 计算文件哈希

为文件生成唯一标识(哈希值),用于断点续传和文件校验。

typescript 复制代码
import { md5 } from "js-md5";

async function calculateFileHash(file: File): Promise<string> {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const hash = md5(e.target?.result as string);
      resolve(hash);
    };
    reader.readAsBinaryString(file);
  });
}

4. 合并分片

所有分片上传完成后,通知服务器合并分片。

typescript 复制代码
async function mergeChunks(fileName: string, fileHash: string) {
  try {
    const response = await axios.post("/api/merge-chunks", {
      fileName,
      fileHash,
    });
    return response.data;
  } catch (error) {
    console.error("合并分片失败:", error);
    throw error;
  }
}

5. 断点续传

在上传前检查服务器是否已存在部分分片,避免重复上传。

typescript 复制代码
async function checkUploadedChunks(fileHash: string): Promise<number[]> {
  try {
    const response = await axios.get("/api/uploaded-chunks", {
      params: { fileHash },
    });
    return response.data.uploadedChunks || [];
  } catch (error) {
    console.error("检查已上传分片失败:", error);
    return [];
  }
}

6. 完整上传流程

将上述功能整合为一个完整的上传流程。

typescript 复制代码
async function uploadFile(file: File) {
  const fileHash = await calculateFileHash(file);
  const chunks = createFileChunks(file);
  const uploadedChunks = await checkUploadedChunks(fileHash);

  for (let i = 0; i < chunks.length; i++) {
    if (uploadedChunks.includes(i)) continue; // 跳过已上传的分片
    await uploadChunk(chunks[i], i, fileHash);
  }

  await mergeChunks(file.name, fileHash);
  console.log("文件上传完成");
}

7. 前端调用

在 Vue.js 组件中调用上传功能。

vue 复制代码
<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="handleUpload">上传</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { uploadFile } from "./uploadUtils";

const selectedFile = ref<File | null>(null);

function handleFileChange(event: Event) {
  const target = event.target as HTMLInputElement;
  if (target.files && target.files[0]) {
    selectedFile.value = target.files[0];
  }
}

async function handleUpload() {
  if (selectedFile.value) {
    await uploadFile(selectedFile.value);
  } else {
    alert("请先选择文件");
  }
}
</script>

服务器端实现(Node.js 示例)

1. 接收分片

javascript 复制代码
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");

const app = express();
const upload = multer({ dest: "uploads/" });

app.post("/api/upload-chunk", upload.single("chunk"), (req, res) => {
  const { index, fileHash } = req.body;
  const chunkPath = path.join("uploads", `${fileHash}-${index}`);
  fs.renameSync(req.file.path, chunkPath);
  res.send({ success: true });
});

2. 合并分片

javascript 复制代码
app.post("/api/merge-chunks", (req, res) => {
  const { fileName, fileHash } = req.body;
  const chunkDir = path.join("uploads");
  const chunks = fs.readdirSync(chunkDir).filter((name) => name.startsWith(fileHash));
  chunks.sort((a, b) => parseInt(a.split("-")[1]) - parseInt(b.split("-")[1]));

  const filePath = path.join("uploads", fileName);
  const writeStream = fs.createWriteStream(filePath);
  chunks.forEach((chunk) => {
    const chunkPath = path.join(chunkDir, chunk);
    const chunkData = fs.readFileSync(chunkPath);
    writeStream.write(chunkData);
    fs.unlinkSync(chunkPath); // 删除分片
  });
  writeStream.end();
  res.send({ success: true });
});

3. 检查已上传分片

javascript 复制代码
app.get("/api/uploaded-chunks", (req, res) => {
  const { fileHash } = req.query;
  const chunkDir = path.join("uploads");
  const uploadedChunks = fs.readdirSync(chunkDir)
    .filter((name) => name.startsWith(fileHash))
    .map((name) => parseInt(name.split("-")[1]));
  res.send({ uploadedChunks });
});

总结

通过分片上传和断点续传技术,可以有效解决大文件上传中的网络不稳定、服务器压力大和用户体验差等问题。前端将文件分片并逐个上传,服务器接收分片并最终合并,同时支持断点续传功能。

相关推荐
编程猪猪侠11 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞15 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路37 分钟前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失94939 分钟前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue86840 分钟前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界1 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架