Skip to content

增量更新

插件版本 v4.0.0,需 electron-egg v4

介绍

增量更新优点

  • 1.绕过 macOS 权限,无需购买开发者账号(600 元/每年)。

  • 2.如果上架其它平台应用商店 ,理论上也可以绕过官方的升级方式。

  • 3.减小更新包大小,约 4 ~ 10 倍,减少大量 cdn 资源费用。

  • 4.用户重启即可完成更新,不需要走安装流程。

  • 5.支持更多平台 win32 位、win64 位、windows7

  • 6.支持额外资源目录 extraResources 更新

  • 7.不限制应用个数

使用步骤

安装

bash

# 安装插件
npm i ee-incremental-updater@4.0.0
# 升级 ee-bin
npm i ee-bin@4.1.9 -D
# 升级 ee-core
npm i ee-core@4.1.4

移动文件 仅仅 windows 平台需要

bash

1. 把文件 '项目/node_modules/ee-incremental-updater/updater.exe' '...../updater32.exe'
复制或移动到 '项目/build/extraResources/updater.exe', '项目/build/extraResources/updater32.exe'

添加生成增量资源的命令

注意

  1. 注意:asarFile 是 app.asar 的路径,不同平台不同架构,这个路径可能不一样,根据实际填写
  • 修改 bin.js 配置(项目左边箭头)
js
// 项目 /cmd/bin.js

module.exports = {
  // 添加一个命令对象
  /**
   * 增量更新命令
   * ee-bin updater --platform=
   */
  updater: {
    windows_32: {
      asarFile: "./out/win-ia32-unpacked/resources/app.asar",
      extraResources: [
        "./build/extraResources/goapp.exe",
        "./build/extraResources/read.txt",
        "./build/extraResources/hello/**/*",
      ],
      output: {
        directory: "./out",
        file: "incremental-latest.json",
        zip: "app.zip",
      },
      cleanCache: false,
    },
    windows_64: {
      asarFile: "./out/win-unpacked/resources/app.asar",
      extraResources: [
        "./build/extraResources/goapp.exe",
        "./build/extraResources/read.txt",
        "./build/extraResources/hello/**/*",
      ],
      output: {
        directory: "./out",
        file: "incremental-latest.json",
        zip: "app.zip",
      },
      cleanCache: false,
    },
    macos_intel: {
      asarFile: "./out/mac/项目名.app/Contents/Resources/app.asar",
      extraResources: [
        "./build/extraResources/goapp",
        "./build/extraResources/read.txt",
        "./build/extraResources/hello/**/*",
      ],
      output: {
        directory: "./out",
        file: "incremental-latest.json",
        zip: "app.zip",
      },
      cleanCache: false,
    },
    macos_apple: {
      asarFile: "./out/mac-arm64/项目名.app/Contents/Resources/app.asar",
      extraResources: [
        "./build/extraResources/goapp.exe",
        "./build/extraResources/read.txt",
        "./build/extraResources/hello/**/*",
      ],
      output: {
        directory: "./out",
        file: "incremental-latest.json",
        zip: "app.zip",
      },
      cleanCache: false,
    },
    linux: {
      asarFile: "./out/linux-unpacked/resources/app.asar",
      extraResources: [
        "./build/extraResources/goapp",
        "./build/extraResources/read.txt",
        "./build/extraResources/hello/**/*",
      ],
      output: {
        directory: "./out",
        file: "incremental-latest.json",
        zip: "app.zip",
      },
      cleanCache: false,
    },
  },
};

参数说明

  • asarFile 原始文件,(框架构建后生成的,用来生成增量包)

  • extraResources 资源目录文件,指定更新的文件

  • 额外资源目录说明

bash

extraResources: [
  './build/extraResources/hello/**/*'
],
举例:
"./build/extraResources/hello/a.txt"            - 精确匹配 a.txt 文件
"./build/extraResources/hello/*.txt"            - 匹配 hello 目录(不包括子目录)中,所有 .txt 结尾的文件
"./build/extraResources/hello/**/*.csv"         - 匹配 hello 目录(包括子目录)中,所有 .csv 结尾的文件
"./build/extraResources/hello/**/*"             - 匹配 hello 目录(包括子目录)及下面的所有文件
更多语法:
星号(*)        — 匹配除斜杠(路径分隔符)和隐藏文件(名称以 . 开头)之外的所有内容。
双星(**)       - 匹配零个或多个目录。
问号(?)        - 匹配除斜杠(路径分隔符)以外的任何单个字符。
方括号([seq])  - 匹配方括号序列中的任何字符。
  • output 增量更新输出的资源

  • output.directory 输出的目录

  • output.file 升级信息文件,见底部说明

  • output.zip 增量包,见底部说明

  • cleanCache 清理缓存

命令使用

注意

注:在对应操作系统上生成增量文件,相比 v1.x 版本,增加了 platform 参数

  1. platform 指定平台,值:windows_32、windows_64、macos_intel、macos_apple、linux

  2. 在执行 npm run build-w 时,会生成全量资源和增量资源。

bash

# 编辑 package.json 中 scripts 属性
# 追加 && ee-bin updater 命令
# 如:原始的 build-w
"build-w": "ee-bin build --cmds=win64",
# 现在
"build-w": "ee-bin build --cmds=win64 && ee-bin updater --platform=windows_64",

# macOS 同理

把增量资源放到你的 cdn 上面

bash

# 示例
# 打开 项目/out 目录:
./out/incremental-latest-xxx.json     // 升级信息文件,如希望只升级某个平台,修改它的版本号即可
./out/app-xxx-1.0.0.zip               // 增量包

latest.yml 文件

# 放到 CDN 目录:域名换成你自己的
http://kodo.qiniu.com/electron-egg/

# 放置后如下 域名换成你自己的
 http://kodo.qiniu.com/electron-egg/incremental-latest-xxx.json

代码示例

业务层

./electron/service/updater.js (填写其中的 config)

js
"use strict";
const { app: electronApp } = require("electron");
const { logger } = require("ee-core/log");
const { getMainWindow, setCloseAndQuit } = require("ee-core/electron");
const { is } = require("ee-core/utils");
const { isPackaged } = require("ee-core/ps");
const { sleep } = require("ee-core/utils/helper");
const IncrUpdaterPlugin = require("ee-incremental-updater");

/**
 * Updater(service层为单例)
 * @class
 */
class UpdaterService {
  load() {
    const status = {
      error: -1,
      available: 1,
      noAvailable: 2,
      downloading: 3,
      downloaded: 4,
    };
    const config = {
      // CDN 目录url 换成你自己的
      url: "https://www.xxxxxx.com/upload/electronegg/",
      // 如果有特殊平台,可以指定一个固定的 json 文件
      // urlFile: 'incremental-latest-xxx.json',
      // 密钥 换成你自己的
      secret: "T_4F9pA7bYZ6rX0",
      // debug: false,
    };
    // 设置配置
    IncrUpdaterPlugin.setConfig(config);
    // 监听可用更新事件
    IncrUpdaterPlugin.on("update-available", (info) => {
      const content = {
        status: status.available,
        version: info.version,
      };
      this._sendToWindow(content);
    });
    // 监听不可用更新事件
    IncrUpdaterPlugin.on("update-not-available", () => {
      const content = {
        status: status.noAvailable,
      };
      this._sendToWindow(content);
    });
    // 监听下载进度事件
    // state 包含以下属性:
    // {
    //   percent: 0.6121836146128672,
    //   size: { total: 9359669, transferred: 5729836 }
    // }
    IncrUpdaterPlugin.on("download-progress", (state) => {
      const content = {
        status: status.downloading,
        percent: String(Math.round(state.percent * 100)),
        totalSize: IncrUpdaterPlugin.bytesChange(state.size.total),
        transferredSize: IncrUpdaterPlugin.bytesChange(state.size.transferred),
      };

      this._sendToWindow(content);
    });
    // 监听下载完成事件
    IncrUpdaterPlugin.on("update-downloaded", () => {
      const content = {
        status: status.downloaded,
        desc: "下载完成",
      };
      this._sendToWindow(content);

      // 托盘插件默认会阻止窗口关闭,这里设置允许关闭窗口
      setCloseAndQuit(true);
    });
    // 监听下载错误事件
    IncrUpdaterPlugin.on("error", (error) => {
      logger.error(error.msg);
      const content = {
        status: status.error,
        error: "更新失败",
      };
      this._sendToWindow(content);
    });
  }

  /**
   * 检查更新
   */
  checkUpdate() {
    IncrUpdaterPlugin.checkAvailable({ timeout: 4000 });
  }

  async download() {
    IncrUpdaterPlugin.download();
  }

  async relaunchApp() {
    // 打包安装后才调用
    if (!isPackaged()) return;

    // 安装并重启
    // 等待1秒,让日志打印完毕,因为经过测试上面代码大概在100ms内执行完毕,为了保险起见,等待1秒,让日志打印完
    IncrUpdaterPlugin.installApp();
    await sleep(1000);
    if (is.macOS()) {
      electronApp.relaunch();
      electronApp.quit();
    } else {
      electronApp.quit();
    }
  }

  /**
   * 本地版本
   */
  currentVersion() {
    const v = electronApp.getVersion();
    return v;
  }

  /**
   * 向窗口发消息
   */
  _sendToWindow(content = {}) {
    const textJson = JSON.stringify(content);
    const channel = "custom/app/updater";
    const win = getMainWindow();
    win.webContents.send(channel, textJson);
  }
}
UpdaterService.toString = () => "[class UpdaterService]";

module.exports = {
  UpdaterService,
  updaterService: new UpdaterService(),
};

控制器

  • 控制器层

./electron/controller/updater.js

js
"use strict";
const { updaterService } = require("../service/updater");

class UpdaterController {
  /**
   * 获取应用基础信息
   */
  appInfo() {
    const appInfo = {
      currentVersion: "",
    };
    appInfo.currentVersion = updaterService.currentVersion();
    return appInfo;
  }

  /**
   * 检查是否有新版本
   */
  checkForUpdater() {
    updaterService.checkUpdate();
    return;
  }

  /**
   * 下载新版本
   */
  downloadApp() {
    updaterService.download();
    return;
  }

  /**
   * 安装新版本
   */
  relaunchApp() {
    updaterService.relaunchApp();
    return;
  }
}
UpdaterController.toString = () => "[class UpdaterController]";

module.exports = UpdaterController;

预加载层

  • ./electron/preload/index.js
js
/*************************************************
 ** preload为预加载模块,该文件将会在程序启动时加载 **
 *************************************************/

const { updaterService } = require("../service/updater");

function preload() {
  // updater
  updaterService.load();
}

/**
 * 预加载模块入口
 */
module.exports = {
  preload,
};

前端代码

vue
<template>
  <section id="hero">
    <h1 class="tagline">
      <span class="accent">Electron-Egg</span>
    </h1>
    <p class="description">A fast, desktop software development framework</p>
    <p class="actions">
      <a class="setup" href="https://www.kaka996.com/" target="_blank"
        >Get Started</a
      >
    </p>
  </section>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { ipcRoute, specialIpcRoute } from "@/api/index";
import { ipc } from "@/utils/ipcRenderer";

const appVersion = ref("");
const versionTips = ref("");
const available = ref(false);

onMounted(() => {
  init();
});

function init() {
  ipc.removeAllListeners(specialIpcRoute.appUpdater);
  ipc.on(specialIpcRoute.appUpdater, (event, result) => {
    result = JSON.parse(result);
    console.log(result);
    const { version, status, percent, error } = result;
    console.log("啊哈哈哈哈");
    console.log(status);
    if (status == 1) {
      available.value = true;
      versionTips.value = "有可用更新 v" + version;
      // 这里 最好用 element-plus 的 message 提示 我就直接下载了
      download();
    } else if (status == 2) {
      versionTips.value = "已经是最新版本";
      // 最好来个提示之类的
    } else if (status == 3) {
      versionTips.value = "已下载 " + percent + "%";
      // 最好用element-plus 的 进度条组件
    } else if (status == 4) {
      versionTips.value = "下载完成";
      window.alert("下载完成,重新启动");
      confirmOk();
    } else {
      // 最好用 element-plus 的 message 提示
      window.alert(error);
    }
  });
  ipc.invoke(ipcRoute.getAppInfo).then((result) => {
    console.log(result);
    const { currentVersion } = result;
    appVersion.value = currentVersion;
  });
  // 检查版本
  checkForUpdater();
}

function checkForUpdater() {
  ipc.invoke(ipcRoute.checkForUpdater, {});
}

function download() {
  ipc.invoke(ipcRoute.downloadApp, {});
}

function confirmOk() {
  ipc.invoke(ipcRoute.relaunchApp);
}
</script>
<style scoped>
.updatebutton {
  width: 100px;
  height: 40px;
  background-color: #42d392;
  color: #fff;
  text-align: center;
  line-height: 40px;
  cursor: pointer;
  margin: 0 auto;
}

section {
  padding: 42px 32px;
}

#hero {
  padding: 150px 32px;
  text-align: center;
  height: 100%;
}

.tagline {
  font-size: 52px;
  line-height: 1.25;
  font-weight: bold;
  letter-spacing: -1.5px;
  max-width: 960px;
  margin: 0px auto;
}
html:not(.dark) .accent,
.dark .tagline {
  background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.description {
  max-width: 960px;
  line-height: 1.5;
  color: var(--vt-c-text-2);
  transition: color 0.5s;
  font-size: 22px;
  margin: 24px auto 40px;
}
.actions a {
  font-size: 16px;
  display: inline-block;
  background-color: var(--vt-c-bg-mute);
  padding: 8px 18px;
  font-weight: 500;
  border-radius: 8px;
  transition: background-color 0.5s, color 0.5s;
  text-decoration: none;
}
.actions .setup {
  color: var(--vt-c-text-code);
  background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
}
.actions .setup:hover {
  background-color: var(--vt-c-gray-light-4);
  transition-duration: 0.2s;
}
</style>

前端路由

frontend/src/api/index.js

js
/**
 * 主进程与渲染进程通信频道定义
 * Definition of communication channels between main process and rendering process
 */
const ipcApiRoute = {
  test: "controller/example/test",
};

// 增量更新文档

const ipcRoute = {
  // updater
  getAppInfo: "controller/updater/appInfo",
  checkForUpdater: "controller/updater/checkForUpdater",
  downloadApp: "controller/updater/downloadApp",
  relaunchApp: "controller/updater/relaunchApp",
};

/**
 * 自定义频道
 * 格式:自定义(推荐添加一个前缀)
 * custom chennel
 */
const specialIpcRoute = {
  appUpdater: "custom/app/updater", // updater channel
};

export { ipcApiRoute, ipcRoute, specialIpcRoute };

其他说明

  • 升级文件信息

window 平台:

  • ./out/incremental-latest-windows_32.json

  • ./out/incremental-latest-windows_64.json

macOS inter 芯片:

  • ./out/incremental-latest-macos-intel.json

macOS 苹果芯片:

  • ./out/incremental-latest-macos-apple.json

linux 平台:

  • ./out/incremental-latest-linux.json

内容举例

json
{
  // 版本
  "version": "1.0.0",
  // 要下载的文件名
  "file": "app-macos-intel-1.0.0.zip",
  // 大小
  "size": 24824079,
  // 验证
  "sha1": "3125553050eb63c9051088570701f876ab0b72b3",
  // 发布日期
  "releaseDate": "2024-09-11"
}
  • 增量包

windows 平台:

  • ./out/app-windows-32-1.2.0.zip

  • ./out/app-windows-64-1.2.0.zip

macOS inter 芯片

  • ./out/app-macos-intel-1.1.0.zip

macOS 苹果芯片

  • ./out/app-macos-apple-1.1.0.zip

linux 平台:

  • ./out/app-linux-1.1.0.zip

指定平台

使用 git 新建一个或多个平台的分支 windonws 、macos_inter 或 macos_apple;

注意

  1. Tips:多分支是跨平台开发的最简单有效的方式。它可以避免因配置、资源不同而导致的需要编写复杂的脚本。

  2. 理论上 windows 64 位 构建的资源可以在 win32、win64 平台运行;macOS intel 芯片构建的资源可以在 macos_intel、macos_apple 平台运行。

问题排查

  • 检查 CDN 上的资源,能否在浏览器中直接下载

  • 检查 CDN 上的资源,json 文件里面的文件名是否和 zip 文件一致

  • 下载无响应

bash

方案一:在 setCoinfig 配置里,打开 debug,查看日志

方案二:在 setCoinfig 配置里,把 url 的协议 改成 http:// https://

方案三:在 ./electron/index.js ready() 函数添加
async ready () {
  // 关闭同源策略
  electronApp.commandLine.appendSwitch("disable-web-security");
  // 不要尝试解析代理服务器
  electronApp.commandLine.appendSwitch("auto-detect", "false");
  electronApp.commandLine.appendSwitch("no-proxy-server");
}
  • npm run start (非安装环境)下,仅到下载步骤,无安装替换效果,因为这时候软件未安装。

  • 升级插件后,客户端需要迭代两个版本才能看到插件新功能效果。因为用户已经安装的是旧版本,迭代一次后更新了插件,再一次迭代才能看到 上一次的效果