资讯专栏INFORMATION COLUMN

【Electron】酷家乐客户端开发实践分享 — 下载管理器

yuxue / 397人阅读

摘要:作者钟离,酷家乐客户端负责人原文地址酷家乐客户端下载地址文章背景在酷家乐客户端在改版成功后,我们积累了许多的宝贵的经验和最佳实践。

作者:钟离,酷家乐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是一致的,页面中的a标签或者js跳转等等行为都可能触发下载,具体视访问的资源而定。什么样的资源会触发浏览器的下载行为呢?

response header中的Content-Disposition为attachment。参考MDN Content-Disposition

response header中的Content-Type,是浏览器无法直接打开的文件类型,例如application/octet-stream,此时取决于浏览器的具体实现了。例子: IE无法打开pdf文件,chrome可以直接打开pdf文件,因此pdf类型的url在chrome上可以直接打开,而在IE下会触发下载行为。

在Electron中还有一种方法可以触发下载: webContents.download。相当于直接调用chromium底层的下载逻辑,忽略headers中的那些判断,直接下载。

上述两种下载行为,都会触发session的will-download事件,在这里可以获取到关键的downloadItem对象

整体流程

设置文件路径

如果不做任何处理的话,触发下载行为时Electron会弹出一个系统dialog,让用户来选择文件存放的目录。这个体验并不好,因此我们首先需要把这个系统dialog去掉。使用downloadItem.savePath即可。

// 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/归档
    }
}

用户点击更换下载目录,此时需要以下步骤:

弹出文件目录选择dialog,使用dialog.showOpenDialog实现

更新本地缓存中的自定义下载目录

修改当前app的默认下载目录

更新下载窗口中的下载目录文案

// 用户点击更改下载目录的回调
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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/54036.html

相关文章

  • Electron家乐户端开发实践分享下载管理

    摘要:作者钟离,酷家乐客户端负责人原文地址酷家乐客户端下载地址文章背景在酷家乐客户端在改版成功后,我们积累了许多的宝贵的经验和最佳实践。 作者:钟离,酷家乐PC客户端负责人原文地址:https://webfe.kujiale.com/electron-ku-jia-le-ke-hu-duan-kai-fa-shi-jian-fen-xiang-jin-cheng-tong-xin/酷家乐客...

    zhouzhou 评论0 收藏0
  • Electron家乐户端开发实践分享下载管理

    摘要:作者钟离,酷家乐客户端负责人原文地址酷家乐客户端下载地址文章背景在酷家乐客户端在改版成功后,我们积累了许多的宝贵的经验和最佳实践。 作者:钟离,酷家乐PC客户端负责人原文地址:https://webfe.kujiale.com/electron-ku-jia-le-ke-hu-duan-kai-fa-shi-jian-fen-xiang-jin-cheng-tong-xin/酷家乐客...

    褰辩话 评论0 收藏0
  • Electron家乐户端开发实践分享 — 软件自动更新

    摘要:作者钟离,酷家乐客户端负责人原文地址酷家乐客户端下载地址文章背景在酷家乐客户端在改版成功后,我们积累了许多的宝贵的经验和最佳实践。用户在电脑上安装客户端,实际上会将客户端代码文件持久储存到本机。通常我们会在软件启动时检查更新。 作者:钟离,酷家乐PC客户端负责人原文地址:https://webfe.kujiale.com/electron-autoupdate/酷家乐客户端:下载地址...

    phpmatt 评论0 收藏0
  • Electron家乐户端开发实践分享 — 软件自动更新

    摘要:作者钟离,酷家乐客户端负责人原文地址酷家乐客户端下载地址文章背景在酷家乐客户端在改版成功后,我们积累了许多的宝贵的经验和最佳实践。用户在电脑上安装客户端,实际上会将客户端代码文件持久储存到本机。通常我们会在软件启动时检查更新。 作者:钟离,酷家乐PC客户端负责人原文地址:https://webfe.kujiale.com/electron-autoupdate/酷家乐客户端:下载地址...

    leiyi 评论0 收藏0
  • Electron家乐户端开发实践分享 — 浏览启动户端

    摘要:作者钟离,酷家乐客户端负责人原文地址酷家乐客户端下载地址文章背景在酷家乐客户端在改版成功后,我们积累了许多的宝贵的经验和最佳实践。钟离可以注册多个协议接收参数协议注册完毕之后,我们已经可以在浏览器中,通过访问自定义协议来启动客户端了。 作者:钟离,酷家乐PC客户端负责人原文地址:https://webfe.kujiale.com/browser-to-client/酷家乐客户端:下载...

    Cciradih 评论0 收藏0

发表评论

0条评论

yuxue

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<