在 Electron 中实现下载文件实时显示进度条

2021/05/08
8分钟阅读

前言

公司有个项目,分别做了 web 端 和 桌面端 (Electron 实现) , 在 web 端中下载文件,直接跳转到新页面,使用浏览器自带的下载器即可获得便捷的下载体验。然而 桌面端 不能直接跳到新页面进行下载而且不能显示下载进度,影响用户体验,于是领导给到我这样一个需求:

实现一个下载进度条,可以实时看到下载进度,下载前可以选择文件保存位置

这样做可以弥补了 Web 浏览器的一些先天不足:

  • 支持原生的系统托盘
  • 增强网络状态变更的感知

而且不通过系统浏览器下载携带敏感信息,而是在 APP 里实现理论上更安全。此前虽然接触过 Electron , 但没有深入了解。这次需求的实现是我在查阅很多资料后实现的,故写一篇文章来记载整个过程。

页面样式

因为整个项目使用的 Element UI 框架,所以进度条 自然也沿用框架自带的进条度组件,只需要控制 dialog 弹窗显示与进度百分比就可以了,界面样式如下:

界面样式

选择下载位置

在此之前先简单介绍一下本次功能实现所需要的模块,在主进程和渲染进程进行引入:

主进程模块

  • ipcMain-> 从主进程到渲染进程的异步通信。
  • dialog -> 显示用于打开和保存文件、警报等的本机系统对话框。
  • shell -> 使用默认应用程序管理文件和 url。

渲染进程模块

  • ipcRenderer -> 从渲染器进程到主进程的异步通信。

用户在点击下载后首先需要触发系统弹窗,而不是自定义的进度弹窗。需要搞清楚先后顺序,所在在主进程中需要先写好一个 download 事件 .

// 监听渲染进程发出的 download 事件
ipcMain.on('download', async (evt, args) => {
  // 打开系统弹窗 选择文件下载位置
  dialog.showOpenDialog(
    {
      properties: ['openFile', 'openDirectory'],
    },
    (files) => {
      saveUrl = files[0] // 保存文件路径
      if (!saveUrl) return // 如果用户没有选择路径,则不再向下进行
      let url = JSON.parse(args)
      downloadUrl = url.downloadUrl // 获取渲染进程传递过来的 下载链接
      mainWindow.webContents.downloadURL(downloadUrl) // 触发 will-download 事件
    }
  )
})

由于 electron 是基于 chromium 实现的,通过调用 webContents 的 downloadURL 方法,相当于调用了 chromium 底层实现的下载,会忽略响应头信息,触发 will-download 事件。

在渲染进程中,需要在用户点击下载时,发送 download 事件传递要下载的文件链接,具体代码如下:

ipcRenderer.send(
  'download',
  JSON.stringify({
    downloadUrl: href,
  })
)
选择文件夹

监听下载过程并计算进度

在触发 will-download事件后,就进入到了监听下载的过程::

  1. 保存下载路径

根据是否存在 item.setSavePath() 语句,来决定是否跳出 save dialog 系统默认保存对话框。这个方法接受一个参数即文件存放路径。 但是,一定保证路径里包含了文件名! 否则,就算路径不正确它也不会报错而是选择存放在默认路径下。如果不设置这条语句就会跳出默认的系统保存弹窗,无需任何配置,但是 保存对话框不阻塞进程,在选择目录的时候,没准都已经下载完了,那么显示下载进度就毫无意义了。

  1. 监听下载过程

使用 item 的 updated 事件实时更新触发监听,state 等于progressing表示下载进行中,state 等于interrupted表示下载被打断。如果状态为 下载进行中 就实时将百分比传到渲染进程进行页面展示:

mainWindow.webContents.send('updateProgressing', value)
  1. 计算下载进度

在 item 的 updated 事件中 获取的 item 信息如下:

  • item.getFilename() 下载文件的名称
  • item.getSavePath() 下载文件的路径
  • item.getReceivedBytes() 下载文件已经下载的字节数
  • item.getTotalBytes() 下载文件的总字节数

了解这些后,获取下载进度就容易多了:

parseInt(100 * (item.getReceivedBytes() / item.getTotalBytes()))
  1. 监听下载结束

使用 item 的 done事件监听下载完成。state 的值::

  • completed -> 下载完成
  • cancelled -> 用户主动取消下载

如果下载完成就使用 electron 的 shell 模块来实现打开文件(openPath)和打开文件所在位置(showItemInFolder)

shell.showItemInFolder(filePath)

完成代码如下

主进程:

mainWindow.webContents.session.on('will-download', (e, item) => {
  const filePath = path.join(saveUrl, item.getFilename())
  item.setSavePath(filePath) // 'C:\Users\kim\Downloads\第 12 次.zip'
  //监听下载过程,计算并设置进度条进度
  item.on('updated', (evt, state) => {
    if ('progressing' === state) {
      //此处  用接收到的字节数和总字节数求一个比例  就是进度百分比
      if (item.getReceivedBytes() && item.getTotalBytes()) {
        value = parseInt(100 * (item.getReceivedBytes() / item.getTotalBytes()))
      }
      // 把百分比发给渲染进程进行展示
      mainWindow.webContents.send('updateProgressing', value)
      // mac 程序坞、windows 任务栏显示进度
      mainWindow.setProgressBar(value)
    }
  })
  //监听下载结束事件
  item.on('done', (e, state) => {
    //如果窗口还在的话,去掉进度条
    if (!mainWindow.isDestroyed()) {
      mainWindow.setProgressBar(-1)
    }
    //下载被取消或中断了
    if (state === 'interrupted') {
      electron.dialog.showErrorBox(
        '下载失败',
        `文件 ${item.getFilename()} 因为某些原因被中断下载`
      )
    }
    // 下载成功后打开文件所在文件夹
    if (state === 'completed') {
      setTimeout(() => {
        shell.showItemInFolder(filePath)
      }, 1000)
    }
  })
})

在渲染进程中,需要在 生命周期 created 中进行接受:

ipcRenderer.removeAllListeners('updateProgressing')
ipcRenderer.on('updateProgressing', (e, value) => {
  this.$nextTick(() => {
    this.downloadStatus = true // 开启进度弹窗
    this.downloadPercent = value // 设置下载百分比
  })
})

总结

Electron 能实现的功能远不止如此,还需要不断探索。在日常工中可以根据需求结合 Electron 的原生能力综合实现。这次功能实现踩了不坑,总结出来一条经验:在实现过程中,不要对着一条方法使劲琢磨,其实换一种方法也可以实现想要的需求,能够让自己感觉豁然开朗。

以上只是我摸索出来的一种实现方法,如果你有其他更好的方法,欢迎在评论区畅所欲言!

参考链接

  1. Electron 构建下载文件桌面应用 (opens in a new tab)
  2. electron 程序,如何监控文件下载进度,并显示进度条? (opens in a new tab)
  3. Electron 官方文档 (opens in a new tab)

阅读更多

返回
+1