

【Electron】酷家乐客户端开发实践分享 — 下载管理器
source link: https://www.tuicool.com/articles/qYfuAjJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

背景
打开酷家乐客户端,可以在左下角的更多菜单中找到下载管理这个功能,今天我们就来看看在Electron中如何实现一个下载管理器。
如何触发下载行为
由于Electron渲染层是基于chromium的,触发下载的逻辑和chromium是一致的,页面中的a标签或者js跳转等等行为都可能触发下载,具体视访问的资源而定。什么样的资源会触发浏览器的下载行为呢?
- response header中的
Content-Disposition
为attachment。参考 MDN Content-Disposition - response header中的
Content-Type
,是浏览器无法直接打开的文件类型,例如application/octet-stream
,此时取决于浏览器的具体实现了。例子: IE无法打开pdf文件,chrome可以直接打开pdf文件,因此pdf类型的url在chrome上可以直接打开,而在IE下会触发下载行为。
在Electron中还有一种方法可以触发下载: webContents.download 。相当于直接调用chromium底层的下载逻辑,忽略headers中的那些判断,直接下载。
上述两种下载行为,都会触发session的 will-download 事件, 在这里可以获取到关键的 downloadItem 对象
整体流程
设置文件路径
如果不做任何处理的话,触发下载行为时Electron会弹出一个系统dialog,让用户来选择文件存放的目录。这个体验并不好,因此我们首先需要把这个系统dialog去掉。使用 downloadItem.savePath
即可。
// Set the save path, making Electron not to prompt a save dialog. downloadItem.setSavePath('/tmp/save.pdf');
为文件设置默认下载路径,就需要考虑文件名重复的情况,一般来说会使用文件名自增的逻辑,例如:test.jpg、test.jpg(1)这种格式。文件默认存放目录,也是一个问题,我们统一使用 app.getPath('downloads')
作为文件下载目录。为了用户体验,后续提供修改文件下载目录功能即可。
// in main.js 主进程中 const { session } = require('electron'); session.defaultSession.on('will-download', async (event, item) => { const fileName = item.getFilename(); const url = item.getURL(); const startTime = item.getStartTime(); const initialState = item.getState(); const downloadPath = app.getPath('downloads'); let fileNum = 0; let savePath = path.join(downloadPath, fileName); // savePath基础信息 const ext = path.extname(savePath); const name = path.basename(savePath, ext); const dir = path.dirname(savePath); // 文件名自增逻辑 while (fs.pathExistsSync(savePath)) { fileNum += 1; savePath = path.format({ dir, ext, name: `${name}(${fileNum})`, }); } // 设置下载目录,阻止系统dialog的出现 item.setSavePath(savePath); // 通知渲染进程,有一个新的下载任务 win.webContents.send('new-download-item', { savePath, url, startTime, state: initialState, paused: item.isPaused(), totalBytes: item.getTotalBytes(), receivedBytes: item.getReceivedBytes(), }); // 下载任务更新 item.on('updated', (e, state) => { // eslint-disable-line win.webContents.send('download-item-updated', { startTime, state, totalBytes: item.getTotalBytes(), receivedBytes: item.getReceivedBytes(), paused: item.isPaused(), }); }); // 下载任务完成 item.on('done', (e, state) => { // eslint-disable-line win.webContents.send('download-item-done', { startTime, state, }); }); });
现在触发下载行为,文件就已经会下载到 Downloads
目录了,文件名带有自增逻辑。 同时,对下载窗口发送了关键事件,下载窗口可以根据这些事件和数据,创建、更新下载任务 。
上述步骤在渲染进程使用remote实现会有问题,无法获取到实时的下载数据。因此建议在主进程实现。
下载记录
下载功能需要缓存下载历史在本地,下载历史的数据比较多,因此我们使用 nedb 作为本地数据库。
// 初始化 nedb 数据库 const db = nedbStore({ filename, autoload: true }); ipcRenderer.on('new-download-item', (e, item) => { // 数据库新增一条新纪录 db.insert(item); // UI中新增一条下载任务 this.addItem(item); }) // 更新下载窗口的任务进度 ipcRenderer.on('download-item-updated', (e, item) => { this.updateItem(item) }) // 下载结束,更新数据 ipcRenderer.on('download-item-done', (e, item) => { // 更新数据库 db.update(item); // 更新UI中下载任务状态 this.updateItem(item); });
此时本地数据库中的数据,是这样的:
{"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/酷家乐装修网-保利金色佳苑-户型图.jpg","startTime":1560415098.731598,"state":"completed","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBAVDQKN4BE6AABAAAAACY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560415094020","_id":"6AorFZvpI0N8Yzw9"} {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/Kujiale-12.0.2-stable(1).dmg","startTime":1560415129.488072,"state":"progressing","totalBytes":80762523,"url":"https://qhstaticssl.kujiale.com/download/kjl-software12/Kujiale-12.0.2-stable.dmg?timestamp=1560415129351","_id":"YAeWIy2xoeWTw0Ht"} {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/酷家乐装修网-保利金色佳苑-户型图(1).jpg","startTime":1560418413.240669,"state":"progressing","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBBLFYKN4BE6AABAAAAADY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560418409875","_id":"obFLotKillhzTw09"} {"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/酷家乐装修网-保利金色佳苑-户型图(1).jpg","startTime":1560418413.240669,"state":"completed","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBBLFYKN4BE6AABAAAAADY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560418409875","_id":"obFLotKillhzTw09"}
在渲染进程初始化的时候,需要读取下载记录,数据按下载时间倒序。读取数量需要做一下限制,否则会影响性能,暂时限制50条。
// 渲染进程中 const db = nedbStore({ filename, autoload: true }); // 读取历史数据 const downloadHistory = await db.cfind({}).sort({ startTime: -1, }).limit(50).exec() .catch(err => logger.error(err)); if (downloadHistory) { this.setList(downloadHistory.map((d) => { const item = d; // 历史记录中,只有需要未完成和完成两个状态 if (item.state !== 'completed') { item.state = 'cancelled'; } return item; })); }
自定义下载目录
默认下载目录在Electron默认为本机上的 Downloads
目录,提供用户设置下载目录的功能,就需要在本地缓存用户自定义的下载目录。这种基础配置我们使用 electron-store 来实现
// in config.json { "downloadsPath": "/Users/ww/Downloads/归档" }
在窗口初始化的时候,检查缓存中是否有自定义下载目录,如果有则更改app的默认下载目录
componentDidMount() { const downloadsPath = store.get('downloadsPath'); if (downloadsPath) { app.setPath('downloads', downloadsPath); // app.getPath('downloads'); -> /Users/ww/Downloads/归档 } }
用户点击更换下载目录,此时需要以下步骤:
dialog.showOpenDialog
// 用户点击更改下载目录的回调 changeDoiwnloadHandler = () => { const paths = dialog.showOpenDialog({ title: '选择文件存放目录', properties: ['openDirectory'], }); if (paths && paths.length) { // 先更新一下本地缓存 store.set('downloadsPath', paths[0]); // 更新当前的下载目录 app.setPath('downloads', paths[0]); // 更新下载目录文案 this.updateDownloadsPath(); } }
计算下载进度
拿到 downloadItem 之后,可以获取到已下载的字节数和文件的总字节数,以此来计算下载进度。
const percent = item.getReceivedBytes() / item.getTotalBytes();
操作文件
在下载管理窗口中,双击下载任务可以打开该文件,点击查看按钮可以打开文件所在目录。我们统一使用Electron的 shell 模块来实现。
openFile = (path) => { if (!fs.pathExistsSync) return; // 文件不存在的情况 shell.openItem(path); // 打开文件 } openFileFolder = async (path) => { if (!fs.pathExistsSync(path)) { // 文件不存在 return; } shell.showItemInFolder(path); // 打开文件所在文件夹 }
获取文件关联图标
仔细观察下载管理窗口我们可以发现,文件的图标都是从系统获取的,和我们在文件管理器中看到的文件图标一致。
上图中dmg、jpg文件都展示了系统关联的文件图标,用户体验很好。我们可以使用 getFileIcon 来获取系统图标,以下是具体实现代码。
const { app } = require('electron').remote; // 封装一个函数 const getFileIcon = (path) => { return new Promise((resolve) => { const defaultIcon = 'some-default.jpg'; if (!path) return resolve(defaultIcon); return app.getFileIcon(path, (err, nativeImage) => { if (err) { return resolve(defaultIcon); } return resolve(nativeImage.toDataURL()); // 使用base64展示图标 }); }); }; // 获取图标 const imgSrc = await getFileIcon('./test.jpg');
最后
欢迎大家在评论区讨论,技术交流 & 内推 -> [email protected]
Recommend
-
179
标题是我以第一视角基于 Electron 开发客户端产品的体验,我将在之后分一系列文章向有兴趣的朋友一步一步介绍我是怎么从玩玩具的心态开始接触 Electron 到去开发客户端产品,最后随着业务和功能的复杂度提升再不断地优化客户端。 这是该系列的第一篇,我也是边学边...
-
153
标题是我以第一视角基于 Electron 开发客户端产品的体验,我将在之后分一系列文章向有兴趣的朋友一步一步介绍我是怎么从玩玩具的心态开始接触 Electron 到去开发客户端产品,最后随着业务和功能的复杂度提升再不断地优化客户端。
-
82
缘起 之前我用nwjs做过一个博客园文章编辑器的客户端 发了好几个版本,最后一个版本到5.0.0了 其实第一个版本已经很好了,不知足,后来自己又做了兼容markdown的,结果用来用去,发现不是自己想
-
92
README.md
-
600
本文的初衷 Electron所使用的技术栈(JavaScript、NodeJs、HTML、CSS)和web前端工程师完美契合。于是,越来越多的前端工程师,用Electron来开发桌面客户端的开发,我也是其中的一员。 虽然Electron技术栈对前端工程...
-
92
作者:钟离,酷家乐PC客户端负责人 原文地址: https://webfe.kujiale.com/browser-to-client/ 酷家乐客户端:下载地址
-
81
更新原理 在讲客户端更新方案之前,我们先了解一下web和客户端更新的原理 web应用 在web应用的世界里,我们通常会更新web服务器上的前端代码(模板、HTML,也可能是js、css),来发布新的功能。在此之后用...
-
48
前言 Electron中的进程,其实就是计算机中的进程,我们先来看看什么是进程通信 进程间通信(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法 每个进程都有自己...
-
5
Dear,大家好,我是“前端小鑫同学”,😇长期从事前端开发,安卓开发,热衷技术,在编程路上越走越远~ Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 E...
-
5
1 环境建设背景首先介绍下酷家乐的前后端架构,后端架构和大部分的互联网公司类似,分为前台、中台、基础设施,是一套微服务的架构体系,服务间依赖关系错综复杂,并且随着业务的发展服务粒度也逐渐细化,数量在增多,同时相对于线上环境,线下环境...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK