一文带你搞懂前端大文件上传

本文介绍大文件上传的概念,带你搞懂什么是大文件上传以及大文件上传解决了什么痛点。另外还将介绍大文件上传的一些功能点以及难点。最后推出一款非常好用的大文件上传库,可以做到开箱即用,无论是vue2/vu3、react还是jquery原生js开发。并且内置了诸如错误重传策略,断点续传、重传、重试和暂停等功能,即使你从未做过大文件上传,你也可以使用的随心应手。

npm库:enlarge-file-upload

线上演示地址:jiang-12-13.com:8988/

概念

1. 什么是大文件上传?

大文件上传是指将体积较大的文件(通常指几十MB到GB级别的文件)从客户端(如浏览器)传输到服务器的过程。由于文件体积较大,直接使用传统的文件上传方式可能会导致上传失败、网络超时或服务器资源占用过高等问题。因此,大文件上传通常需要采用分片上传、断点续传、并行上传等特殊技术手段来确保上传的稳定性和效率。


2. 大文件上传能解决什么问题?

大文件上传主要解决以下问题:

  • 网络稳定性问题:避免因网络波动或中断导致上传失败。
  • 服务器资源限制:减少服务器内存和CPU资源的占用,防止因上传大文件导致服务器崩溃。
  • 用户体验问题:提供上传进度、断点续传等功能,提升用户在使用大文件上传时的体验。
  • 文件完整性验证:确保上传的文件在传输过程中没有损坏或丢失。

3. 大文件上传和普通文件上传的优势在哪里?

  • 支持大体积文件:普通文件上传通常难以处理大文件(如GB级别),而大文件上传通过分片技术可以解决这一问题。
  • 断点续传:普通文件上传一旦中断需要重新上传,而大文件上传支持从中断点继续上传,节省时间和带宽。
  • 并行上传:大文件上传可以将文件分成多个分片并行上传,提升上传速度。
  • 资源优化:通过对文件分片上传,减少服务器内存占用,提升服务器性能。
  • 进度监控:大文件上传可以提供更精准的上传进度显示,方便用户了解上传状态。

4. 大文件上传需要攻破的功能点有哪些?(从前端角度)

从前端角度来看,实现大文件上传需要解决以下关键功能点:

(1) 文件分片(Chunking)

  • 将大文件按照固定大小(如 1MB 或 5MB)分成多个小文件(分片)。
  • 使用 File API(如 File.slice())进行文件切割。

(2) 分片上传

  • 将每个分片通过 XMLHttpRequestFetch API 上传到服务器。
  • 支持并行上传(同时上传多个分片)以提升效率。

(3) 断点续传(Resume Upload)

  • 在上传中断后,能够记录已上传的分片,下次继续上传未完成的分片。
  • 需要前端与服务器配合,通常服务器会记录已上传的分片信息(如文件哈希值或分片索引)。

(4) 上传进度监控

  • 使用 XMLHttpRequestprogress 事件或 Fetch APIReadableStream 实时监控上传进度。
  • 计算已上传文件的百分比并展示给用户。

(5) 文件完整性校验

  • 在上传完成后,计算文件的哈希值(如 MD5 或 SHA-256)并与服务器端校验,确保文件完整无误。
  • 使用 FileReader 读取文件内容并生成哈希值。

(6) 错误处理与重试机制

  • 处理上传过程中可能出现的网络错误、服务器错误等问题。
  • 为每个分片设置重试机制,确保上传失败的分片能够重新上传。

(7) 并发控制

  • 控制同时上传的分片数量,避免过多请求占用带宽或服务器资源。
  • 使用队列或 Promise 池管理并发上传任务。

(8) 用户体验优化

  • 提供清晰的 UI 反馈,如进度条、上传速度、剩余时间等。
  • 支持拖拽上传、文件选择、批量上传等功能。

(9) 安全性考虑

  • 防止恶意文件上传,使用文件类型校验、文件大小限制等手段。
  • 对敏感数据进行加密传输(如使用 HTTPS)。

大文件上传库介绍

需要自己从0到1手写一个大文件上传库确实困难,有非常多的功能点需要实现,并非短期内可以做到。这里推荐一个非常好用的大文件上传库 enlarge-file-upload 实现了上述的所有功能,可以做到开箱即用,无论是vue2/vu3、react还是jquery原生js开发,都可以使用这个库

1. 安装

css 复制代码
npm i enlarge-file-upload

2. 参数介绍

markdown 复制代码
/**
 * 本库计算文件hash值,使用的是 crypto-js 依赖包进行计算,版本为 4.0.0 版
 * 链接:https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js
 */

/**
 * descript 入参详解
 * @param {Number} chunkSize? 文件单个切片大小,默认5MB
 * @param {Number} concurrency? 并发上传数量,默认5
 * @param {Number} maxRetries? 失败重试次数,默认3
 * @param {Number} startOffset? 断点续传偏移量,默认0
 * @param {Array} includeChunks? 需要上传的切片索引,默认上传所有切片
 * 如果startOffset和includeChunks参数同时存在,且startOffset不为0,默认优先使用startOffset参数
 * @param {Boolen} hash? 是否开启hash计算,默认不开启,可选参数
 * @param {Boolen} awaitHash? 是否等待hash值,默认等待,可选参数(仅在hash为true时生效)
 * awaitHash参数,如果上传文件较小时可以开启等待;如果文件超大,建议关闭,大文件hash值计算时间较长,避免等待阻塞主线程
 * @param {Function} uploadFunction 上传函数,必传参数
 * @param {Function} onProgress? 上传进度回调函数,可选
 * @param {Function} onSpeed? 上传速度回调函数,可选
 * @param {Function} onSuccess? 上传成功后回调函数,可选
 * @param {Function} beginHash? 开始计算Hash值回调函数,可选(只会在上传前调用一次,且hash.open必须true)
 * @param {Function} endHash? Hash值计算完毕回调函数,可选(只会在上传前调用一次,且hash.open必须true)
 */
// 参数示例
const config = {
  chunkSize: 5 * 1024 * 1024,
  concurrency: 5,
  maxRetries: 3,
  startOffset:0,
  includeChunks,
  hash: false,
  awaitHash: false,
  uploadFunction,
  onProgress,
  onSuccess
  onSpeed,
  beginHash,
  endHash
};

/**
 * descript fileUploadTool方法返回一个对象,包含上传方法、暂停方法、继续方法等
 * upload 上传方法
 * pause 暂停方法
 * resume 继续方法
 * state 状态对象,包含上传进度、hash值、上传速度等(具体请看对应的TS属性State)
 */
// 方法调用
const { upload, pause, resume, state } = fileUploadTool(config);

使用案例

jquery原生 js 中使用示例

xml 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>文件上传</title>
  </head>
  <script src="https://cdn.jsdelivr.net/npm/enlarge-file-upload/dist/upload.min.js"></script>
  <!-- 引入 Axios 库,用于发送 HTTP 请求 -->
  <script src="https://cdn.jsdelivr.net/npm/axios"></script>

  <body>
    <input type="file" id="fileInput" />
    <button id="pauseButton">暂停上传</button>
    <button id="resumeButton">继续上传</button>
    <div id="progress">上传进度:0%</div>
    <div id="speed">上传速度:0 MB/s</div>
    <script>
      // 定义上传函数
      async function uploadFunction({ chunk, index, hash, cancelToken }) {
        const formData = new FormData();
        formData.append("chunk", chunk);
        formData.append("hash", hash);
        formData.append("index", index);
        await axios.post("http://localhost:3000/api/users", formData, {
          cancelToken,
        });
      }

      // 使用示例
      const config = {
        chunkSize: 5 * 1024 * 1024, // 5MB
        concurrency: 5,
        maxRetries: 3,
        // startOffset: 6, // 从索引为10的切片位置开始传
        // includeChunks:[1,6], // 只上传索引为1和6的切片,只有startOffset为0或空时才生效
        uploadFunction,
        onProgress: (progress) => {
          document.getElementById(
            "progress"
          ).innerText = `上传进度:${state.progress.toFixed(2)}%`;
        },
        onSuccess: () => {
          console.log("上传完毕");
        },
        onSpeed: (speed) => {
          document.getElementById("speed").innerText = `上传速度:${speed}`;
        },
      };

      const { upload, pause, resume, state } = createUploader(config);
      const fileInput = document.getElementById("fileInput");
      fileInput.addEventListener("change", () => {
        const file = fileInput.files[0];
        upload(file);
      });
      // 暂停上传
      document.getElementById("pauseButton").addEventListener("click", () => {
        pause();
      });
      // 继续上传
      document.getElementById("resumeButton").addEventListener("click", () => {
        resume();
      });
    </script>
  </body>
</html>

在 vue3 中使用

xml 复制代码
<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="handlePause">暂停上传</button>
    <button @click="handleResume">继续上传</button>
    <div>上传进度:{{ progress.toFixed(2) }}%</div>
    <div>上传速度:{{ speed }}</div>
  </div>
</template>

<script lang="ts" setup>
import { ref, computed } from "vue";
import createUploader from "enlarge-file-upload";
import type { Config, UploadOptions } from "enlarge-file-upload"; // TS导入类型
import axios from "axios";

const progress = ref<number>(0);
const speed = ref<string>("0 MB/s");

// 定义上传函数
async function uploadFunction({
  chunk,
  index,
  hash,
  cancelToken,
}: UploadOptions): Promise<void> {
  const formData = new FormData();
  formData.append("chunk", chunk);
  formData.append("hash", hash);
  formData.append("index", index.toString());
  await axios.post("http://localhost:3000/api/users", formData, {
    cancelToken,
  });
}

// 使用 computed 来生成 uploader 配置,类型为 Config
const uploaderConfig = computed<Config>(() => ({
  chunkSize: 0.01 * 1024 * 1024, // 5MB
  concurrency: 5,
  maxRetries: 3,
  uploadFunction,
  onProgress: (progressValue: number) => {
    progress.value = progressValue;
  },
  onSuccess: () => {
    console.log("上传完毕");
  },
  onSpeed: (speedValue: string) => {
    speed.value = speedValue;
  },
}));

const uploader = ref<ReturnType<typeof createUploader> | null>(null);
uploader.value = createUploader(uploaderConfig.value);

const handleFileChange = (event: Event) => {
  const target = event.target as HTMLInputElement;
  const file = target.files?.[0];
  if (file) {
    uploader.value?.upload(file);
  }
};

const handlePause = () => {
  uploader.value?.pause();
};

const handleResume = () => {
  uploader.value?.resume();
};
</script>

<style scoped></style>

在 vue2 中使用

xml 复制代码
<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="handlePause">暂停上传</button>
    <button @click="handleResume">继续上传</button>
    <div>上传进度:{{ progress.toFixed(2) }}%</div>
    <div>上传速度:{{ speed }}</div>
  </div>
</template>

<script>
import Vue from "vue";
import createUploader from "enlarge-file-upload";
import axios from "axios";

export default Vue.extend({
  data() {
    return {
      progress: 0, // 进度
      speed: "0 MB/s", // 速度
      uploader: null, // 上传器实例
    };
  },
  methods: {
    // 定义上传函数
    async uploadFunction({ chunk, index, hash, cancelToken }) {
      const formData = new FormData();
      formData.append("chunk", chunk);
      formData.append("hash", hash);
      formData.append("index", index.toString());
      await axios.post("http://localhost:3000/api/users", formData, {
        cancelToken,
      });
    },

    // 文件选择处理
    handleFileChange(event) {
      const file = event.target.files?.[0];
      if (file && this.uploader) {
        this.uploader.upload(file);
      }
    },

    // 暂停上传
    handlePause() {
      if (this.uploader) {
        this.uploader.pause();
      }
    },

    // 继续上传
    handleResume() {
      if (this.uploader) {
        this.uploader.resume();
      }
    },
  },
  created() {
    const uploaderConfig = {
      chunkSize: 5 * 1024 * 1024, // 5MB
      concurrency: 5,
      maxRetries: 3,
      uploadFunction: this.uploadFunction,
      onProgress: (progressValue) => {
        this.progress = progressValue;
      },
      onSuccess: () => {
        console.log("上传完毕");
      },
      onSpeed: (speedValue) => {
        this.speed = speedValue;
      },
    };

    // 创建上传器实例
    this.uploader = createUploader(uploaderConfig);
  },
});
</script>

<style scoped></style>

React 中使用示例

在 react 中,这里的 useMemo 是必须要使用的,避免组件渲染的时候,重新创建 uploader 对象

javascript 复制代码
import React, { useState, useMemo } from "react";
import createUploader from "enlarge-file-upload";
import type { Config, UploadOptions } from "enlarge-file-upload";
import axios from "axios";

const FileUpload = () => {
  const [progress, setProgress] = useState(0);
  const [speed, setSpeed] = useState("0 MB/s");

  // 定义上传函数
  async function uploadFunction({
    chunk,
    index,
    hash,
    cancelToken,
  }: UploadOptions) {
    const formData = new FormData();
    formData.append("chunk", chunk);
    formData.append("hash", hash);
    formData.append("index", index);
    await axios.post("http://localhost:3000/api/users", formData, {
      cancelToken,
    });
  }

  const uploaderConfig: Config = useMemo(
    () => ({
      chunkSize: 5 * 1024 * 1024, // 5MB
      concurrency: 5,
      maxRetries: 3,
      // startOffset: 6, // 从索引为10的切片位置开始传
      // includeChunks:[1,6], // 只上传索引为1和6的切片,只有startOffset为0或空时才生效
      uploadFunction,
      onProgress: (progress) => {
        setProgress(progress);
      },
      onSuccess: () => {
        console.log("上传完毕");
      },
      onSpeed: (speed) => {
        setSpeed(speed);
      },
    }),
    []
  );

  const uploader = useMemo(
    () => createUploader(uploaderConfig),
    [uploaderConfig]
  );

  const handleFileChange = (event) => {
    const file = event.target.files[0];
    uploader?.upload(file);
  };

  const handlePause = () => {
    uploader?.pause();
  };

  const handleResume = () => {
    uploader?.resume();
  };

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <button onClick={handlePause}>暂停上传</button>
      <button onClick={handleResume}>继续上传</button>
      <div>上传进度:{progress.toFixed(2)}%</div>
      <div>上传速度:{speed}</div>
    </div>
  );
};

export default FileUpload;

封装为 react Hooks

一个简单的封装,如有特殊需求,可以基于这个 hooks 进行二次修改

ini 复制代码
import { useState, useMemo, useCallback } from "react";
import createUploader from "enlarge-file-upload";
import type { Config, UploadOptions } from "enlarge-file-upload";
import axios from "axios";

const useFileUploader = () => {
  const [progress, setProgress] = useState(0);
  const [speed, setSpeed] = useState("0 MB/s");

  const uploadFunction = useCallback(
    async ({ chunk, index, hash, cancelToken }: UploadOptions) => {
      const formData = new FormData();
      formData.append("chunk", chunk);
      formData.append("hash", hash);
      formData.append("index", index);
      await axios.post("http://localhost:3000/api/users", formData, {
        cancelToken,
      });
    },
    []
  );

  const uploaderConfig: Config = useMemo(
    () => ({
      chunkSize: 5 * 1024 * 1024, // 5MB
      concurrency: 5,
      maxRetries: 3,
      // startOffset: 6, // 从索引为10的切片位置开始传
      // includeChunks:[1,6], // 只上传索引为1和6的切片,只有startOffset为0或空时才生效
      uploadFunction,
      onProgress: (progress) => {
        setProgress(progress);
      },
      onSuccess: () => {
        console.log("Upload complete");
      },
      onSpeed: (speed) => {
        setSpeed(speed);
      },
    }),
    [uploadFunction]
  );

  const uploader = useMemo(
    () => createUploader(uploaderConfig),
    [uploaderConfig]
  );

  const uploadFile = useCallback(
    (file) => {
      uploader?.upload(file);
    },
    [uploader]
  );

  const pauseUpload = useCallback(() => {
    uploader?.pause();
  }, [uploader]);

  const resumeUpload = useCallback(() => {
    uploader?.resume();
  }, [uploader]);

  return {
    progress,
    speed,
    uploadFile,
    pauseUpload,
    resumeUpload,
  };
};

export default useFileUploader;

使用上面封装好的 Hooks 示例

javascript 复制代码
import React from "react";
import useFileUploader from "./useFileUploader.tsx";

const FileUpload = () => {
  const { progress, speed, uploadFile, pauseUpload, resumeUpload } =
    useFileUploader();

  const handleFileChange = (event) => {
    const file = event.target.files[0];
    uploadFile(file);
  };

  return (
    <div>
      <input type="file" onChange={handleFileChange} />
      <button onClick={pauseUpload}>Pause Upload</button>
      <button onClick={resumeUpload}>Resume Upload</button>
      <div>Upload Progress: {progress.toFixed(2)}%</div>
      <div>Upload Speed: {speed}</div>
    </div>
  );
};

export default FileUpload;

参考地址:www.npmjs.com/package/enl...

相关推荐
JosieBook5 分钟前
【web应用】Maven:Java 生态的构建与依赖管理利器
java·前端·maven
典学长编程7 分钟前
前端开发(HTML,CSS,VUE,JS)从入门到精通!第六天(Vue)
javascript·css·vue.js·vue·html
鲸落落丶10 分钟前
前端三大核心要素以及前后端通讯
javascript·css·html·jquery
OEC小胖胖13 分钟前
幕后英雄 —— Background Scripts (Service Worker)
开发语言·前端·javascript·浏览器·web·扩展
Monika Zhang13 分钟前
React框架深度解析与主流前端框架对比
前端·react.js·前端框架
胡斌附体25 分钟前
小程序省市级联组件使用
前端·javascript·小程序·uniapp·picker级联组件
江城开朗的豌豆27 分钟前
Redux三剑客:揭秘reducer这个'状态改造师'的魔法
前端·javascript·react.js
江城开朗的豌豆30 分钟前
setState vs replaceState:React状态更新的'温柔一刀'与'彻底翻脸'
前端·javascript·react.js
Bdygsl3 小时前
前端开发:CSS(2)—— 选择器
前端·css
斯~内克3 小时前
CSS包含块与百分比取值机制完全指南
前端·css·tensorflow