首页
/ Electron DownloadItem 详解:主进程文件下载控制 API 与源码实现解析

Electron DownloadItem 详解:主进程文件下载控制 API 与源码实现解析

2026-09-04 11:19:20作者:廉彬冶Miranda

DownloadItem 是 Electron 主进程中专用于控制"从远程来源下载文件"的对象,它不是 'electron' 模块的导出项,只能作为 Sessionwill-download 事件的回调参数获得。读完本篇,你将完整掌握 DownloadItem 的全部实例事件与方法(保存路径设置、暂停/恢复/取消、进度与状态查询、断点续传元数据),并能结合 Electron 源码理解状态机映射、保存路径决策链和下载取消机制,写出带进度、可取消、支持断点恢复的生产级下载功能。

类定位:一个"不可直接构造"的事件发射器

DownloadItem 是一个 EventEmitter,代表 Electron 中的一个下载项。它与原生下载对象的生命周期绑定方式决定了使用方式:

  • 进程归属:仅存在于主进程(Main Process),渲染进程无法直接持有它。
  • 获取途径:该类不从 'electron' 模块导出,唯一获得实例的方式是监听 Sessionwill-download 事件,事件回调参数中会传入下载项对象。
  • 生命周期:下载进入终态后对象即被销毁。源码中 electron_api_download_item.ccCheckAlive() 会对已销毁的对象抛错 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();
  }
}

这里还有两个实现细节值得注意:

  1. will-download 的底层来源是 content::DownloadManagerOnDownloadCreated 回调,Electron 会为每个原生 download::DownloadItem 创建(或复用)一个 JS 包装对象后发出事件;
  2. 当下载项以 INTERRUPTED 状态重新进入 will-download(例如会话重启后的可恢复下载)时,Electron 会自动把保存路径预设为原目标文件路径,因此续传下载通常不会再弹出保存对话框。

实例事件:状态机的两个出口

Event: 'updated'

下载有更新且尚未结束时发出。

Returns:

  • event Event
  • state string - 取值为 progressinginterrupted
    • progressing — 下载正在进行中;
    • interrupted — 下载被中断,但可以恢复(例如断网后重连)。

Event: 'done'

下载进入终态时发出,涵盖三种终态:

  • completed — 下载成功完成;
  • cancelled — 下载被取消(例如调用 downloadItem.cancel(),或在 will-downloadpreventDefault);
  • interrupted — 下载被中断且无法恢复

Returns:

  • event Event
  • state string - 取值为上述三者之一。

这四个状态字符串并非随意命名,而是从 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)

  • path string - 下载项的保存文件路径。

该 API 仅在 sessionwill-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)

  • options SaveDialogOptions - 设置保存对话框选项。该对象的属性与 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-ModifiedETag 响应头。否则 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 当前状态:progressingcompletedcancelledinterrupted

关于 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.ccDetermineDownloadTarget() 中体现为一条清晰的决策链:

  1. 强制路径:若原生下载项带有 GetForcedFilePath()(如 session.downloadURL 的内部机制),直接使用;
  2. JS 侧设置的路径:通过 GetItemSavePath() 回查 JS 包装对象(即你在 will-download 中调用 setSavePathitem.savePath = ... 设置的值),非空则直接作为目标路径——这正是"设置保存路径后不弹对话框"的实现依据
  3. 默认路径:以上皆无时,先用"上次保存目录"或系统默认下载目录(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 destroyedL675-L705);
  • session.downloadURL(url) 触发下载后,在 will-download 回调中设置 item.savePath = downloadFilePathdone 事件返回 completed,且 item.savePath 为绝对路径、getReceivedBytes()/getTotalBytes() 与服务器实际发送字节数一致、getFilename()/getMimeType()/getContentDisposition() 与响应头一致(L1362-L1414);
  • 测试还覆盖了带 Authorization 请求头的 downloadURL、下载失败与取消、暂停后 resume() 恢复等场景(L1460 起L1813 起)。

session.downloadURL() 是除页面内 <a download>window.location 等导航外触发下载的另一入口,will-download 事件与 DownloadItem 对其同样生效。

实践要点小结

  1. will-download 回调内完成所有配置setSavePathsetSaveDialogOptionssavePath 属性都只在该回调中生效;不在回调内设置等于放弃控制,落入弹窗默认流程。
  2. 需要静默下载就显式 setSavePath:设置后 Electron 递归创建目录并直接落盘,不弹对话框;想控制对话框外观则用 setSaveDialogOptions(与 dialog.showSaveDialog 的 options 相同)。
  3. updated 驱动进度 UIprogressing + isPaused() 区分"暂停"与"传输中",配合 getReceivedBytes()/getPercentComplete()/getCurrentBytesPerSecond() 可绘制完整的进度条与速率显示。
  4. 区分 interrupted 的两个语义updated 中的 interrupted 表示"中断但可恢复",done 中的 interrupted 表示"中断且无法恢复";前者应提示用户等待或调用 resume(),后者只能按失败处理。
  5. 断点续传依赖服务器能力resume() 只有在服务器支持 Range 且返回 Last-Modified + ETag 时才真正续传,否则从头下载;getETag()getLastModifiedTime()getURLChain()getStartTime()getEndTime() 就是为跨会话恢复下载准备的元数据。
  6. 不要跨终态缓存 itemdone 事件发出后 JS 对象即失效,后续调用会抛错;需要在 done 回调里一次性取好所需信息(如 getSavePath()getFilename())再使用。
登录后查看全文
热门项目推荐
相关项目推荐