[ Day 36 ] - Electron 应用程序 - 更新自动化 ( 实做篇 )

昨天我们谈论了 Electron 应用程序手动更新的流程 ,

今天 , 我们就在 app 开启时放入昨天那些手动更新的流程 ,

让我们的 app 有自动更新的功能 (^.^)/


更新流程分析

类型 流程
Portal 下载新执行档 => 取代旧档
NSIS 下载安装档 => 执行安装

Portal 的更新与 NSIS 的更新流程差别不大 , 因此下方本鲁只介绍 NSIS 的更新流程 ,

相信厉害的邦友们 , 必定能举一反三 , 写出 Portal 版本的更新自动化

下载档案

我们利用 download.js 辅助我们下载新版的 exe 档案

首先 , 制作 downloadUtil.js 方便我们之後操作下载行为

// utils/downloadUtil.js 
const download = require('download');
const fs = require('fs');
const _ = require('lodash');

const fileDownload = (url, dest) => {

  // duplexStream is a Promise & EventEmitter
  const duplexStream = download(url);

  let downloadedLength = 0;
  const writeStream = fs.createWriteStream(dest);

  // 完成写入档案到指定位置
  writeStream.on("finish", () => duplexStream.emit('write-finish'));

  // 写入档案出错时
  writeStream.on("error", err => duplexStream.emit('write-error', err));

  // 限制每 0.5 秒至多执行 1 次
  const throttleFunc = _.throttle(func => func(), 500);

  duplexStream.on('response', res => {
    const totalLength = res.headers['content-length'];

    res.on('data', data => {
      downloadedLength += data.length;

      // 因为 duplexStream 是 EventEmitter 所以 emit channel : "got-data"
      const params = {data, downloadedLength, totalLength};
      throttleFunc(() => duplexStream.emit('got-data', params));
    });
  });

  duplexStream.on("error", err => console.error(err));

  duplexStream.pipe(writeStream);

  // duplexStream.pause();  // 下载暂停
  // duplexStream.resume(); // 下载继续
  return duplexStream;
}

module.exports = fileDownload;

如何使用 downloadUtil.js ?

const doDownload = (url, dest, cb) => {

  return new Promise((resolve, reject) => {

    downloadUtil(url, dest)
      .on('got-data', ({downloadedLength, totalLength}) => {

         const saved = new Intl.NumberFormat().format(downloadedLength);
         const total = new Intl.NumberFormat().format(totalLength);
         const percent = ((downloadedLength / totalLength) * 100).toFixed(4)
         console.log(`downloaded :  ${saved} / ${total}  ( ${percent} % ) `);
          
         // 取得资料时 , 呼叫回呼函式 cb
         cb && cb({downloadedLength, totalLength});
      })
      .on('write-finish', resolve)
      .catch(reject);
  })
}

显示对话框 , 让使用者决定是否要更新到最新的版本

// 更新对话框
function createUpdateWindow() {
    const win = new BrowserWindow({
        width: 400,
        height: 200,
        frame: false,      // 标题列不显示
        transparent: true, // 背景透明
        autoHideMenuBar: true, //  工具列不显示
        webPreferences: {
            nodeIntegration: true,
        },
    });
    win.loadFile('./update.html');
    return win;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Update Confirm</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <style>
        body {
            user-select: none;
            height: 100%;
            background-color: #3cc245;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        h1 {
            margin: 20px;
        }
    </style>
</head>
<body>
<h1>有新的版本可使用 , 您是否要更新 ?</h1>
<div class="progress" style="width: 150px;display: none">
    <div class="progress-bar progress-bar-striped progress-bar-animated"></div>
</div>
<div id="btn-group">
    <button class="btn btn-primary" onclick="confirm()">更新</button>
    <button class="btn btn-secondary" onclick="cancel()">取消</button>
</div>
<script>
    // ...JS code , 可参考 https://github.com/andrew781026/ithome_ironman_2020/blob/master/day-36/update.html
</script>
</body>
</html>

完整下载安装档後 , 利用 spawn 执行它

利用昨天整理的 doInstall 函式 , 执行之

如有需求背景执行 , 可追加 args 参数

参数 说明
--updated 更新模式
/S 背景执行 ( silent mode )
--force-run 安装完成 , 执行应用程序
const doInstall = (exe = 'installer_path', args = ["--updated"]) => {

    return new Promise((resolve, reject) => {

        const process = spawn(exe, args, {
            detached: true,  // 让执行绪与 NodeJS 脱钩
            stdio: "ignore",
        })
        process.on("error", error => reject(error))
        process.unref()

        if (process.pid) resolve(true);
    })
}

利用 spawn 执行时 , 会出现安装画面

之後你利用捷径 开启应用程序 , 就会看到更新後的版本了 !

成果图

更新

不执行更新

下方为范例的安装档

参考资料

今年小弟第一次参加 `铁人赛` , 如文章有误 , 请各位前辈提出指正 , 感谢  <(_ _)>

<<:  [鼠年全马] W39 - 使用Vuex管理资料状态(下)

>>:  日记20

【第一天 - CTF介绍】

CTF 全名 Capture The Flag,并且分为下列几类的解题方式 解谜式(Jeopard...

Day 18 ( 中级 ) 地球绕着太阳转

地球绕着太阳转 教学原文参考:地球绕着太阳转 这篇文章会介绍,如何在 Scratch 3 里使用重复...

Day 28 - 设定 GRE Tunnel

如果有用过 HE 提供的 Tunnel Broker 服务的话,应该对 SIT 隧道不陌生。 但是,...

[Day26] 透过GCP实作(2/4):进行前後端分离

在昨日的文章中,简单地向各位展示直接藉由Function抓取API 所能得到的架构会是何者 而今天...

30天零负担轻松学会制作APP介面及设计【DAY 26】

大家好,我是YIYI,今天我要来检讨一下目前的问题~ 问题 第一个部分是页面,我认为可以再增加一些页...