electron 實現文件下載管理器

文件下載是我們開發中比較常見的業務需求,比如:導出 excel。

web 應用文件下載存在一些局限性,通常是讓後端將響應的頭信息改成 Content-Disposition: attachment; filename=xxx.pdf,觸發瀏覽器的下載行為。

在 electron 中的下載行為,都會觸發 session 的 will-download 事件。在該事件裏面可以獲取到 downloadItem 對象,通過 downloadItem 對象實現一個簡單的文件下載管理器:

demo.gif

1. 如何觸發下載

由於 electron 是基於 chromium 實現的,通過調用 webContents 的 downloadURL 方法,相當於調用了 chromium 底層實現的下載,會忽略響應頭信息,觸發 will-download 事件。

// 觸發下載
win.webContents.downloadURL(url)

// 監聽 will-download
session.defaultSession.on('will-download', (event, item, webContents) => {})

2. 下載流程

flow_chart.png

3. 功能設計

實現一個簡單的文件下載管理器包含以下功能:

  • 設置保存路徑
  • 暫停/恢復和取消
  • 下載進度
  • 下載速度
  • 下載完成
  • 打開文件和打開文件所在位置
  • 文件圖標
  • 下載記錄

3.1 設置保存路徑

如果沒有設置保存路徑,electron 會自動彈出系統的保存對話框。不想使用系統的保存對話框,可以使用 setSavePath 方法,當有重名文件時,會直接覆蓋下載。

item.setSavePath(path)

為了更好的用戶體驗,可以讓用戶自己選擇保存位置操作。當點擊位置輸入框時,渲染進程通過 ipc 與主進程通信,打開系統文件選擇對話框。

select_path.gif

主進程實現代碼:

/**
 * 打開文件選擇框
 * @param oldPath - 上一次打開的路徑
 */
const openFileDialog = async (oldPath: string = app.getPath('downloads')) => {
  if (!win) return oldPath

  const { canceled, filePaths } = await dialog.showOpenDialog(win, {
    title: '選擇保存位置',
    properties: ['openDirectory', 'createDirectory'],
    defaultPath: oldPath,
  })

  return !canceled ? filePaths[0] : oldPath
}

ipcMain.handle('openFileDialog', (event, oldPath?: string) => openFileDialog(oldPath))

渲染進程代碼:

const path = await ipcRenderer.invoke('openFileDialog', 'PATH')

3.2 暫停/恢復和取消

拿到 downloadItem 後,暫停、恢復和取消分別調用 pauseresumecancel 方法。當我們要刪除列表中正在下載的項,需要先調用 cancel 方法取消下載。

3.3 下載進度

在 DownloadItem 中監聽 updated 事件,可以實時獲取到已下載的位元組數據,來計算下載進度和每秒下載的速度。

// 計算下載進度
const progress = item.getReceivedBytes() / item.getTotalBytes()

download_progress.png
在下載的時候,想在 Mac 系統的程序塢和 Windows 系統的任務欄展示下載信息,比如:

  • 下載數:通過 app 的 badgeCount 屬性設置,當為 0 時,不會顯示。也可以通過 dock 的 setBadge 方法設置,該方法支持的是字符串,如果不要顯示,需要設置為 ”。
  • 下載進度:通過窗口的 setProgressBar 方法設置。

由於 Mac 和 Windows 系統差異,下載數僅在 Mac 系統中生效。加上 process.platform === ‘darwin’ 條件,避免在非 Mac、Linux 系統下出現異常錯誤。

下載進度(Windows 系統任務欄、Mac 系統程序塢)顯示效果:

windows_progress.png

mac_download_progress.png

// mac 程序塢顯示下載數:
// 方式一
app.badgeCount = 1
// 方式二
app.dock.setBadge('1')

// mac 程序塢、windows 任務欄顯示進度
win.setProgressBar(progress)

3.4 下載速度

由於 downloadItem 沒有直接為我們提供方法或屬性獲取下載速度,需要自己實現。

思路:在 updated 事件里通過 getReceivedBytes 方法拿到本次下載的位元組數據減去上一次下載的位元組數據。

// 記錄上一次下載的位元組數據
let prevReceivedBytes = 0

item.on('updated', (e, state) => {
  const receivedBytes = item.getReceivedBytes()
  // 計算每秒下載的速度
  downloadItem.speed = receivedBytes - prevReceivedBytes
  prevReceivedBytes = receivedBytes
})

需要注意的是,updated 事件執行的時間約 500ms 一次。

updated_event.png

3.5 下載完成

當一個文件下載完成、中斷或者被取消,需要通知渲染進程修改狀態,通過監聽 downloadItem 的 done 事件。

item.on('done', (e, state) => {
  downloadItem.state = state
  downloadItem.receivedBytes = item.getReceivedBytes()
  downloadItem.lastModifiedTime = item.getLastModifiedTime()

  // 通知渲染進程,更新下載狀態
  webContents.send('downloadItemDone', downloadItem)
})

3.6 打開文件和打開文件所在位置

使用 electron 的 shell 模塊來實現打開文件(openPath)和打開文件所在位置(showItemInFolder)。

由於 openPath 方法支持返回值 Promise<string>,當不支持打開的文件,系統會有相應的提示,而 showItemInFolder 方法返回值是 void。如果需要更好的用戶體驗,可使用 nodejs 的 fs 模塊,先檢查文件是否存在。

import fs from 'fs'

// 打開文件
const openFile = (path: string): boolean => {
  if (!fs.existsSync(path)) return false

  shell.openPath(path)
  return true
}

// 打開文件所在位置
const openFileInFolder = (path: string): boolean => {
  if (!fs.existsSync(path)) return false

  shell.showItemInFolder(path)
  return true
}

3.7 文件圖標

很方便的是使用 app 模塊的 getFileIcon 方法來獲取系統關聯的文件圖標,返回的是 Promise<NativeImage> 類型,我們可以用 toDataURL 方法轉換成 base64,不需要我們去處理不同文件類型顯示不同的圖標。

const getFileIcon = async (path: string) => {
  const iconDefault = './icon_default.png'
  if (!path) Promise.resolve(iconDefault)

  const icon = await app.getFileIcon(path, {
    size: 'normal'
  })

  return icon.toDataURL()
}

3.8 下載記錄

隨着下載的歷史數據越來越多,使用 electron-store 將下載記錄保存在本地。

其他

項目的地址:
//github.com/tal-tech/electron-playground

如果想看更完整的文檔,請參考下面文檔
Electron-Playground 官方文檔