【Electron】酷家樂客戶端開發實踐分享 — 下載管理器

做者:鍾離,酷家樂PC客戶端負責人css

原文地址:webfe.kujiale.com/electron-ku…前端

酷家樂客戶端:下載地址 www.kujiale.com/activity/13…git

文章背景:在酷家樂客戶端在V12改版成功後,咱們積累了許多的寶貴的經驗和最佳實踐。前端社區裏關於Electron知識相對較少,所以但願將這些內容以系列文章的形式分享出來。github

系列文章:web

背景

打開酷家樂客戶端,能夠在左下角的更多菜單中找到下載管理這個功能,今天咱們就來看看在Electron中如何實現一個下載管理器。chrome

下載管理器

如何觸發下載行爲

因爲Electron渲染層是基於chromium的,觸發下載的邏輯和chromium是一致的,頁面中的a標籤或者js跳轉等等行爲均可能觸發下載,具體視訪問的資源而定。什麼樣的資源會觸發瀏覽器的下載行爲呢?shell

  1. response header中的Content-Disposition爲attachment。參考MDN Content-Disposition
  2. response header中的Content-Type,是瀏覽器沒法直接打開的文件類型,例如application/octet-stream,此時取決於瀏覽器的具體實現了。例子: IE沒法打開pdf文件,chrome能夠直接打開pdf文件,所以pdf類型的url在chrome上能夠直接打開,而在IE下會觸發下載行爲。

在Electron中還有一種方法能夠觸發下載: webContents.download。至關於直接調用chromium底層的下載邏輯,忽略headers中的那些判斷,直接下載。數據庫

上述兩種下載行爲,都會觸發session的will-download事件,在這裏能夠獲取到關鍵的downloadItem對象json

總體流程

流程圖

設置文件路徑

若是不作任何處理的話,觸發下載行爲時Electron會彈出一個系統dialog,讓用戶來選擇文件存放的目錄。這個體驗並很差,所以咱們首先須要把這個系統dialog去掉。使用downloadItem.savePath便可。api

// 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/歸檔
    }
}
複製代碼

用戶點擊更換下載目錄,此時須要如下步驟:

  1. 彈出文件目錄選擇dialog,使用dialog.showOpenDialog實現
  2. 更新本地緩存中的自定義下載目錄
  3. 修改當前app的默認下載目錄
  4. 更新下載窗口中的下載目錄文案
// 用戶點擊更改下載目錄的回調
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

相關文章
相關標籤/搜索