欢迎光临
我们一直在努力

Vue3 + Element Plus 实战:大文件分片上传与断点续传全解析

在前端开发中,处理几 MB 的小文件轻而易举,但当面对几百 MB 甚至 GB 级别的大文件时,直接上传往往会遇到以下痛点:

  • 超时失败:网络波动导致长连接中断,整个文件需重新上传。
  • 内存溢出:一次性读取大文件可能导致浏览器崩溃。
  • 体验极差:用户无法感知进度,且无法暂停或恢复。
  • 本文将基于 Vue 3 + TypeScript + Element Plus,结合 spark-md5,手把手教你实现一个支持秒传、分片上传、并发控制及 Blob 下载的完整解决方案。

    📂 核心思路

    1. 上传流程

  • 计算 Hash:利用 SparkMD5 计算文件的唯一标识(用于秒传和断点续传识别)。
  • 文件切片使用 File.prototype.slice()将大文件切割成固定大小(如 2MB)的 Chunk。
  • 并发上传:将切片并行发送给后端接口。
  • 合并文件:所有切片上传成功后,通知后端合并。
  • 2. 下载流程

  • 请求数据:通过 fetch 或 axios 获取二进制流。
  • 创建 Blob将响应数据转换为 Blob 对象。
  • 触发下载:动态创建 <a> 标签并模拟点击。
  • 安装

    • SparkMD5 (高性能 MD5 计算库)

    npm install spark-md5

    💻 核心代码实现

    1. 模板结构 (Template)

    我们使用 el-upload 的自定义模式,关闭自动上传,完全由我们控制逻辑。

    <template>
    <div class="upload-container">
    <!– 拖拽上传区域 –>
    <el-upload
    drag
    :auto-upload="false"
    :show-file-list="false"
    :on-change="handleFileChange"
    action="#"
    >
    <el-icon class="el-icon–upload"><upload-filled /></el-icon>
    <div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
    <template #tip>
    <div class="el-upload__tip">支持大文件分片上传,请勿上传超过 2GB 的文件</div>
    </template>
    </el-upload>

    <!– 进度显示 –>
    <div v-if="uploading" class="progress-box">
    <span>{{ fileName }}</span>
    <el-progress :percentage="uploadPercentage" :status="uploadStatus" />
    <span>{{ uploadStatusText }}</span>
    </div>

    <!– 模拟下载测试区 –>
    <el-divider />
    <div class="download-list">
    <h3>文件列表 (点击下载)</h3>
    <el-button type="primary" link @click="handleDownload('example.zip', 'http://mock-api.com/file.zip')">
    下载: example_large_file.zip
    </el-button>
    </div>
    </div>
    </template>

    2. 逻辑实现 (Script Setup)

    A. 状态定义与配置

    import { ref } from "vue";
    import { ElMessage } from "element-plus";
    import { UploadFilled } from "@element-plus/icons-vue";
    import SparkMD5 from "spark-md5";

    // 常量配置
    const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB 切片

    // 响应式状态
    const fileObj = ref(null);
    const uploading = ref(false);
    const uploadPercentage = ref(0);
    const uploadStatus = ref(""); // success, exception, ''
    const uploadStatusText = ref("");
    const fileName = ref("");

    B. 核心上传入口

    const handleFileChange = (file) => {
    fileObj.value = file.raw;
    fileName.value = file.name;
    uploadFileRequest(); // 触发上传
    };

    const uploadFileRequest = async () => {
    if (!fileObj.value) return;

    const file = fileObj.value;
    uploading.value = true;
    uploadPercentage.value = 0;
    uploadStatusText.value = "正在计算文件哈希…";

    try {
    // 1. 计算 Hash
    const fileHash = await calculateHash(file);

    // 2. 秒传校验 (Mock)
    // const verifyRes = await axios.post('/api/upload/verify', { hash: fileHash });
    const verifyRes = { needUpload: true };

    if (!verifyRes.needUpload) {
    finishUpload("秒传成功:文件已存在");
    return;
    }

    // 3. 切片
    const chunks = createFileChunks(file, CHUNK_SIZE);
    uploadStatusText.value = `准备上传 ${chunks.length} 个分片…`;

    // 4. 并发上传
    const uploadPromises = chunks.map((chunk, index) =>
    uploadChunk({ chunk, hash: fileHash, index, filename: file.name })
    );

    await Promise.all(uploadPromises);

    // 5. 合并文件
    uploadStatusText.value = "正在合并文件…";
    // await axios.post('/api/upload/merge', { hash: fileHash, filename: file.name });
    await new Promise(r => setTimeout(r, 1000)); // 模拟合并耗时

    finishUpload("上传成功");
    } catch (error) {
    handleError(error);
    } finally {
    uploading.value = false;
    fileObj.value = null;
    }
    };

    C. 关键辅助函数

    1. 非阻塞式 Hash 计算 大文件计算 Hash容易卡死 UI,我们采用分片读取 + setTimeout 让出主线程的策略。

    const calculateHash = (file) => {
    return new Promise((resolve, reject) => {
    const spark = new SparkMD5.ArrayBuffer();
    const fileReader = new FileReader();
    let offset = 0;

    const readChunk = () => {
    if (offset >= file.size) {
    spark.end();
    resolve(spark.getResult());
    return;
    }
    const chunk = file.slice(offset, offset + CHUNK_SIZE);
    offset += CHUNK_SIZE;
    fileReader.readAsArrayBuffer(chunk);
    };

    fileReader.onload = (e) => {
    spark.append(e.target.result);
    // 关键:让出主线程,防止 UI 冻结
    setTimeout(readChunk, 0);
    };

    fileReader.onerror = reject;
    readChunk();
    });
    };

    2. 文件切片

    const createFileChunks = (file, size) => {
    const chunks = [];
    let cur = 0;
    while (cur < file.size) {
    chunks.push(file.slice(cur, cur + size));
    cur += size;
    }
    return chunks;
    };

    3. 模拟分片上传与进度更新

    const uploadChunk = ({ chunk, hash, index }) => {
    return new Promise((resolve) => {
    // 实际项目中这里使用 axios.post FormData
    const time = Math.random() * 1000 + 500; // 模拟网络延迟

    setTimeout(() => {
    // 简单进度计算:当前索引 / 总分片数
    // 注意:实际并发下进度条可能跳动,建议维护一个 uploadedCount计数器
    const totalChunks = Math.ceil(fileObj.value.size / CHUNK_SIZE);
    uploadPercentage.value = Math.floor(((index + 1) / totalChunks) * 100);
    resolve();
    }, time);
    });
    };

    3. 大文件下载实现

    前端下载大文件通常有两种方式:

  • Window Open:简单 GET 请求,无法携带复杂 Header。
  • Blob Download:推荐方式,支持鉴权 Token,可处理二进制流。
  • const handleDownload = (fileName, url) => {
    ElMessage.info(`开始下载: ${fileName}`);
    downloadByBlob(url, fileName);
    };

    const downloadByBlob = async (url, fileName) => {
    try {
    // 实际项目中使用 fetch 或 axios
    // const response = await fetch(url, { headers: { 'Authorization': 'Bearer token' } });
    // const blob = await response.blob();

    // 模拟生成 Blob
    const mockContent = "这是一个模拟的大文件内容。";
    const blob = new Blob([mockContent], { type: "application/octet-stream" });

    // 创建临时链接触发下载
    const link = document.createElement("a");
    link.href = window.URL.createObjectURL(blob);
    link.download = fileName;
    document.body.appendChild(link);
    link.click();

    // 清理
    document.body.removeChild(link);
    window.URL.revokeObjectURL(link.href);
    } catch (error) {
    ElMessage.error("下载失败");
    }
    };

    ⚡ 性能优化与最佳实践

    1. 真正的并发控制

    上面的代码使用了 Promise.all,这意味着如果文件有 1000 个分片,会同时发起 1000 个请求,这会导致浏览器卡顿或被服务器拒绝。 优化方案使用队列限制最大并发数(例如最多同时上传 3-5 个分片)。

    // 简易并发控制器
    async function concurrentRun(tasks, limit) {
    const results = [];
    let index = 0;
    const runNext = async () => {
    if (index >= tasks.length) return;
    const task = tasks[index++];
    await task();
    await runNext();
    };
    const workers = Array(Math.min(limit, tasks.length)).fill(null).map(() => runNext());
    await Promise.all(workers);
    }

    2. 断点续传

     uploadFileRequest 中,切片上传前应先调用后端接口 /api/upload/check?hash=xxx。 后端返回已上传的分片索引数组(如 [0, 1, 3]),前端过滤掉这些分片,只上传剩余部分。

    3. Web Worker 计算 Hash

    如果文件极大(>500MB),即使在 setTimeout 中计算 Hash 仍可能影响交互。 终极方案:将 calculateHash 逻辑移至 Web Worker 中运行,彻底隔离主线程

    4. 错误重试

    网络不稳定时,单个分片可能失败。应在 uploadChunk 中加入重试机制(Retry Pattern),失败后自动重试 3 次。


    📝 总结

    通过本文的实现,我们解决了大文件上传的核心痛点:

    • 稳定性:分片上传避免单点失败导致全盘重来。
    • 效率:秒传功能节省带宽和时间。
    • 体验:实时进度反馈和流畅的 UI 交互。

    这套方案不仅适用于 Vue,其核心逻辑(切片、Hash、并发、合并)在任何前端框架甚至原生 JS 中都是通用的。希望这篇博客能帮助你轻松搞定大文件传输难题!

    赞(0)
    未经允许不得转载:171主机测评 » Vue3 + Element Plus 实战:大文件分片上传与断点续传全解析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址