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

应用背景

在现代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 });
});

总结

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

相关推荐
聪明的墨菲特i3 分钟前
React与Vue:哪个框架更适合入门?
开发语言·前端·javascript·vue.js·react.js
时光少年4 分钟前
Android 副屏录制方案
android·前端
拉不动的猪11 分钟前
v2升级v3需要兼顾的几个方面
前端·javascript·面试
时光少年14 分钟前
Android 局域网NIO案例实践
android·前端
半兽先生29 分钟前
VueDOMPurifyHTML 防止 XSS(跨站脚本攻击) 风险
前端·xss
冴羽32 分钟前
SvelteKit 最新中文文档教程(20)—— 最佳实践之性能
前端·javascript·svelte
Jackson__39 分钟前
面试官:谈一下在 ts 中你对 any 和 unknow 的理解
前端·typescript
zpjing~.~1 小时前
css 二维码始终显示在按钮的正下方,并且根据不同的屏幕分辨率自动调整位置
前端·javascript·html
红虾程序员1 小时前
Linux进阶命令
linux·服务器·前端
yinuo1 小时前
uniapp在微信小程序中实现 SSE 流式响应
前端