做者:鍾離,酷家樂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知識相對較少,所以但願將這些內容以系列文章的形式分享出來。
系列文章:css
打開酷家樂客戶端,能夠在左下角的更多菜單中找到下載管理這個功能,今天咱們就來看看在Electron中如何實現一個下載管理器。前端
因爲Electron渲染層是基於chromium的,觸發下載的邏輯和chromium是一致的,頁面中的a標籤或者js跳轉等等行爲均可能觸發下載,具體視訪問的資源而定。什麼樣的資源會觸發瀏覽器的下載行爲呢?git
Content-Disposition
爲attachment。參考MDN Content-Disposition Content-Type
,是瀏覽器沒法直接打開的文件類型,例如application/octet-stream
,此時取決於瀏覽器的具體實現了。例子: IE沒法打開pdf文件,chrome能夠直接打開pdf文件,所以pdf類型的url在chrome上能夠直接打開,而在IE下會觸發下載行爲。在Electron中還有一種方法能夠觸發下載: webContents.download。至關於直接調用chromium底層的下載邏輯,忽略headers中的那些判斷,直接下載。github
上述兩種下載行爲,都會觸發session的will-download事件,在這裏能夠獲取到關鍵的downloadItem對象web
若是不作任何處理的話,觸發下載行爲時Electron會彈出一個系統dialog,讓用戶來選擇文件存放的目錄。這個體驗並很差,所以咱們首先須要把這個系統dialog去掉。使用downloadItem.savePath
便可。chrome
// 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')
做爲文件下載目錄。爲了用戶體驗,後續提供修改文件下載目錄功能便可。shell
// 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做爲本地數據庫。json
// 初始化 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); });
此時本地數據庫中的數據,是這樣的:api
{"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');
歡迎你們在評論區討論,技術交流 & 內推 -> zhongli@qunhemail.com