大文件下载、断点续传功能

实现文件下载断点续传功能:从零到一

前言

在现代Web应用中,大文件下载是一个常见的需求。为了提高用户体验,实现断点续传功能是必不可少的。本文将详细介绍如何使用Vue3和NestJS实现一个支持断点续传的文件下载功能。

技术栈

  • 前端:Vue3 + Element Plus
  • 后端:NestJS
  • 文件系统:Node.js fs模块

功能特点

  • 支持文件列表展示
  • 支持断点续传
  • 支持暂停/继续/停止操作
  • 实时显示下载进度
  • 优雅的错误处理

后端实现

typescript 复制代码
@Controller('download')
export class DownloadController {
  private readonly targetDir: string;

  constructor() {
    this.targetDir = path.resolve(process.cwd(), 'target');
    // 确保目录存在
    if (!fs.existsSync(this.targetDir)) {
      fs.mkdirSync(this.targetDir, { recursive: true });
    }
  }

  @Get(':filename')
  async downloadFile(@Param('filename') filename: string, @Res() res: Response) {
    try {
      const decodedFilename = decodeURIComponent(filename);
      const filePath = path.join(this.targetDir, decodedFilename);
      
      if (!fs.existsSync(filePath)) {
        return res.status(200).json({ 
          code: 200,
          message: '',
          requestedFile: decodedFilename,
          data: fs.readdirSync(this.targetDir)
        });
      }

      const stat = fs.statSync(filePath);
      const fileSize = stat.size;
      const range = res.req.headers.range;

      if (range) {
        // 处理断点续传
        const parts = range.replace(/bytes=/, '').split('-');
        const start = parseInt(parts[0], 10);
        const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
        const chunksize = (end - start) + 1;
        
        const file = fs.createReadStream(filePath, { start, end });
        
        res.writeHead(206, {
          'Content-Range': `bytes ${start}-${end}/${fileSize}`,
          'Accept-Ranges': 'bytes',
          'Content-Length': chunksize,
          'Content-Type': 'application/octet-stream',
        });
        
        file.pipe(res);
      } else {
        // 普通下载
        res.writeHead(200, {
          'Content-Length': fileSize,
          'Content-Type': 'application/octet-stream',
          'Content-Disposition': `attachment; filename=${encodeURIComponent(decodedFilename)}`,
          'Accept-Ranges': 'bytes',
        });
        
        fs.createReadStream(filePath).pipe(res);
      }
    } catch (error) {
      return res.status(500).json({ 
        message: '下载文件失败',
        error: error.message 
      });
    }
  }
}

前端实现

vue 复制代码
<template>
  <div class="download-container">
    <h1>文件下载</h1>
    <div class="file-list">
      <el-table v-loading="loading" :data="fileList" style="width: 100%">
        <el-table-column prop="filename" label="文件名" />
        <el-table-column prop="size" label="文件大小" />
        <el-table-column prop="createdAt" label="创建时间">
          <template #default="scope">
            {{ formatDate(scope.row.createdAt) }}
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template #default="scope">
            <div class="operation-column">
              <template v-if="!downloadingStates[scope.row.filename]">
                <el-button type="primary" @click="startDownload(scope.row.filename)">
                  下载
                </el-button>
              </template>
              <template v-else>
                <el-button 
                  v-if="!downloadingStates[scope.row.filename].paused"
                  type="warning" 
                  @click="pauseDownload(scope.row.filename)"
                >
                  暂停
                </el-button>
                <el-button 
                  v-else
                  type="success" 
                  @click="resumeDownload(scope.row.filename)"
                >
                  继续
                </el-button>
                <el-button 
                  type="danger" 
                  @click="stopDownload(scope.row.filename)"
                >
                  停止
                </el-button>
              </template>
              <el-progress 
                v-if="downloadingStates[scope.row.filename]"
                :percentage="downloadProgress[scope.row.filename] || 0"
                :stroke-width="15"
                :show-text="false"
                style="width: 100px; margin-left: 10px;"
              />
            </div>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

核心功能实现

1. 下载控制

typescript 复制代码
const startDownload = async (filename) => {
  if (downloadingStates.value[filename]) return;

  downloadingStates.value[filename] = {
    paused: false,
    receivedLength: 0,
    totalSize: 0
  };

  downloadControllers.value[filename] = new AbortController();
  await downloadFile(filename);
};

const pauseDownload = (filename) => {
  if (downloadingStates.value[filename]) {
    downloadingStates.value[filename].paused = true;
    downloadControllers.value[filename].abort();
  }
};

const resumeDownload = async (filename) => {
  if (downloadingStates.value[filename]?.paused) {
    downloadingStates.value[filename].paused = false;
    downloadControllers.value[filename] = new AbortController();
    await downloadFile(filename);
  }
};

const stopDownload = (filename) => {
  if (downloadingStates.value[filename]) {
    downloadControllers.value[filename].abort();
    delete downloadingStates.value[filename];
    delete downloadControllers.value[filename];
    downloadProgress.value[filename] = 0;
    downloading.value = '';
    ElMessage.info('下载已停止');
  }
};

2. 文件下载实现

typescript 复制代码
const downloadFile = async (filename) => {
  const state = downloadingStates.value[filename];
  
  try {
    const response = await fetchWithRetry(`http://localhost:3030/download/${filename}`, {
      headers: {
        'Accept': 'application/octet-stream',
        'Range': `bytes=${state.receivedLength}-`
      },
      signal: downloadControllers.value[filename].signal
    });

    if (!downloadingStates.value[filename]) return;

    const contentLength = response.headers.get('Content-Length');
    const totalSize = parseInt(contentLength, 10) + state.receivedLength;
    state.totalSize = totalSize;
    
    const reader = response.body.getReader();
    const chunks = [];
    let receivedLength = state.receivedLength;

    while(true) {
      if (!downloadingStates.value[filename]) return;
      if (state.paused) break;

      const {done, value} = await reader.read();
      if (done) break;
      
      chunks.push(value);
      receivedLength += value.length;
      state.receivedLength = receivedLength;
      
      const progress = Math.round((receivedLength / totalSize) * 100);
      downloadProgress.value[filename] = progress;
    }

    if (!state.paused && downloadingStates.value[filename]) {
      const blob = new Blob(chunks);
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      document.body.removeChild(a);
      ElMessage.success('下载成功');
      
      delete downloadingStates.value[filename];
      delete downloadControllers.value[filename];
      downloadProgress.value[filename] = 0;
    }
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('下载已暂停或停止');
    } else {
      ElMessage.error(error.message || '下载失败,请重试');
      delete downloadingStates.value[filename];
      delete downloadControllers.value[filename];
      downloadProgress.value[filename] = 0;
    }
  }
};

后续优化方向

  1. 添加下载速度显示
  2. 实现多文件并行下载
  3. 添加下载历史记录
  4. 实现下载队列管理
  5. 添加文件校验功能

希望这篇文章对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论。

相关推荐
—Qeyser39 分钟前
用 Deepseek 写的uniapp血型遗传查询工具
前端·javascript·ai·chatgpt·uni-app·deepseek
codingandsleeping40 分钟前
HTTP1.0、1.1、2.0 的区别
前端·网络协议·http
小满blue42 分钟前
uniapp实现目录树效果,异步加载数据
前端·uni-app
天天扭码2 小时前
零基础 | 入门前端必备技巧——使用 DOM 操作插入 HTML 元素
前端·javascript·dom
咖啡虫3 小时前
css中的3d使用:深入理解 CSS Perspective 与 Transform-Style
前端·css·3d
拉不动的猪3 小时前
设计模式之------策略模式
前端·javascript·面试
旭久3 小时前
react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR
前端·javascript·react.js
独行soc3 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf
uhakadotcom4 小时前
Google Earth Engine 机器学习入门:基础知识与实用示例详解
前端·javascript·面试
麓殇⊙4 小时前
Vue--组件练习案例
前端·javascript·vue.js