1. 程式人生 > >electron 實現檔案下載管理器

electron 實現檔案下載管理器

檔案下載是我們開發中比較常見的業務需求,比如:匯出 excel。 web 應用檔案下載存在一些侷限性,通常是讓後端將響應的頭資訊改成 `Content-Disposition: attachment; filename=xxx.pdf`,觸發瀏覽器的下載行為。 在 electron 中的下載行為,都會觸發 session 的 [will-download](https://www.electronjs.org/docs/api/session#instance-events) 事件。在該事件裡面可以獲取到 [downloadItem](https://www.electronjs.org/docs/api/download-item) 物件,通過 [downloadItem](https://www.electronjs.org/docs/api/download-item) 物件實現一個簡單的檔案下載管理器: ![demo.gif](https://cdn.nlark.com/yuque/0/2020/gif/1233799/1603696161110-a6e96603-0205-4807-9861-4b98eaaeee38.gif#align=left&display=inline&height=400&margin=%5Bobject%20Object%5D&name=demo.gif&originHeight=400&originWidth=599&size=133118&status=done&style=stroke&width=599) ## **1. 如何觸發下載** 由於 electron 是基於 chromium 實現的,通過呼叫 webContents 的 [downloadURL](https://www.electronjs.org/docs/api/web-contents#contentsdownloadurlurl) 方法,相當於呼叫了 chromium 底層實現的下載,會忽略響應頭資訊,觸發 [will-download](https://www.electronjs.org/docs/api/session#instance-events) 事件。 ```javascript // 觸發下載 win.webContents.downloadURL(url) // 監聽 will-download session.defaultSession.on('will-download', (event, item, webContents) => {}) ``` ## **2. 下載流程** ![flow_chart.png](https://cdn.nlark.com/yuque/0/2020/png/1233799/1603443257821-3b875f31-208c-4b63-9c64-2fc69e4bb9a5.png#align=left&display=inline&height=664&margin=%5Bobject%20Object%5D&name=flow_chart.png&originHeight=1256&originWidth=518&size=90209&status=done&style=stroke&width=274) ## **3. 功能設計** 實現一個簡單的檔案下載管理器包含以下功能: - 設定儲存路徑 - 暫停/恢復和取消 - 下載進度 - 下載速度 - 下載完成 - 開啟檔案和開啟檔案所在位置 - 檔案圖示 - 下載記錄 ### **3.1 設定儲存路徑** 如果沒有設定儲存路徑,electron 會自動彈出系統的儲存對話方塊。不想使用系統的儲存對話方塊,可以使用 [setSavePath](https://www.electronjs.org/docs/api/download-item#downloaditemsetsavepathpath) 方法,當有重名檔案時,會直接覆蓋下載。 ```javascript item.setSavePath(path) ``` 為了更好的使用者體驗,可以讓使用者自己選擇儲存位置操作。當點選位置輸入框時,渲染程序通過 ipc 與主程序通訊,開啟系統檔案選擇對話方塊。 ![select_path.gif](https://cdn.nlark.com/yuque/0/2020/gif/1233799/1604307314902-a39d6a37-c3f4-422b-a500-d1f6d603989a.gif#align=left&display=inline&height=561&margin=%5Bobject%20Object%5D&name=select_path.gif&originHeight=561&originWidth=818&size=701341&status=done&style=none&width=818) 主程序實現程式碼: ```typescript /** * 開啟檔案選擇框 * @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)) ``` 渲染程序程式碼: ```javascript const path = await ipcRenderer.invoke('openFileDialog', 'PATH') ``` ### **3.2 暫停/恢復和取消** 拿到 [downloadItem](https://www.electronjs.org/docs/api/download-item) 後,暫停、恢復和取消分別呼叫 `pause`、`resume` 和 `cancel` 方法。當我們要刪除列表中正在下載的項,需要先呼叫 cancel 方法取消下載。 ### **3.3 下載進度** 在 DownloadItem 中監聽 updated 事件,可以實時獲取到已下載的位元組資料,來計算下載進度和每秒下載的速度。 ```javascript // 計算下載進度 const progress = item.getReceivedBytes() / item.getTotalBytes() ``` ![download_progress.png](https://cdn.nlark.com/yuque/0/2020/png/1233799/1603443428433-83b1fb15-d070-43cc-a792-7e181add4cd2.png#align=left&display=inline&height=164&margin=%5Bobject%20Object%5D&name=download_progress.png&originHeight=164&originWidth=1192&size=28095&status=done&style=none&width=1192) 在下載的時候,想在 Mac 系統的程式塢和 Windows 系統的工作列展示下載資訊,比如: - 下載數:通過 app 的 [badgeCount](https://www.electronjs.org/docs/api/app#appbadgecount-linux-macos) 屬性設定,當為 0 時,不會顯示。也可以通過 dock 的 [setBadge](https://www.electronjs.org/docs/api/app#appsetbadgecountcount-linux-macos) 方法設定,該方法支援的是字串,如果不要顯示,需要設定為 ''。 - 下載進度:通過視窗的 [setProgressBar](https://www.electronjs.org/docs/api/browser-window#winsetprogressbarprogress-options) 方法設定。 > 由於 Mac 和 Windows 系統差異,下載數僅在 Mac 系統中生效。加上 process.platform === 'darwin' 條件,避免在非 Mac、Linux 系統下出現異常錯誤。 下載進度(Windows 系統工作列、Mac 系統程式塢)顯示效果: ![windows_progress.png](https://cdn.nlark.com/yuque/0/2020/png/1233799/1603443468964-53d9170a-1ac6-4b53-9a0c-8525ec43139a.png#align=left&display=inline&height=53&margin=%5Bobject%20Object%5D&name=windows_progress.png&originHeight=53&originWidth=196&size=10184&status=done&style=none&width=196) ![mac_download_progress.png](https://cdn.nlark.com/yuque/0/2020/png/1233799/1603443459605-0705af0d-ed7e-45ef-80b8-179b9d128078.png#align=left&display=inline&height=118&margin=%5Bobject%20Object%5D&name=mac_download_progress.png&originHeight=118&originWidth=108&size=17008&status=done&style=none&width=108) ```javascript // mac 程式塢顯示下載數: // 方式一 app.badgeCount = 1 // 方式二 app.dock.setBadge('1') // mac 程式塢、windows 工作列顯示進度 win.setProgressBar(progress) ``` ### **3.4 下載速度** 由於 [downloadItem](https://www.electronjs.org/docs/api/download-item) 沒有直接為我們提供方法或屬性獲取下載速度,需要自己實現。 > 思路:在 updated 事件裡通過 getReceivedBytes 方法拿到本次下載的位元組資料減去上一次下載的位元組資料。 ```javascript // 記錄上一次下載的位元組資料 let prevReceivedBytes = 0 item.on('updated', (e, state) => { const receivedBytes = item.getReceivedBytes() // 計算每秒下載的速度 downloadItem.speed = receivedBytes - prevReceivedBytes prevReceivedBytes = receivedBytes }) ``` > 需要注意的是,updated 事件執行的時間約 500ms 一次。 ![updated_event.png](https://cdn.nlark.com/yuque/0/2020/png/1233799/1604396024337-516a4cb3-eb2b-4757-9115-8222d5bb7287.png#align=left&display=inline&height=286&margin=%5Bobject%20Object%5D&name=updated_event.png&originHeight=286&originWidth=878&size=128126&status=done&style=none&width=878) ### **3.5 下載完成** 當一個檔案下載完成、中斷或者被取消,需要通知渲染程序修改狀態,通過監聽 [downloadItem](https://www.electronjs.org/docs/api/download-item) 的 done 事件。 ```javascript 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`,當不支援開啟的檔案,系統會有相應的提示,而 showItemInFolder 方法返回值是 `void`。如果需要更好的使用者體驗,可使用 nodejs 的 fs 模組,先檢查檔案是否存在。 ```typescript 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](https://www.electronjs.org/docs/api/app#appgetfileiconpath-options) 方法來獲取系統關聯的檔案圖示,返回的是 `Promise` 型別,我們可以用 toDataURL 方法轉換成 base64,不需要我們去處理不同檔案型別顯示不同的圖示。 ```typescript 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](https://github.com/sindresorhus/electron-store) 將下載記錄儲存在本地。 ## **其他** 專案的地址: [https://github.com/tal-tech/electron-playground](https://github.com/tal-tech/electron-playground) 如果想看更完整的文件,請參考下面文件 [Electron-Playground 官方文件](https://www.yuque.com/ezg6c4/