大文件系列拓展-分段下载功能(Vue+Express)

前言

大文件上传实现专栏中,4篇文章带掘友们从0到1,用express+vue实现了大文件上传的主流程:包括了切片上传、断点续传、秒传等热门功能。

今天来实现跟上传相反的功能,那就大文件分段下载功能。

需求分析

大文件分段下载,跟大文件上传时要切片的初心一样,就是为了"快"。如果能够同时下载一个大文件的多个片段,然后组装起来,那将能够充分利用我们的网络资源,加速下载。

后端静态文件服务

sh 复制代码
# 新建一个后端项目
mkdir server-by-express && cd server-by-express && npm init -y
# 安装需要的依赖
npm i express fs-extra cors nodemon

用vscode打开,新建一个index.js文件,输入如下内容:

js 复制代码
const express = require("express");
const fs = require("fs-extra");
const path = require("path");

// 静态资源目录
const PUBLIC_DIR = path.resolve(__dirname, "public");
// 确保静态资源目录存在
fs.ensureDir(PUBLIC_DIR);

const app = express();
// 静态资源服务
app.use(express.static(PUBLIC_DIR));

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

然后新建一个public目录,在里面放一个index.html

添加一个 scriptstart: "nodemon index.js", 执行 npm run start跑起来:

然后放一个测试的视频文件 video-for-test.mp4,在 index.html 引入展示看看

效果是这样的:

好了,以上都是为了证明静态服务功能没问题。

前端搭建

为了后面分段下载的美观性,我们用vite+vue快速搭建页面。

引入 element-plus

js 复制代码
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";

const app = createApp(App);

app.use(ElementPlus);
app.mount("#app");

把无关文件删除掉,只留下 App.vue

html 复制代码
<script setup>
import { ref } from "vue";
const filename = ref("video-for-test.mp4");
</script>

<template>
  <div class="wrapper">
    <el-input v-model="filename" placeholder="文件名"></el-input>
  </div>
</template>

<style scoped>
.wrapper {
  padding: 20px;
  height: 100vh;
}
</style>

效果如下:

Range 分段下载

先添加一个按钮用来触发下载

http请求头部有个Range字段,可以用于请求部分文件段, 具体介绍和语法,看 MDN

但前提得知道要请求的文件大小是多少,然后才能根据分段大小设置Range拿到文件片段

这个时候,Head请求派上用场了,可以只在header获取文件的大小类型信息。我们用 axios 来发送请求,先安装下 npm i axios, 然后试一下

然而发生跨域了,浏览器阻止我们拿到跨域请求响应的内容

express 里面设置一下允许跨域

js 复制代码
cons cors = require('cors')
...
app.use(cors());

再试一下

响应头部包括了 accept-range: bytes 支持分段请求;并且也拿到了文件的类型和大小信息

接下来,实现分段请求逻辑,思路是线性的,直接看注释很清晰

js 复制代码
const handleDownload = async () => {
  const res = await axios.head(`http://localhost:3000/${filename.value}`);
  // 获取文件信息
  const chunkSize = 1024 * 1024 * 100; // 100MB
  const totalSize = res.headers["content-length"];
  const mime = res.headers["content-type"];
  const count = Math.ceil(totalSize / chunkSize);

  // 并发分段下载
  const tasks = Array.from({ length: count }, (_, i) => {
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, totalSize);
    return axios.get(`http://localhost:3000/${filename.value}`, {
      headers: {
        Range: `bytes=${start}-${end - 1}`,
      },
      responseType: "blob",
    });
  });

  // 合并分段
  const blobs = await Promise.all(tasks);
  const blob = new Blob(
    blobs.map((blob) => blob.data),
    { type: mime }
  );

  // 保存为文件
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = filename.value;
  link.click();
  link.remove();
};

来看看效果:

我们来加个进度显示吧

html 复制代码
<div class="progress-wrapper">
  <div
    class="progress-item"
    v-for="(progress, key) in progressInfo"
    :key="key"
  >
    <div class="label">{{ key }}</div>
    <el-progress type="circle" :percentage="progress" />
  </div>
</div>
css 复制代码
.progress-wrapper {
  display: flex;
  flex-wrap: wrap;
  margin-top: 20px;
}

效果如下:

那么到这里range请求进行分段下载功能实现了。

自定义接口分段下载

上面是通过 http range 头部请求服务器静态文件实现的分段下载,那么问题来了,如果服务器不支持range呢?或者大文件不是放在静态服务里面,通过流获取文件呢?阁下当如何应对?

那就得自己接口实现呀,思路保持一致即可

  • head 获取信息接口
js 复制代码
const mime = require("mime-types");
...
app.head("/download/:filename", async (req, res) => {
  // 获取文件名
  const { filename } = req.params;
  const filepath = path.resolve(PUBLIC_DIR, filename);
  // 获取文件信息
  const fileInfo = await fs.stat(filepath);
  res.header("Content-Length", fileInfo.size);
  res.header("Content-Type", mime.lookup(filepath));
  res.end();
})
...

用 postman 测试一下:

  • get 下载接口
js 复制代码
app.get("/download/:filename", async (req, res) => {
  // 获取文件名
  const { filename } = req.params;
  const filepath = path.resolve(PUBLIC_DIR, filename);
  // 获取文件信息
  const fileInfo = await fs.stat(filepath);
  // 读取文件的起始位置
  const start = req.query.start ? parseInt(req.query.start) : 0;
  // 读取文件的结束位置
  const end = req.query.end ? parseInt(req.query.end) : fileInfo.size;
  // 创建可读流
  const readStream = fs.createReadStream(filepath, { start, end });
  // 设置响应头
  res.header("Content-Length", end - start + 1);
  // 管道流
  readStream.pipe(res);
});

这里有个细节点:我们得设置响应头的Content-Length,且不能设置少了,不然流的接收会截断,导致文件打不开,另外没有设置,我们的进度也会没有e.total而报错,因为axios下载回调里面的total就是来自Content-Length

用 postman 试了一下,没报错,但看不出来什么。

接一下前端添加一个按钮,来试一下下载效果。

前端页面改一下请求接口,请求参数。

看看效果。

总结

这篇文章,图文并茂,从0到1实现了大文件分段下载功能:包括常见的range分段下载,也通过自定义接口实现了分段下载。代码放到gitee, 视频文件就不放上去了,拉取代码之后自己搞个文件试试效果。

对了,这里同样有个并发问题:参考专栏上一篇文章《大文件上传实现-并发控制功能》

思考

不知道掘友们,有没有注意到一个常见的功能:"暂停下载",比如网盘文件下载可以暂停,然后可以恢复继续下载。那么这个功能该怎么实现呢?如果是electron开发,可以访问文件系统,那么根据专栏前面的文章,和断点续传是相同道理的。但前端,该怎么实现呢?

我这里提供一个思路:我们通过分段下载,但分段粒度小一点,下载完了存一下相关信息,如果暂停下载,那么取消所有未完成的请求。继续下载,则根据刚刚存的信息,恢复下载。这个方案可能会造成总进度条在恢复下载的时候有一点回退,因为部分未完成的请求被取消但没有存信息,重新开始只能从已经完成的部分开始。也因此,分段粒度越小,所会造成的浪费也越小。

如果掘友们有其他思路,欢迎讨论~

题外话

今天写文章的时候,突然收到明天高铁票候补成功的信息,开心😄,这应该是过节前最后一篇文章了,不卷了。提前祝大家新年快乐,万事如意~

相关推荐
Dread_lxy25 分钟前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
用户3157476081351 小时前
成为程序员的必经之路” Git “,你学会了吗?
面试·github·全栈
龙猫蓝图2 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js
peachSoda72 小时前
随手记:简单实现纯前端文件导出(XLSX)
前端·javascript·vue.js
Tttian6223 小时前
Vue全栈开发旅游网项目(11)-用户管理前端接口联调
前端·vue.js·django
龙猫蓝图4 小时前
vue el-date-picker 日期选择 回显后成功后无法改变的解决办法
前端·javascript·vue.js
刘志辉5 小时前
Pure Adminrelease(水滴框架配置)
vue.js
工业互联网专业5 小时前
Python毕业设计选题:基于Django+uniapp的公司订餐系统小程序
vue.js·python·小程序·django·uni-app·源码·课程设计
黄景圣6 小时前
CURD低代码程序设计
前端·vue.js·后端
lin-lins6 小时前
Vue 模板编译原理
前端·javascript·vue.js