前言
在大文件上传实现专栏中,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
。
添加一个 script
: start: "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开发,可以访问文件系统,那么根据专栏前面的文章,和断点续传是相同道理的。但前端,该怎么实现呢?
我这里提供一个思路:我们通过分段下载,但分段粒度小一点,下载完了存一下相关信息,如果暂停下载,那么取消所有未完成的请求。继续下载,则根据刚刚存的信息,恢复下载。这个方案可能会造成总进度条在恢复下载的时候有一点回退,因为部分未完成的请求被取消但没有存信息,重新开始只能从已经完成的部分开始。也因此,分段粒度越小,所会造成的浪费也越小。
如果掘友们有其他思路,欢迎讨论~
题外话
今天写文章的时候,突然收到明天高铁票候补成功的信息,开心😄,这应该是过节前最后一篇文章了,不卷了。提前祝大家新年快乐,万事如意~