作者:酷家乐PC客户端负责人钟力原文地址:https://webfe.kujiale.com/electron-ku-jia-le-ke-hu-duan-kai-fa-shi-jian-fen-xiang-jin-cheng-tong-xin/酷家乐客户端:下载地址https://www.kujiale.com/activity/136文章背景:我们积累了很多宝贵的经验酷家乐客户端V12改版成功后的最佳实践。前端社区对Electron的了解比较少,所以希望以系列文章的形式分享这方面的内容。系列文章:【Electron】酷家乐客户端开发实践分享-入坑【Electron】酷家乐客户端开发实践分享-软件自动更新【Electron】酷家乐客户端开发实践分享-浏览器启动客户端【Electron】酷家乐客户端开发实践分享-过程交流【Electron】酷家乐客户端开发实践分享-下载管理器不定期更新...后台打开酷家乐客户端,可以在左下角更多菜单中找到下载管理功能。今天我们来看看如何在Electron中实现下载管理器。如何触发下载行为由于Electron渲染层是基于chromium的,所以触发下载的逻辑和chromium是一样的。页面中的标签或js跳转等行为可能会触发下载,具体取决于访问的资源。什么样的资源会触发浏览器的下载行为?响应头中的Content-Disposition是附件。参考MDNContent-Dispositionresponse头中的Content-Type,是浏览器无法直接打开的文件类型,如application/octet-stream,具体取决于浏览器的具体实现。例子:IE不能打开pdf文件,但是chrome可以直接打开pdf文件,所以pdf类型的url可以直接在chrome上打开,但是在IE下会触发下载行为。Electron中还有一个触发下载的方法:webContents.download。相当于直接调用chromium底层的下载逻辑,忽略headers中的那些判断,直接下载。以上两个下载行为都会触发session的will-download事件,这里可以获取关键的downloadItem对象整体流程设置文件路径如果不做任何处理,Electron会在触发下载行为时弹出一个系统对话框,让用户选择存储文件的目录。这个体验不好,所以我们首先要去掉这个系统对话框。只需使用downloadItem.savePath。//设置保存路径,Electron不提示保存dialog.downloadItem.setSavePath('/tmp/save.pdf');设置文件的默认下载路径,需要考虑文件名重复,一般来说使用文件名自增的逻辑,例如:test.jpg,test.jpg(1)格式。默认的文件存放目录也是一个问题。我们统一使用app.getPath('downloads')作为文件下载目录。为了用户体验,后期会提供修改文件下载目录的功能。//在main.js主进程中const{session}=require('electron');session.defaultSession.on('will-download',async(event,item)=>{constfileName=item.getFilename();consturl=item.getURL();conststartTime=item.getStartTime();constinitialState=item.getState();constdownloadPath=app.getPath('downloads');letfileNum=0;letsavePath=path.join(downloadPath,fileName);//保存路径基本信息constext=path.extname(savePath);constname=path.basename(savePath,ext);constdir=path.dirname(savePath);//文件名自增Logicwhile(fs.pathExistsSync(savePath)){fileNum+=1;savePath=path.format({dir,ext,name:`${name}(${fileNum})`,});}//设置下载目录,防止系统对话框出现item.setSavePath(savePath);//通知渲染进程有新的下载任务win.webContents.send('new-download-item',{savePath,url,startTime,state:initialState,暂停:item.isPaused(),totalBytes:item.getTotalBytes(),receivedBytes:item.getReceivedBytes(),});//下载任务更新item.on('updated',(e,state)=>{//eslint-disable-linewin.webContents.send('download-item-updated',{startTime,state,totalBytes:item.getTotalBytes(),receivedBytes:item.getReceivedBytes(),paused:item.isPaused(),});});//下载任务完成item.on('done',(e,state)=>{//eslint-disable-linewin.webContents.send('download-item-done',{startTime,state,});});});现在下载行为被触发,文件将被下载到Downloads目录。文件名有自增逻辑。同时,按键事件被发送到下载窗口。下载窗口可以根据这些事件和数据创建和更新下载任务。如果在渲染过程中使用remote实现上述步骤,就会出现问题,无法获取到实时下载数据。所以建议在主进程中实现。下载记录下载功能需要在本地缓存下载历史,而下载历史数据比较大,所以我们使用nedb作为本地数据库。//初始化nedb数据库constdb=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,"reves:"/Users/ww/Downloads/Kujiale-12.0.2-stable(1).dmg"“startTime”:1560415129.488072,“state”:“进行中”,“totalBytes”:80762523,“url”:“https://qhstaticssl.kujiale.com/download/kjl-software12/Kujiale-12.0.2-stable.dmg?timestamp=1560415129351”,“_id”:“YAeWIy2xoeWTw0Ht”}{“暂停”:false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/酷家乐装修网-聚金花园-平面图(一).jpg","startTime":1560418413.240669,"state":"进行中","totalBytes":236568,"url":"https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBBLFYKN4BE6AABAAAAAADY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.15607584098"obTw098"obTw09""}{"paused":false,"receivedBytes":0,"savePath":"/Users/ww/Downloads/酷家乐装修网-聚金花园-平面图(1).jpg","startTime":1560418413.240669,“状态”:“已完成”,“totalBytes”:236568,“url”:“https://qhtbdoss.kujiale.com/fpimgnew/prod/3FO4EGX11S9S/op/LUBBLFYKN4BE6AABAAAAAADY8.jpg?kpm=9V8.32a74ad82d44e7d0.3dba44f.1560418409875","_id":"obFLotKillhzTw09"}初始化渲染进程时,需要读取下载记录,需要限制按下载时间倒序读取的数据数量,否则会影响性能,暂时限制50个条目。//在渲染过程中constdb=nedbStore({filename,autoload:true});//读取历史数据constdownloadHistory=awaitdb.cfind({}).sort({startTime:-1,}).limit(50).exec().catch(err=>logger.error(err));if(downloadHistory){this.setList(downloadHistory.map((d)=>{constitem=d;//在历史中,只需要未完成和完成两种状态if(item.state!=='completed'){item.state='cancelled';}returnitem;}));}自定义下载目录默认下载目录是Electron默认的本机Downloads目录为用户提供了设置下载目录的功能,所以用户自定义下载目录需要缓存到本地。我们使用electron-store来实现这个基本配置//在config.json{"downloadsPath":"/Users/ww/Downloads/archive"}窗口初始化时,检查缓存中是否有自定义下载目录,如果是,则更改应用程序的默认下载目录componentDidMount(){constdownloadsPath=store.get('downloadsPath');如果(downloadsPath){app.setPath('downloads',downloadsPath);//app.getPath('下载');->/Users/ww/Downloads/archive}}用户点击更改下载目录。此时需要进行如下操作:弹出文件目录选择对话框,使用dialog.showOpenDialog更新本地缓存中的自定义下载目录并修改当前app默认下载目录更新下载目录文案下载窗口//用户点击改变下载目录的回调changeDoiwnloadHandler=()=>{constpaths=dialog.showOpenDialog({title:'选择文件存放目录',properties:['openDirectory'],});if(paths&&paths.length){//先更新本地缓存store.set('downloadsPath',paths[0]);//更新当前下载目录app.setPath('downloads',paths[0]);//更新下载目录copythis.updateDownloadsPath();}}计算下载进度获取downloadItem后,可以获取下载的字节数和文件的总字节数来计算下载进度。constpercent=item.getReceivedBytes()/item.getTotalBytes();操作文件在下载管理窗口,双击下载任务打开文件,点击查看按钮打开文件所在目录。我们统一使用Electron的shell模块来实现。openFile=(path)=>{如果(!fs.pathExistsSync)返回;//文件不存在shell.openItem(path);//打开文件}openFileFolder=async(path)=>{if(!fs.pathExistsSync(path)){//文件不存在return;}shell.showItemInFolder(路径);//打开文件所在文件夹}获取文件关联图标仔细观察下载管理窗口可以发现,文件的图标是从系统获取的,与我们在文件中看到的文件图标一致经理。上图中的dmg和jpg文件都显示了与系统关联的文件图标,用户体验非常好。我们可以使用getFileIcon来获取系统图标,下面是具体的实现代码。const{app}=require('electron').remote;//封装一个函数constgetFileIcon=(path)=>{returnnewPromise((resolve)=>{constdefaultIcon='some-default.jpg';if(!path)returnresolve(defaultIcon);returnapp.getFileIcon(path,(err,nativeImage)=>{if(err){returnresolve(defaultIcon);}returnresolve(nativeImage.toDataURL());//使用base64显示图标});});};//获取图标constimgSrc=awaitgetFileIcon('./test.jpg');最后欢迎大家在评论区讨论,技术交流&内推->zhongli@qunhemail.com
