在一个桌面应用中,文件下载几乎是必备的基础功能。Electron 让这件事变得既简单又强大:你可以在主进程里利用 Node.js 的全套 I/O 能力,配合 HTTP 的 Range 请求头,轻松实现断点续传和大文件的稳定下载。与此同时,通过 IPC 将进度、速度、状态实时推送到渲染进程,前端就能展示一个友好的下载管理器界面。
13.5.1 整体方案设计
下载功能的核心逻辑放在 主进程 中,因为:
- 只有主进程能够无限制地访问文件系统,把文件写入任意用户指定的目录。
- 只有主进程能够保持 DownloadItem 会话(如果使用 Electron 原生的
will-download事件)或者长时间运行后台任务,不受窗口关闭的影响。 - 通过主进程集中管理下载任务,可以轻松实现队列、暂停、取消、断点续传等复杂操作。
渲染进程只负责展示和发起指令:用户点击“下载”按钮 → 渲染进程通过 IPC 向主进程发送 start-download 消息 → 主进程根据消息中的 URL、保存路径等参数开始下载,并在过程中持续向渲染进程发送 download-progress 更新。
13.5.2 断点续传的原理
断点续传的核心在于 HTTP/1.1 协议中定义的 Range 请求头。当服务器支持部分内容请求时(响应头中包含 Accept-Ranges: bytes),客户端就可以在请求中加上:
Range: bytes=已下载字节数-
服务器会返回 206 Partial Content 状态码,并从指定位置开始传输数据。我们只需要在本地记录已下载的文件大小,重连时从该大小处继续请求,并将新接收的数据追加写入到同一个文件中。
要实现这一点,主进程的逻辑分为三步:
- 检查本地临时文件:如果目标文件已经存在了一部分,读取其大小作为已下载量。
- 发送带 Range 头的请求:如果已下载量大于 0,带上 Range 请求头;否则正常从头下载。
- 流式追加写入:将服务器返回的数据流以追加模式写入文件,并同步更新已下载大小。
这样即便网络中断、应用崩溃,已下载的部分也不会丢失,下次启动下载时就能自动续传。
13.5.3 核心实现代码
下面展示一个简化但可直接运行的主进程下载模块。它使用 Node.js 内置的 http/https 模块和 fs 模块,不依赖任何第三方下载库。
// main-process/downloadManager.js
const fs = require('fs');
const path = require('path');
const http = require('http');
const https = require('https');
const { EventEmitter } = require('events');
class DownloadManager extends EventEmitter {
constructor() {
super();
this.activeDownloads = new Map(); // 管理当前下载任务
}
/**
* 开始或恢复一个下载任务
* @param {string} url 下载地址
* @param {string} savePath 完整保存路径
* @param {object} options 可选配置
*/
startDownload(url, savePath, options = {}) {
const taskId = `${url}@${savePath}`;
if (this.activeDownloads.has(taskId)) {
throw new Error('该下载任务已存在');
}
// 确保保存目录存在
fs.mkdirSync(path.dirname(savePath), { recursive: true });
const task = {
url,
savePath,
tempPath: savePath + '.tmp', // 临时下载文件
downloaded: 0, // 已下载字节
totalSize: 0, // 总大小(可能未知)
status: 'starting',
abortController: new AbortController(),
};
// 如果临时文件已存在,获取已下载大小
if (fs.existsSync(task.tempPath)) {
task.downloaded = fs.statSync(task.tempPath).size;
}
this.activeDownloads.set(taskId, task);
this._performDownload(taskId);
return taskId;
}
// 执行下载请求
async _performDownload(taskId) {
const task = this.activeDownloads.get(taskId);
if (!task) return;
const { url, tempPath, downloaded, abortController } = task;
const parsedUrl = new URL(url);
const httpModule = parsedUrl.protocol === 'https:' ? https : http;
const headers = {};
if (downloaded > 0) {
headers.Range = `bytes=${downloaded}-`;
}
const requestOptions = {
method: 'GET',
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname + parsedUrl.search,
headers,
signal: abortController.signal,
};
const req = httpModule.request(requestOptions, (res) => {
// 处理重定向(简化版,仅处理一次)
if (res.statusCode === 301 || res.statusCode === 302) {
const redirectUrl = res.headers.location;
task.url = redirectUrl; // 更新task中的url,实际应递归处理
this.activeDownloads.set(taskId, task);
this._performDownload(taskId);
return;
}
// 检查服务器是否支持断点续传
if (downloaded > 0 && res.statusCode !== 206) {
// 服务器不支持续传,从头下载
task.downloaded = 0;
fs.unlinkSync(tempPath); // 删除旧文件
this._performDownload(taskId); // 重新请求不带Range
return;
}
if (res.statusCode !== 200 && res.statusCode !== 206) {
task.status = 'error';
this.emit('download-error', taskId, `服务器返回状态码: ${res.statusCode}`);
return;
}
// 读取文件总大小
const contentRange = res.headers['content-range'];
if (contentRange) {
// 格式: bytes 0-100/1001
const total = parseInt(contentRange.split('/')[1], 10);
if (!isNaN(total)) task.totalSize = total;
} else if (res.headers['content-length']) {
task.totalSize = parseInt(res.headers['content-length'], 10) + downloaded;
}
task.status = 'downloading';
this.emit('download-start', taskId, {
totalSize: task.totalSize,
downloaded: task.downloaded,
});
// 创建写入流(追加模式)
const writeStream = fs.createWriteStream(tempPath, { flags: 'a' });
res.pipe(writeStream);
res.on('data', (chunk) => {
task.downloaded += chunk.length;
this.emit('download-progress', taskId, {
downloaded: task.downloaded,
totalSize: task.totalSize,
speed: this._getSpeed(task), // 可自行计算瞬时速度
});
});
writeStream.on('finish', () => {
// 下载完成,将临时文件重命名为正式文件名
fs.renameSync(tempPath, task.savePath);
task.status = 'completed';
this.emit('download-complete', taskId);
this.activeDownloads.delete(taskId);
});
writeStream.on('error', (err) => {
task.status = 'error';
this.emit('download-error', taskId, err.message);
});
});
req.on('error', (err) => {
if (err.name === 'AbortError') {
task.status = 'paused'; // 由外部暂停触发
this.emit('download-paused', taskId);
} else {
task.status = 'error';
this.emit('download-error', taskId, err.message);
}
});
req.end();
}
// 暂停下载
pauseDownload(taskId) {
const task = this.activeDownloads.get(taskId);
if (task && task.abortController) {
task.abortController.abort();
}
}
// 取消下载并删除临时文件
cancelDownload(taskId) {
const task = this.activeDownloads.get(taskId);
if (task) {
if (task.abortController) task.abortController.abort();
if (fs.existsSync(task.tempPath)) fs.unlinkSync(task.tempPath);
this.activeDownloads.delete(taskId);
this.emit('download-canceled', taskId);
}
}
}
13.5.4 主进程与渲染进程通信
在 Electron 的主进程入口文件中,实例化 DownloadManager,并注册 IPC 处理函数,将下载事件桥接到渲染进程。
// main.js (主进程)
const { ipcMain } = require('electron');
const DownloadManager = require('./downloadManager');
const downloadManager = new DownloadManager();
// 渲染进程发起下载
ipcMain.handle('download:start', async (event, { url, savePath }) => {
try {
const taskId = downloadManager.startDownload(url, savePath);
return { success: true, taskId };
} catch (err) {
return { success: false, error: err.message };
}
});
ipcMain.on('download:pause', (event, taskId) => {
downloadManager.pauseDownload(taskId);
});
ipcMain.on('download:cancel', (event, taskId) => {
downloadManager.cancelDownload(taskId);
});
// 将下载事件转发给所有渲染进程
downloadManager.on('download-progress', (taskId, info) => {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('download:progress', taskId, info);
});
});
downloadManager.on('download-complete', (taskId) => {
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('download:complete', taskId);
});
});
// 其他事件类似处理
渲染进程(前端)的使用方式:
// renderer.js (渲染进程)
const { ipcRenderer } = window.require('electron');
// 开始下载
async function startDownload(url, savePath) {
const result = await ipcRenderer.invoke('download:start', { url, savePath });
if (result.success) {
console.log('下载任务已创建:', result.taskId);
}
}
// 监听进度更新
ipcRenderer.on('download:progress', (event, taskId, { downloaded, totalSize, speed }) => {
const percent = totalSize > 0 ? Math.round((downloaded / totalSize) * 100) : 0;
console.log(`任务 ${taskId}: ${percent}% (${downloaded}/${totalSize})`);
// 更新前端进度条
});
13.5.5 大文件下载的稳定性技巧
在实现大文件(数 GB 级别)下载时,有几个工程经验值得注意:
- 使用流(Stream)处理数据:千万不要把整个文件读入内存再写入磁盘,那样会直接撑爆内存。上面的代码中使用了
res.pipe(writeStream),数据会以块的形式边接收边写入,内存占用极小。 - 临时文件机制:下载过程中写入
.tmp后缀的临时文件,完成后才重命名为正式文件。这样可以防止下载中断时留下损坏文件,也便于恢复下载时判断已下载量。 - 错误重试:网络抖动很常见,可以在
req.on('error')中添加重试逻辑(比如最多重试 3 次,间隔 2 秒)。注意使用指数退避避免频繁重连。 - 限速与并发控制:如果应用需要同时下载多个大文件,可以通过
stream.pause()/stream.resume()或自定义 Transform 流来控制下载速度,避免占满用户带宽。 - 校验完整性:如果服务器提供文件的 MD5/SHA256 值,下载完成后应当对文件进行校验,确保数据无误。这可以在
finish事件中用 Node.js 的crypto模块计算哈希对比。 - 内存与 CPU 管理:默认的 Node.js 流在高带宽下载时可能产生大量
data事件,导致微任务队列压力过大。可以通过调整highWaterMark或使用stream.pipeline优化。在大规模应用中,也可以考虑使用 C++ 插件或 Electron 的DownloadItemAPI(下文介绍)来处理下载本身,但在我们自己实现的方案中注意这些细节就足够了。
13.5.6 使用 Electron 原生的下载功能
除了自己实现下载管理器,Electron 也内置了会话模块的下载功能。通过监听 session 的 will-download 事件,可以获取一个 DownloadItem 对象,它封装了暂停、恢复、取消、进度追踪等能力,并且支持断点续传。
// 主进程
const { session } = require('electron');
session.defaultSession.on('will-download', (event, item, webContents) => {
// 设置保存路径
item.setSavePath('/path/to/save/file.zip');
item.on('updated', (event, state) => {
if (state === 'progressing') {
const progress = item.getReceivedBytes() / item.getTotalBytes();
console.log(`下载进度: ${Math.round(progress * 100)}%`);
// 可以通过 webContents.send 推送到渲染进程
} else if (state === 'completed') {
console.log('下载完成');
}
});
// 允许暂停恢复
item.on('done', (event, state) => {
if (state === 'completed') {
// 完成
} else if (state === 'cancelled') {
// 取消
} else if (state === 'interrupted') {
// 中断,可以稍后调用 item.resume() 恢复
}
});
});
这种方式更简单,但缺点是灵活性不如自己实现:你无法在中间加入自定义的数据处理(如解密、解压),也无法完全控制重试策略。对于大多数常规下载需求,已经足够。
13.5.7 总结
在 Electron 中实现稳健的断点续传、大文件下载,核心是善用 Node.js 的流处理能力和 HTTP Range 请求。我们将下载逻辑封装在主进程的专用模块里,通过事件机制向前端推送状态,既保证了功能的可靠性,又让界面层保持简洁。根据项目需要,可以选择自行实现精细控制,或者直接使用 Electron 内置的下载 API 快速搭建。无论哪种方式,把下载与 UI 解耦、采用临时文件和追加写入、处理网络异常等设计原则,都是保障用户体验的关键。