前言
公司有个项目,分别做了 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
事件后,就进入到了监听下载的过程::
- 保存下载路径
根据是否存在 item.setSavePath()
语句,来决定是否跳出 save dialog
系统默认保存对话框。这个方法接受一个参数即文件存放路径。
但是,一定保证路径里包含了文件名!
否则,就算路径不正确它也不会报错而是选择存放在默认路径下。如果不设置这条语句就会跳出默认的系统保存弹窗,无需任何配置,但是
保存对话框不阻塞进程,在选择目录的时候,没准都已经下载完了,那么显示下载进度就毫无意义了。
- 监听下载过程
使用 item 的 updated 事件实时更新触发监听,state 等于progressing
表示下载进行中,state 等于interrupted
表示下载被打断。如果状态为 下载进行中 就实时将百分比传到渲染进程进行页面展示:
mainWindow.webContents.send('updateProgressing', value)
- 计算下载进度
在 item 的 updated 事件中 获取的 item 信息如下:
- item.getFilename() 下载文件的名称
- item.getSavePath() 下载文件的路径
- item.getReceivedBytes() 下载文件已经下载的字节数
- item.getTotalBytes() 下载文件的总字节数
了解这些后,获取下载进度就容易多了:
parseInt(100 * (item.getReceivedBytes() / item.getTotalBytes()))
- 监听下载结束
使用 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 的原生能力综合实现。这次功能实现踩了不坑,总结出来一条经验:在实现过程中,不要对着一条方法使劲琢磨,其实换一种方法也可以实现想要的需求,能够让自己感觉豁然开朗。
以上只是我摸索出来的一种实现方法,如果你有其他更好的方法,欢迎在评论区畅所欲言!
参考链接
- Electron 构建下载文件桌面应用 (opens in a new tab)
- electron 程序,如何监控文件下载进度,并显示进度条? (opens in a new tab)
- Electron 官方文档 (opens in a new tab)
阅读更多
arrify 万物皆可转换为数组2022/05/26
如何给你的个人网站添加点赞功能2023/07/10