简单且详细地实现 Electron 自动更新(Auto Update)

简单且详细地实现 Electron 自动更新(Auto Update)

前言

最近团队接到一个新需求要用到 Electron 相关的技术开发端上应用(Windows 和 Linux),然后笔者选择的就是自己比较熟悉的技术栈 React + Electron 来进行项目开发。开发过程中其实就是纯 Web 体验,然后在迭代了几个版本之后,用户就反馈“能不能增加自动更新功能?”因为现在每次都是将安装包放到一个 CDN 上然后告诉用户下载重新安装替换原来的版本,原本我觉得自动更新是 Electron 的一个很简单的配置,应该花费不了太多时间,但是事与愿违,在查看了文档以及各种网上资料过后,磕磕绊绊的终于实现了 Windows 和 Linux 下应用自动更新的功能,在这里就把整个过程记录下来,帮助后续有需要的朋友。

需要强调一下,可能 Electron 自动更新真的是一个简单的功能,但是从网上能查到的资料来看,大家真的对新手不负责,对自己写的文章不负责,基本上 10 篇文章都说能自动更新,9 篇文章的代码更新不了无法使用甚至是根本运行不了,笔者相信他们自己是会自动更新的,但是他们写出来的东西真的不是为了告诉别人怎么实现自动更新的,应该单纯就是凑篇文章吧。咱就说也不是啥重要的内容,藏着掖着真没必要。

更新前准备

本文主要是讲 Electron 的自动更新,因此核心是自动更新的逻辑,所以无论你是原生 Electron 还是 React/Vue 的 Electron 大前端开发,本文只集中在核心的自动更新逻辑部分,并且因为前面提到过,笔者对 Electron 也不是很熟悉,所以不会讲过多内部源码细节,你就算问我也不会其实😄。

Electron 代码仓库

笔者这边使用的是一个通用的脚手架来做示例代码的,脚手架地址electron-react-boilerplate,还是那句话既然你看这篇文章了,那么说明至少你和我一样是接触过前端正在搞 Electron 的,更多基础的东西我就不介绍了。

本文所有代码均在本地真实地运行跑通过,所有的截图均来自 electron-react-auto-update 代码仓库。

更新服务器

通常官方文档提供的更新示例都是极简的,比如写一个简单的脚手架然后推到 Github 上,通过 Github 的 Releases 服务进行更新,因此他们的服务配置一般来说都是这样:

{
    ...
    "build": {
        ...,
           "publish": {
              "provider": "github",
              "owner": "electron-react-boilerplate",
              "repo": "electron-react-boilerplate"
            }
    }
}

这里的 Provider 比较重要,简单来说就是表示以何种方式进行应用的升级,以下是一些可能的 provider 值及其含义(我觉得必要的知识还是需要介绍大家了解一下的):

  • github: 使用 GitHub Releases 作为更新源。
  • gitlab: 使用 GitLab Releases 作为更新源。
  • s3: 使用 Amazon S3 或兼容 S3 的存储作为更新源。
  • spaces: 使用 DigitalOcean Spaces 作为更新源。
  • generic: 使用一个通用的 HTTP 服务器作为更新源。这时,你需要手动上传更新文件到你指定的服务器,并确保访问 URL 正确。

官方文档这么介绍没问题,因为官方需要保证通用性简便性,但是其实 90% 的应用场景肯定都是私有化部署升级,因此我这边只介绍 generic 这种方式,也就是使用 HTTP 服务器作为更新源来升级,这样的话就需要文件服务器,因此在实现代码更新之前各位开发者需要准备一个文件服务器。为了简单,我为大家实现了一个简易的本地离线文件服务器,并作了如下约定:

  1. 目前仓库代码开发模式版本为 v0.1.0
  2. 我已经提前打包好了 v0.3.6 的版本更新所需的内容放到了本地服务器
  3. 服务器地址就是仓库下 file-server 文件夹
  4. npm install 之后在根目录下运行 npm run start:file-server 即可启动服务器

文件服务器启动效果如下图:

简单且详细地实现 Electron 自动更新(Auto Update)

简单且详细地实现 Electron 自动更新(Auto Update)

至此,前期准备工作完成,也就是说开发者将仓库代码 Clone 到本地(直接下载也可以)之后,本地的代码就是 0.1.0 的版本(这里的当前版本脚手架读取的是 /release/app/package.json 里面的 version 字段),然后如果想查看更新效果,运行本地的文件更新服务器,打包之后就可以实现自动更新功能的试用。

废话不多说,接下来直接进行应用更新代码部分的讲解,我会非常详细的为大家讲解 Electron 应用自动更新的步骤。

简单的自动更新

需要注意的是,在开发模式下是无法预览自动更新的,网上有很多办法说可以,我试了一圈都不行,是我自己太菜的问题,所以我建议大家在生产模式下进行验证。

对于我提供的示例代码,大家只需要执行如下几步:

  1. Clone 代码到本地(直接下载也可以)
  2. npm install 安装必要依赖
  3. npm run package 打包应用
  4. 访问生产包,路径:/release/build/xxxx.exe(Windows) 或者 /release/build/xxxx.AppImage(Linux)

执行完上述操作过后,再根据前面的步骤把文件服务器启动起来,点击打开应用,就能查看应用的自动更新效果。

  • Windows 下的自动更新

简单且详细地实现 Electron 自动更新(Auto Update)

  • Linux 下的自动更新

简单且详细地实现 Electron 自动更新(Auto Update)

从上面两个效果图可以看的出,在 Linux 和 Windows 系统下面均实现了应用的自动更新功能,虽然略显粗糙但是毕竟是可用的。既然这里已经证明了方案的可行性,那么接下来的所有代码和示例截图我就都使用 Windows 系统来进行了! 因为双系统切换截图还是比较麻烦的。

核心代码讲解

接下来,我们进行核心代码的讲解。Electron 的自动更新逻辑还是比较简单的,我这边分为两部分,一部分是更新逻辑,一部分是主线程逻辑,简单的更新逻辑并不需要渲染线程,完全托管在 Electron 原生里面。具体代码如下:

  • 更新逻辑核心代码
// src/main/auto-update.ts
export async function autoUpdateApp() {
  // 等待 3 秒再检查更新,确保窗口准备完成,用户进入系统
  await sleep(3000);
  // 每次启动自动更新检查更新版本
  autoUpdater.checkForUpdates();
  autoUpdater.logger = logger;
  autoUpdater.disableWebInstaller = false;
  // 这个写成 false,写成 true 时,可能会报没权限更新
  autoUpdater.autoDownload = false;
  autoUpdater.on("error", (error) => {
    logger.error(["检查更新失败", error]);
  });
  // 当有可用更新的时候触发。 更新将自动下载。
  autoUpdater.on("update-available", (info) => {
    logger.info("检查到有更新,开始下载新版本");
    const { version } = info;
    logger.info(`最新版本为: ${version}`);
    // 这里做的是检测到更新,直接就下载
    autoUpdater.downloadUpdate();
  });
  // 当没有可用更新的时候触发,其实就是啥也不用做
  autoUpdater.on("update-not-available", () => {
    logger.info("没有可用更新");
  });
  // 下载更新包的进度,可以用于显示下载进度与前端交互等
  autoUpdater.on("download-progress", async (progress) => {
    logger.info(progress);
  });
  // 在更新下载完成的时候触发。
  autoUpdater.on("update-downloaded", (res) => {
    logger.info("下载完毕, 提示安装更新", res);
    // 这里需要注意,Electron.dialog 想要使用,必须在 BrowserWindow 创建之后
    dialog
      .showMessageBox({
        title: "更新应用",
        message: "已为您下载最新应用,点击确定马上替换为最新版本!",
      })
      .then(() => {
        logger.info("退出应用,安装开始!");
        // 重启应用并在下载后安装更新,它只应在发出 update-downloaded 这个事件后方可被调用。
        autoUpdater.quitAndInstall();
      });
  });
}
  • 主线程逻辑
// src/main/main.ts
import { autoUpdateApp } from './app-update';
​
...
​
app
  .whenReady()
  .then(() => {
    // 创建窗口
    createWindow();
    // 检查更新
    autoUpdateApp();
    app.on('activate', () => {
      // On macOS it's common to re-create a window in the app when the
      // dock icon is clicked and there are no other windows open.
      if (mainWindow === null) createWindow();
    });
  })
  .catch(console.log);

从上面的代码来看,真的非常简单,封装好自动更新逻辑之后,只需要在窗口准备完成之后调用方法即可实现自动更新~

此部分的代码我更新到了仓库的 electron-react-auto-update_feature/easy-upgrade 分支,大家可以进行查看。别人的文章可能会给你讲为什么用第三方包 electron-updater 而不是原生的 Electron.autoUpdater,优势好处之类的,在本文里是不会讲的,因为两方面:

  1. 我个人不擅长 Electron,写这篇文章就是踩坑记录帮助别人
  2. 明明有 100 分的答案,为啥还要去告诉别人 60 分的答案?对比了你也不会去用的,意义不大。

前端自定义展示的自动更新

经过上面的步骤,相信大家都已经完成了示例应用的自动更新,那么接下来讨论的就是精益求精,让应用自动更新这个产品需求变得更完美。前面的更新步骤完全是通过 Electron 原生能力进行更新的,我个人觉得存在如下几个可以优化的地方:

  • 不同系统版本的更新配置比较繁琐
"build"{
    ...
    "publish": {
      "provider": "generic",
      "url": "http://localhost:8099/packages/win32/"
    }
}

上面是示例应用自动更新的核心配置项 publish,这里在不同系统内会有一个问题,我在 windows 打包设置当前这个地址,但是我如果在 linux 打包的时候,需要变更这个地址打包才可以,要不然应用其实检测不到对的版本对的安装包。

  • 更新是一个完全自主行为不可控

前面编写的代码检测到更新就直接给用户下载安装了,其实从 UX 的角度来说这一点是值得商榷的,应该更优雅的提示用户。

  • 更新效果粗糙,原生能力有限

对于应用自动更新来说,Electron 的原生效果体验并不是很好,这也不能怨它,因为它负责提供基础的更新能力,所有底层都开放出来了,开发者自行优化就可以了。那么既然是套壳的前端应用,无论是 React.js 还是 Vue.js 应该都能从前端的角度将整个更新过程美化,让用户得到更好的体验。

上面是针对当前版本的示例应用我感知到的三个问题,接下来就一一解答并附上核心代码。

细化配置,实现不同系统自主更新

前面说了,不同系统之间更新配置需要频繁切换 publish.url 地址然后再重新打包才可以,那么有没有办法一次性配置好,根据系统版本进行打包呢?答案是有的,publish 字段表示的就是如何发布自动更新,如果这个字段在最外层,那么就只能有一种写法,但是其实这个字段还可以配置到系统打包参数里,比如下面这样:

 "build": {
    ...
    "win": {
      "target": [
        "nsis"
      ],
      "publish": {
        "provider": "generic",
        "url": "http://localhost:8099/packages/win32/"
      }
    },
    "linux": {
      "target": [
        "AppImage"
      ],
      "category": "Development",
      "publish": {
        "provider": "generic",
        "url": "http://localhost:8099/packages/linux/"
      }
    },
    ...
    "publish": {
      "provider": "generic",
      "url": "http://localhost:8099/packages/linux/"
    }
  },

上面配置完成之后,在不同系统的打包参数里我们都指定了 publish 的详细更新参数,这里的优先级是高于外层的,这样就避免了不同系统需要修改参数再打包的窘境了~

比如将外层的 publish 参数改成 /linux 路径的更新地址,然后在 Windows 系统内进行打包,打包过后会发现更新地址正确的替换成了 /win32,这里如果想要查看具体地址的话,文件位置在 /release/build/win-unpacked/resources/app-update.yml

简单且详细地实现 Electron 自动更新(Auto Update)

前面已经说过了,已经验证完成了 Windows 系统和 Linux 系统的兼容性,后续就不一一介绍,只介绍 Windows 下的各类情况,Linux 部分笔者也全部测试过,是没问题的。

更新自主可控

前面的效果大家应该已经看到了,应用启动检测发现新版本就直接提示用户更新,点完就更新完了,完全没给用户机会,万一用户不想更新怎么办,这里其实可以在更新代码处增加用户交互限制,比如提供跳过此版本更新,取消更新的服务,比如下面这样:

// src/main/main.ts
import {
app,
dialog,
BrowserWindow,
MessageBoxOptions,
MessageBoxReturnValue,
} from "electron";
import { join } from "path";
import { autoUpdater } from "electron-updater";
import logger from "electron-log";
import Store from "electron-store";
​
const store = new Store();
​
async function sleep(ms: number) {
return new Promise((resolve) => {
const timer = setTimeout(() => {
resolve(true);
clearTimeout(timer);
}, ms);
});
}
​
/**
* 用户确定是否下载更新
*/
export function downloadUpdate() {
autoUpdater.downloadUpdate();
}
​
/**
* 自动更新的逻辑
* @param mainWindow
*/
export async function autoUpdateApp(mainWindow: BrowserWindow) {
// 等待 3 秒再检查更新,确保窗口准备完成,用户进入系统
await sleep(3000);
// 每次启动自动更新检查更新版本
autoUpdater.checkForUpdates();
autoUpdater.logger = logger;
autoUpdater.disableWebInstaller = false;
// 这个写成 false,写成 true 时,可能会报没权限更新
autoUpdater.autoDownload = false;
autoUpdater.on("error", (error) => {
logger.error(["检查更新失败", error]);
});
// 当有可用更新的时候触发。 更新将自动下载。
autoUpdater.on("update-available", (info) => {
logger.info("检查到有更新,开始下载新版本");
logger.info(info);
downloadUpdate();
});
// 当没有可用更新的时候触发,其实就是啥也不用做
autoUpdater.on("update-not-available", () => {
logger.info("没有可用更新");
});
// 下载更新包的进度,可以用于显示下载进度与前端交互等
autoUpdater.on("download-progress", async (progress) => {
logger.info(progress);
});
// 在更新下载完成的时候触发。
autoUpdater.on("update-downloaded", (info) => {
// 检查是否用户已经跳过了当前版本
const skippedVersion = store.get("skippedVersion");
logger.info("下载完毕!提示安装更新");
logger.info(info);
// 如果当前下载的版本就是设置的跳过的版本,那么就不提示用户安装
if (info.version === skippedVersion) return;
// 定义 Dialog 参数
const dialogOpts: MessageBoxOptions = {
type: "info",
buttons: ["取消", "跳过版本", "更新"],
title: "升级提示",
message: "已为您下载最新应用!",
detail:
"点击“更新”马上替换为最新版本,点击“跳过版本”不再接收当前版本更新。",
};
// dialog 想要使用,必须在 BrowserWindow 创建之后
dialog
.showMessageBox(dialogOpts)
.then((returnVal: MessageBoxReturnValue) => {
const { response } = returnVal;
if (response === 2) {
logger.info("退出应用,安装开始!");
// 安装的时候如果设置过 skkipVersion, 需要清除掉
store.delete("skippedVersion");
// 走默认的自动更新逻辑
autoUpdater.quitAndInstall();
} else if (response === 1) {
// 如果用户选择跳过版本,我们储存这个版本号到 electron-store
store.set("skippedVersion", info.version);
} else {
logger.info("用户点击了取消,本次不进行升级");
}
});
});
}

上面代码我写了丰富的注释,就不过多进行讲解了,详细的代码放到了仓库feature/control-update 分支,感兴趣的自行查看,实际的效果如下面所示:

  • 本次不更新,但是下一次打开应用依然会提示更新

简单且详细地实现 Electron 自动更新(Auto Update)

  • 跳过本次更新,直到下次有更高的版本之后再提示

简单且详细地实现 Electron 自动更新(Auto Update)

前端精致化显示更新进度

前面两步基本上完成了一整套更新流程,但是更新效果比较粗糙,因为用的是 Electron 原生提供的交互能力,在实现的过程中让我怀念起了大学期间写 C# 的时光,不过既然是一个套壳前端应用,作为一个纯正的前端,肯定是希望能把页面做的尽量酷炫的,饱暖思淫欲,在基本需求满足之后还是希望效果能变得更加精致,用户体验更好,比如下面的交互:

简单且详细地实现 Electron 自动更新(Auto Update)

其实体验可能也没有好那么多,因为只是一个简易版前端 Demo,但是虽然简单,所有的细节都是由前端来操控实现的,我这边就抛砖引玉,有兴趣的读者可以自己去实现一个炫酷炸天的更新页面。接下来主要给大家讲解一下核心的代码细节:

  • 自动更新的逻辑代码
// 这里主要负责获取更新的细节,将信息回传渲染线程,不做任何逻辑处理
// src/main/autoUpdater.js
import { app, BrowserWindow } from 'electron';
import { autoUpdater } from 'electron-updater';
​
/**
* 用户确定是否下载更新
*/
export function downloadUpdate() {
autoUpdater.downloadUpdate();
}
​
/**
* 退出并安装更新
*/
export function installUpdate() {
autoUpdater.quitAndInstall();
}
​
/**
* 自动更新的逻辑
* @param mainWindow
*/
export async function autoUpdateApp(mainWindow: BrowserWindow) {
// 等待 3 秒再检查更新,确保窗口准备完成,用户进入系统
await sleep(3000);
// 每次启动自动更新检查更新版本
autoUpdater.checkForUpdates();
autoUpdater.logger = logger;
autoUpdater.disableWebInstaller = false;
// 这个写成 false,写成 true 时,可能会报没权限更新
autoUpdater.autoDownload = false;
autoUpdater.on('error', (error) => {
logger.error(['检查更新失败', error]);
});
// 当有可用更新的时候触发。 更新将自动下载。
autoUpdater.on('update-available', (info) => {
logger.info('检查到有更新');
logger.info(info);
// 检查到可用更新,交由用户提示是否下载
mainWindow.webContents.send('update-available', info);
});
// 下载更新包的进度,可以用于显示下载进度与前端交互等
autoUpdater.on('download-progress', async (progress) => {
logger.info(progress);
// 计算下载百分比
const downloadPercent = Math.round(progress.percent * 100) / 100;
// 实时同步下载进度到渲染进程,以便于渲染进程显示下载进度
mainWindow.webContents.send('download-progress', downloadPercent);
});
// 在更新下载完成的时候触发。
autoUpdater.on('update-downloaded', (res) => {
logger.info('下载完毕!提示安装更新');
logger.info(res);
// 下载完成之后,弹出对话框提示用户是否立即安装更新
mainWindow.webContents.send('update-downloaded', res);
});
}

这里大部分的代码和之前的更新逻辑都是一样的,核心关注的就是之前检测到更新,下载更新以及安装更新都是原生进行的,而现在通过 mainWindow.webContents.send()方法发送给前端,由前端来控制交互。

  • 主线程
// 主线程用于接收渲染线程的消息,是否下载,是否更新
import { autoUpdateApp, installUpdate, downloadUpdate } from './app-update';
​
// 下载更新
ipcMain.on('download-update', async () => {
downloadUpdate();
});
​
// 安装更新
ipcMain.on('install-update', async () => {
installUpdate();
});
​
app
.whenReady()
.then(() => {
// 创建窗口
createWindow();
// 检查更新
autoUpdateApp(mainWindow as BrowserWindow);
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow();
});
})
.catch(console.log);

主线程逻辑和之前不同的地方在于除了检查更新之外,还需要接收渲染线程的消息,用来处理下载更新以及安装更新的操作。

  • 渲染线程

// 渲染线程,也就是前端,进行逻辑交互
import { useEffect, useState } from 'react';
import { Button, Progress } from 'antd';
import styled from 'styled-components';
​
const Container = styled.div`
position: relative;
height: 100%;
width: 100%;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
​
h1 {
margin-top: 20px;
font-size: 40px;
color: #333;
letter-spacing: 10px;
}
`;
​
export default function Upgrade() {
const [progress, setProgress] = useState<number>(0);
useEffect(() => {
// 跳转到此页面,立刻开始下载
window.electron.ipcRenderer.sendMessage('download-update');
// 同时同步监听下载进度
window.electron.ipcRenderer.on('download-progress', (prog: any) => {
setProgress(prog);
});
// 监听下载完成
window.electron.ipcRenderer.on('update-downloaded', (info) => {
// 提示用户安装更新
console.log('update-downloaded: ', info);
// 下载完成设置 progress 为 100
setProgress(100);
});
}, []);
// 安装更新
const installupdate = () => {
window.electron.ipcRenderer.sendMessage('install-update');
};
return (
<Container>
<Progress type="circle" percent={progress} size={320} />
{progress >= 100 ? (
<h1
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
应用下载完成
<Button
onClick={installupdate}
type="primary"
style={{ marginTop: 20 }}
size="large"
>
退出并安装更新
</Button>
</h1>
) : (
<h1>正在下载更新...</h1>
)}
</Container>
);
}

渲染线程就是前端实现控制更新提示、下载更新、显示更新进度以及安装更新,完全的前端代码,相信各位前端大佬都能明白。

这部分的代码已经合并到了electron-react-auto-update_feature/fe-optimize 分支,感兴趣的朋友可以自行查看。

总结

至此,Electron 应用的自动更新逻辑就讲解完成了,还是那句话,笔者对于这块知识点也是初学探索中,因此有讲解不到的地方大家体谅,有不懂的可以留言交流,目的就是标题 — 希望通过详细的源码以及示例帮助开发者简单的实现 Electron 应用自动推送更新的功能~

原文链接:https://juejin.cn/post/7320152980211499060 作者:前端周公子

(2)
上一篇 2024年1月6日 上午11:09
下一篇 2024年1月6日 下午4:01

相关推荐

发表回复

登录后才能评论