Express文件下载:大文件下载和断点续传
痛点场景:为什么需要专业的文件下载方案?
在日常Web开发中,文件下载看似简单,但当遇到大文件、网络不稳定、需要断点续传等复杂场景时,简单的下载方案往往力不从心。你是否遇到过:
- 大文件下载中途失败,需要重新开始?
- 用户网络不稳定,下载体验差?
- 需要支持多线程下载提升速度?
- 服务器带宽被大文件下载占满?
Express框架提供了强大的文件下载能力,本文将深入解析如何实现专业级的大文件下载和断点续传功能。
Express基础下载功能
1. 基本文件下载
Express通过res.download()方法提供简单的文件下载功能:
const express = require('express');
const path = require('path');
const app = express();
// 基本文件下载
app.get('/download/simple', (req, res) => {
const filePath = path.join(__dirname, 'files', 'document.pdf');
res.download(filePath);
});
// 自定义文件名下载
app.get('/download/custom-name', (req, res) => {
const filePath = path.join(__dirname, 'files', 'report.pdf');
res.download(filePath, '季度报告.pdf');
});
// 带选项的下载
app.get('/download/with-options', (req, res) => {
const filePath = path.join(__dirname, 'files', 'large-file.zip');
res.download(filePath, {
maxAge: '1h', // 缓存1小时
headers: {
'X-File-Type': 'archive'
}
});
});
2. 下载功能核心原理
Express的下载功能基于send模块实现,支持以下特性:
大文件下载优化策略
1. 分块传输(Chunked Transfer)
对于大文件,分块传输是必须的:
app.get('/download/large-file', (req, res) => {
const filePath = path.join(__dirname, 'files', 'large-video.mp4');
const stat = fs.statSync(filePath);
const fileSize = stat.size;
// 获取Range头信息
const range = req.headers.range;
if (range) {
// 解析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': 'video/mp4',
'Content-Disposition': 'attachment; filename="large-video.mp4"'
});
// 管道传输
file.pipe(res);
} else {
// 完整文件下载
res.setHeader('Content-Length', fileSize);
res.setHeader('Content-Type', 'video/mp4');
res.setHeader('Content-Disposition', 'attachment; filename="large-video.mp4"');
fs.createReadStream(filePath).pipe(res);
}
});
2. 流量控制和限速
防止服务器带宽被耗尽:
const throttle = require('throttle');
app.get('/download/limited', (req, res) => {
const filePath = path.join(__dirname, 'files', 'large-file.iso');
const stat = fs.statSync(filePath);
// 限速1MB/s
const throttleStream = new throttle(1024 * 1024);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', 'attachment; filename="large-file.iso"');
fs.createReadStream(filePath)
.pipe(throttleStream)
.pipe(res);
});
断点续传完整实现
1. 服务端断点续传支持
app.get('/download/resumable/:filename', (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'files', filename);
try {
const stat = fs.statSync(filePath);
const fileSize = stat.size;
// 获取Range头
const range = req.headers.range;
if (range) {
// 解析Range头
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
if (start >= fileSize || end >= fileSize) {
// 无效的Range请求
res.status(416).header('Content-Range', `bytes */${fileSize}`).end();
return;
}
const chunksize = (end - start) + 1;
const file = fs.createReadStream(filePath, { start, end });
res.status(206);
res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
res.setHeader('Accept-Ranges', 'bytes');
res.setHeader('Content-Length', chunksize);
res.setHeader('Content-Type', getContentType(filename));
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
file.pipe(res);
} else {
// 完整文件下载
res.setHeader('Content-Length', fileSize);
res.setHeader('Content-Type', getContentType(filename));
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.setHeader('Accept-Ranges', 'bytes');
fs.createReadStream(filePath).pipe(res);
}
} catch (error) {
if (error.code === 'ENOENT') {
res.status(404).send('File not found');
} else {
res.status(500).send('Server error');
}
}
});
function getContentType(filename) {
const ext = path.extname(filename).toLowerCase();
const contentTypes = {
'.pdf': 'application/pdf',
'.mp4': 'video/mp4',
'.zip': 'application/zip',
'.iso': 'application/octet-stream',
'.txt': 'text/plain'
};
return contentTypes[ext] || 'application/octet-stream';
}
2. 客户端断点续传实现
<!DOCTYPE html>
<html>
<head>
<title>断点续传下载器</title>
<script src="https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js"></script>
</head>
<body>
<h1>大文件断点续传下载</h1>
<button onclick="startDownload()">开始下载</button>
<button onclick="pauseDownload()">暂停下载</button>
<button onclick="resumeDownload()">继续下载</button>
<div id="progress">进度: 0%</div>
<script>
let downloadController = null;
let downloadedBytes = 0;
let totalBytes = 0;
let isPaused = false;
async function startDownload() {
try {
const response = await axios.head('/download/resumable/large-file.zip');
totalBytes = parseInt(response.headers['content-length'], 10);
await downloadChunk();
} catch (error) {
console.error('开始下载失败:', error);
}
}
async function downloadChunk() {
if (isPaused) return;
const chunkSize = 1024 * 1024; // 1MB chunks
const end = Math.min(downloadedBytes + chunkSize - 1, totalBytes - 1);
try {
downloadController = new AbortController();
const response = await fetch('/download/resumable/large-file.zip', {
headers: {
'Range': `bytes=${downloadedBytes}-${end}`
},
signal: downloadController.signal
});
if (response.status === 206) {
const blob = await response.blob();
await saveChunk(blob);
downloadedBytes = end + 1;
updateProgress();
if (downloadedBytes < totalBytes) {
await downloadChunk();
} else {
console.log('下载完成!');
}
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error('下载错误:', error);
}
}
}
async function saveChunk(blob) {
// 这里实现文件保存逻辑,可以使用FileSystem API或IndexedDB
console.log('保存数据块:', blob.size, 'bytes');
}
function pauseDownload() {
isPaused = true;
if (downloadController) {
downloadController.abort();
}
}
function resumeDownload() {
if (isPaused) {
isPaused = false;
downloadChunk();
}
}
function updateProgress() {
const progress = (downloadedBytes / totalBytes * 100).toFixed(2);
document.getElementById('progress').textContent = `进度: ${progress}%`;
}
</script>
</body>
</html>
高级特性与最佳实践
1. 多线程下载支持
app.get('/download/multithread/:filename', async (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'files', filename);
const threads = parseInt(req.query.threads) || 4;
try {
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const chunkSize = Math.ceil(fileSize / threads);
res.setHeader('Content-Type', 'application/json');
res.json({
filename,
fileSize,
chunkSize,
threads,
chunks: Array.from({ length: threads }, (_, i) => ({
start: i * chunkSize,
end: Math.min((i + 1) * chunkSize - 1, fileSize - 1)
}))
});
} catch (error) {
res.status(404).json({ error: 'File not found' });
}
});
2. 下载状态管理
class DownloadManager {
constructor() {
this.downloads = new Map();
}
startDownload(sessionId, filePath) {
const stat = fs.statSync(filePath);
const downloadInfo = {
sessionId,
filePath,
totalSize: stat.size,
downloaded: 0,
chunks: [],
startTime: Date.now()
};
this.downloads.set(sessionId, downloadInfo);
return downloadInfo;
}
updateProgress(sessionId, bytesDownloaded) {
const info = this.downloads.get(sessionId);
if (info) {
info.downloaded += bytesDownloaded;
info.chunks.push({
time: Date.now(),
bytes: bytesDownloaded
});
}
}
getProgress(sessionId) {
const info = this.downloads.get(sessionId);
if (!info) return null;
const elapsed = (Date.now() - info.startTime) / 1000;
const speed = info.downloaded / elapsed;
const remaining = (info.totalSize - info.downloaded) / speed;
return {
progress: (info.downloaded / info.totalSize * 100).toFixed(2),
downloaded: info.downloaded,
total: info.totalSize,
speed: this.formatSpeed(speed),
remaining: this.formatTime(remaining)
};
}
formatSpeed(bytesPerSecond) {
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
let speed = bytesPerSecond;
let unitIndex = 0;
while (speed >= 1024 && unitIndex < units.length - 1) {
speed /= 1024;
unitIndex++;
}
return `${speed.toFixed(2)} ${units[unitIndex]}`;
}
formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hours}h ${minutes}m ${secs}s`;
}
}
// 使用示例
const downloadManager = new DownloadManager();
app.get('/download/status/:sessionId', (req, res) => {
const status = downloadManager.getProgress(req.params.sessionId);
res.json(status || { error: 'Session not found' });
});
3. 安全性考虑
// 文件类型白名单
const allowedExtensions = ['.pdf', '.txt', '.zip', '.mp4', '.jpg', '.png'];
app.get('/download/secure/:filename', (req, res) => {
const filename = req.params.filename;
const ext = path.extname(filename).toLowerCase();
// 检查文件类型
if (!allowedExtensions.includes(ext)) {
return res.status(403).send('File type not allowed');
}
// 防止路径遍历攻击
const safeFilename = path.basename(filename);
const filePath = path.join(__dirname, 'files', safeFilename);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
// 设置下载限制
const downloadLimit = 1024 * 1024 * 100; // 100MB限制
res.download(filePath, {
headers: {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
},
maxAge: '1h',
dotfiles: 'deny'
});
});
性能优化表格
| 优化策略 | 实现方式 | 适用场景 | 性能提升 |
|---|---|---|---|
| 分块传输 | Range头支持 | 大文件下载 | 减少内存占用,支持断点续传 |
| 流量控制 | throttle限流 | 带宽限制环境 | 防止服务器过载 |
| 多线程下载 | 分片并行下载 | 高速网络环境 | 提升下载速度2-4倍 |
| 缓存策略 | 设置Cache-Control | 重复下载 | 减少服务器压力 |
| 压缩传输 | gzip/brotli压缩 | 文本文件 | 减少传输数据量 |
完整示例项目结构
project/
├── app.js # 主应用文件
├── package.json
├── files/ # 下载文件目录
│ ├── document.pdf
│ ├── large-video.mp4
│ └── software.zip
├── middleware/
│ └── download.js # 下载中间件
├── utils/
│ └── fileManager.js # 文件管理工具
└── public/
└── download.html # 下载页面
总结
Express框架提供了强大的文件下载能力,通过合理利用分块传输、断点续传、流量控制等高级特性,可以构建出专业级的大文件下载解决方案。关键要点包括:
- 充分利用HTTP Range头实现分块下载和断点续传
- 实施流量控制防止服务器带宽被耗尽
- 加强安全措施防止路径遍历和非法文件访问
- 提供进度反馈增强用户体验
- 支持多线程下载提升大文件下载效率
通过本文的实践方案,你可以轻松构建出稳定、高效、安全的大文件下载服务,满足各种复杂场景下的文件传输需求。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



