Electron DownloadItem 详解:主进程文件下载控制 API 与源码实现解析
DownloadItem 是 Electron 主进程中专用于控制"从远程来源下载文件"的对象,它不是 'electron' 模块的导出项,只能作为 Session 类 will-download 事件的回调参数获得。读完本篇,你将完整掌握 DownloadItem 的全部实例事件与方法(保存路径设置、暂停/恢复/取消、进度与状态查询、断点续传元数据),并能结合 Electron 源码理解状态机映射、保存路径决策链和下载取消机制,写出带进度、可取消、支持断点恢复的生产级下载功能。
类定位:一个"不可直接构造"的事件发射器
DownloadItem 是一个 EventEmitter,代表 Electron 中的一个下载项。它与原生下载对象的生命周期绑定方式决定了使用方式:
- 进程归属:仅存在于主进程(Main Process),渲染进程无法直接持有它。
- 获取途径:该类不从
'electron'模块导出,唯一获得实例的方式是监听Session的will-download事件,事件回调参数中会传入下载项对象。 - 生命周期:下载进入终态后对象即被销毁。源码中 electron_api_download_item.cc 的
CheckAlive()会对已销毁的对象抛错DownloadItem used after being destroyed——如果你缓存了item引用并在done事件后再次调用其方法,就会看到这条错误。
完整使用示例:监听 will-download 并全程跟踪下载
文档给出的标准用法如下,覆盖了"设置保存路径 + 进度监听 + 终态处理"三件事:
// In the main process.
const { BrowserWindow } = require('electron')
const win = new BrowserWindow()
win.webContents.session.on('will-download', (event, item, webContents) => {
// Set the save path, making Electron not to prompt a save dialog.
item.setSavePath('/tmp/save.pdf')
item.on('updated', (event, state) => {
if (state === 'interrupted') {
console.log('Download is interrupted but can be resumed')
} else if (state === 'progressing') {
if (item.isPaused()) {
console.log('Download is paused')
} else {
console.log(`Received bytes: ${item.getReceivedBytes()}`)
}
}
})
item.once('done', (event, state) => {
if (state === 'completed') {
console.log('Download successfully')
} else {
console.log(`Download failed: ${state}`)
}
})
})
will-download 事件的签名是 (event, item, webContents),其中 webContents 是触发下载的 WebContents,可用于把下载项与具体的窗口/视图关联起来。
有一个文档未明说但源码确认的重要行为:在 will-download 回调中调用 event.preventDefault() 会取消这次下载。见 electron_api_session.cc:
void Session::OnDownloadCreated(content::DownloadManager* manager,
download::DownloadItem* item) {
if (item->IsSavePackageDownload())
return;
// ...
if (item->GetState() == download::DownloadItem::INTERRUPTED)
handle->SetSavePath(item->GetTargetFilePath());
// ...
bool prevent_default = Emit("will-download", handle_object, web_contents);
if (prevent_default) {
item->Cancel(true);
item->Remove();
}
}
这里还有两个实现细节值得注意:
will-download的底层来源是content::DownloadManager的OnDownloadCreated回调,Electron 会为每个原生download::DownloadItem创建(或复用)一个 JS 包装对象后发出事件;- 当下载项以
INTERRUPTED状态重新进入will-download(例如会话重启后的可恢复下载)时,Electron 会自动把保存路径预设为原目标文件路径,因此续传下载通常不会再弹出保存对话框。
实例事件:状态机的两个出口
Event: 'updated'
下载有更新且尚未结束时发出。
Returns:
eventEventstatestring - 取值为progressing或interrupted:progressing— 下载正在进行中;interrupted— 下载被中断,但可以恢复(例如断网后重连)。
Event: 'done'
下载进入终态时发出,涵盖三种终态:
completed— 下载成功完成;cancelled— 下载被取消(例如调用downloadItem.cancel(),或在will-download中preventDefault);interrupted— 下载被中断且无法恢复。
Returns:
eventEventstatestring - 取值为上述三者之一。
这四个状态字符串并非随意命名,而是从 Chromium 内部枚举直接映射而来。electron_api_download_item.cc 中的 gin 转换器展示了完整映射:
switch (state) {
case download::DownloadItem::IN_PROGRESS: download_state = "progressing"; break;
case download::DownloadItem::COMPLETE: download_state = "completed"; break;
case download::DownloadItem::CANCELLED: download_state = "cancelled"; break;
case download::DownloadItem::INTERRUPTED: download_state = "interrupted"; break;
}
事件的分发点在 electron_api_download_item.cc:
void DownloadItem::OnDownloadUpdated(download::DownloadItem* item) {
if (!CheckAlive()) return;
if (download_item_->IsDone()) {
Emit("done", item->GetState());
keep_alive_.Clear();
} else {
Emit("updated", item->GetState());
}
}
即:只要原生对象未结束就发 updated,一旦 IsDone() 为真就发 done,并解除自保活引用,允许该 JS 对象被垃圾回收。
实例方法
保存位置控制
downloadItem.setSavePath(path)
pathstring - 下载项的保存文件路径。
该 API 仅在 session 的 will-download 回调函数中可用。 如果 path 的目录不存在,Electron 会递归创建目录。若用户不通过该 API 设置保存路径,Electron 将走默认流程来确定保存路径——这通常会弹出系统保存对话框。
downloadItem.getSavePath()
Returns string - 下载项的保存路径。它是 setSavePath(path) 设置的值,或用户在保存对话框中选择的值。
downloadItem.savePath 属性
string 属性,作用与 setSavePath/getSavePath 等价,仅在 will-download 回调中可用。从源码的 V8 模板注册可以直接确认两者是同一对底层存取函数:
// shell/browser/api/electron_api_download_item.cc
.SetMethod("setSavePath", &DownloadItem::SetSavePath)
.SetMethod("getSavePath", &DownloadItem::GetSavePath)
.SetProperty("savePath", &DownloadItem::GetSavePath, &DownloadItem::SetSavePath)
downloadItem.setSaveDialogOptions(options)
optionsSaveDialogOptions - 设置保存对话框选项。该对象的属性与dialog.showSaveDialog()的options参数完全相同。
该 API 允许你自定义下载默认弹出的保存对话框(例如指定标题、默认路径、Windows 下的文件类型过滤器)。同样仅在 will-download 回调中可用。
downloadItem.getSaveDialogOptions()
Returns SaveDialogOptions - 返回之前通过 setSaveDialogOptions(options) 设置的选项对象。
暂停 / 恢复 / 取消
downloadItem.pause()
暂停下载。
downloadItem.isPaused()
Returns boolean - 下载是否处于暂停状态。
downloadItem.resume()
恢复已暂停的下载。源码中对应 download_item_->Resume(true /* user_gesture */),即以"用户手势"语义恢复(electron_api_download_item.cc)。
[!NOTE] 要启用断点续传,目标服务器必须支持 Range 请求,并返回
Last-Modified和ETag响应头。否则resume()会丢弃已接收的字节,从头开始下载。
downloadItem.canResume()
Returns boolean - 下载是否可以恢复。
downloadItem.cancel()
取消下载操作。取消后 done 事件将以 cancelled 状态发出。
下载信息读取
| 方法 | 返回类型 | 说明 |
|---|---|---|
getURL() |
string |
下载项的原始 URL |
getMimeType() |
string |
文件的 MIME 类型 |
hasUserGesture() |
boolean |
下载是否由用户手势触发 |
getFilename() |
string |
下载项的文件名(见下方说明) |
getCurrentBytesPerSecond() |
Integer |
当前下载速度(字节/秒) |
getTotalBytes() |
Integer |
下载项总大小(字节);大小未知时返回 0 |
getReceivedBytes() |
Integer |
已接收字节数 |
getPercentComplete() |
Integer |
下载完成百分比 |
getContentDisposition() |
string |
响应头中的 Content-Disposition 字段 |
getState() |
string |
当前状态:progressing、completed、cancelled 或 interrupted |
关于 getFilename() 的一个重要提示:
[!NOTE] 文件名不一定与最终保存到磁盘的文件名一致。如果用户在弹出的保存对话框中修改了文件名,实际保存的文件名就会不同。
getFilename() 的底层实现并非简单读取请求 URL,而是调用 Chromium 的 net::GenerateFileName(url, content_disposition, suggested_filename, mime_type, ...),按优先级综合 URL、Content-Disposition 头、服务器建议文件名与 MIME 类型生成(electron_api_download_item.cc)。这与 electron_download_manager_delegate.cc 中默认保存路径生成所用的逻辑一致。
会话重启后续传专用方法
[!NOTE] 以下方法专门用于在 session 重启后恢复
cancelled的下载项。
| 方法 | 返回类型 | 说明 |
|---|---|---|
getURLChain() |
string[] |
完整 URL 链,包含所有重定向 |
getLastModifiedTime() |
string |
Last-Modified 响应头的值 |
getETag() |
string |
ETag 响应头的值 |
getStartTime() |
Double |
下载开始时刻的 UNIX 时间戳(秒) |
getEndTime() |
Double |
下载结束时刻的 UNIX 时间戳(秒) |
这组方法正好对应 Chromium 下载恢复所需的全部元数据:URL 链、内容标识(ETag / Last-Modified)、时间戳。getETag() 与 getLastModifiedTime() 也正是上一节 resume() 断点续传的前提条件——服务器需要用它们校验已下载部分是否仍然有效。
源码纵深:保存路径是如何被决定的
文档说"未设置保存路径时会弹出保存对话框",这条"默认流程"在 electron_download_manager_delegate.cc 的 DetermineDownloadTarget() 中体现为一条清晰的决策链:
- 强制路径:若原生下载项带有
GetForcedFilePath()(如session.downloadURL的内部机制),直接使用; - JS 侧设置的路径:通过
GetItemSavePath()回查 JS 包装对象(即你在will-download中调用setSavePath或item.savePath = ...设置的值),非空则直接作为目标路径——这正是"设置保存路径后不弹对话框"的实现依据; - 默认路径:以上皆无时,先用"上次保存目录"或系统默认下载目录(
kDownloadDefaultDirectory)拼出候选路径,再进入OnDownloadPathGenerated():若GetItemSavePath()仍为空,则弹出保存对话框(可用setSaveDialogOptions定制标题、默认路径、过滤器等);用户取消对话框时,回调以空路径 +DOWNLOAD_INTERRUPT_REASON_USER_CANCELED通知下载管理器,下载随之以取消/中断终态结束。
will-download 回调 (JS)
├─ setSavePath / savePath / setSaveDialogOptions 写入 JS 包装对象
└─ preventDefault() ──> item->Cancel(true); item->Remove(); // 直接取消
DetermineDownloadTarget (C++)
├─ GetForcedFilePath() 非空 ──────────────> 直接使用该路径
├─ GetItemSavePath() 非空 ───────────────> 直接使用该路径(不弹窗)
└─ 均为空 ──> CreateDownloadPath() 生成候选路径
└─> OnDownloadPathGenerated()
├─ 仍无路径 ──> ShowSaveDialog()(按 dialog options 定制)
│ ├─ 用户选择 ──> 记住目录,SetSavePath(path)
│ └─ 用户取消 ──> 空路径回调 ──> 下载被取消
└─ 已有路径 ──> 直接回调 DownloadTargetInfo
另外,JS 包装对象与原生对象通过弱引用关联:DownloadItem 的构造时会以 UserData 键把 cppgc::WeakPersistent<DownloadItem> 挂到原生 download::DownloadItem 上(electron_api_download_item.cc),两侧的销毁互不阻塞,OnDownloadDestroyed 时 JS 侧置空指针并清掉自保活。这就是为什么 done 之后再触碰该对象会抛 DownloadItem used after being destroyed,也是缓存 item 前必须先想清楚事件时序的原因。
测试用例中的行为印证
Electron 的官方测试套件 api-session-spec.ts 完整覆盖了上述 API 的实际行为,可作为验证依据:
will-download事件参数中可读取getURL(),且对象在销毁后调用getURL()会抛DownloadItem used after being destroyed(L675-L705);session.downloadURL(url)触发下载后,在will-download回调中设置item.savePath = downloadFilePath,done事件返回completed,且item.savePath为绝对路径、getReceivedBytes()/getTotalBytes()与服务器实际发送字节数一致、getFilename()/getMimeType()/getContentDisposition()与响应头一致(L1362-L1414);- 测试还覆盖了带
Authorization请求头的downloadURL、下载失败与取消、暂停后resume()恢复等场景(L1460 起、L1813 起)。
session.downloadURL() 是除页面内 <a download>、window.location 等导航外触发下载的另一入口,will-download 事件与 DownloadItem 对其同样生效。
实践要点小结
- 在
will-download回调内完成所有配置:setSavePath、setSaveDialogOptions与savePath属性都只在该回调中生效;不在回调内设置等于放弃控制,落入弹窗默认流程。 - 需要静默下载就显式
setSavePath:设置后 Electron 递归创建目录并直接落盘,不弹对话框;想控制对话框外观则用setSaveDialogOptions(与dialog.showSaveDialog的 options 相同)。 - 用
updated驱动进度 UI:progressing+isPaused()区分"暂停"与"传输中",配合getReceivedBytes()/getPercentComplete()/getCurrentBytesPerSecond()可绘制完整的进度条与速率显示。 - 区分
interrupted的两个语义:updated中的interrupted表示"中断但可恢复",done中的interrupted表示"中断且无法恢复";前者应提示用户等待或调用resume(),后者只能按失败处理。 - 断点续传依赖服务器能力:
resume()只有在服务器支持 Range 且返回Last-Modified+ETag时才真正续传,否则从头下载;getETag()、getLastModifiedTime()、getURLChain()、getStartTime()、getEndTime()就是为跨会话恢复下载准备的元数据。 - 不要跨终态缓存 item:
done事件发出后 JS 对象即失效,后续调用会抛错;需要在done回调里一次性取好所需信息(如getSavePath()、getFilename())再使用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00