electron 打包与远程在线升级降级

1,171 阅读5分钟

一.配置介绍

1.使用 (electron-builder)[www.electron.build/]和(electron…]

安装 npm i electron-builder --dev or yarn add electron-builder --dev
安装 npm i electron-updater --save or yarn add electron-updater --save
然后在 *package.json*中增加配置
(自动更新配置 Publish)[https://www.electron.build/configuration/publish]
"build": {
    "appId": "",
    "productName": "my-app", <!-- 包名 -->
    "copyright": "Copyright © 2022 hfitech",
    "win": {
      "icon": "./public/icons/icon.ico",
      "target": [
        {
          "target": "nsis"
        }
      ]
    },
    "nsis": {
      "oneClick": false, <!-- 是否一键安装 -->
      "allowToChangeInstallationDirectory": true, <!-- 是否可以选择安装目录 -->
      "installerIcon": "./public/icons/icon.ico",
      "uninstallerIcon": "./public/icons/icon.ico",
      "installerHeaderIcon": "./public/icons/icon.ico"
    },
    "files": [ <!-- 打包文件来源 -->
      "dist/**/*",
      "electron/**/*"
    ],
    "directories": { <!-- 打包输出配置 -->
      "buildResources": "assets",
      "output": "release-build"
    },
    "publish": [ <!-- 自动更新配置 具体可以参考(Publish)[https://www.electron.build/configuration/publish] -->
      {
        "provider": "generic", <!-- 通用服务器配置 -->
        "url": "http://127.0.0.1:5500/" <!-- 服务器地址 注意不要精确到目录不然智能version增量更新不能降级更新-->
      }
    ]
  }

项目结构

font-termibal-front
├─ .env
├─ .env.dev
├─ .env.development
├─ .env.production
├─ .env.test
├─ .eslintrc.js
├─ .gitignore
├─ .prettierignore
├─ .prettierrc.json
├─ dev-app-update.yml <!-- 为了在不打包应用程序的情况下开发/测试更新的 UI/UX -->
├─ electron
│  ├─ main.js
│  ├─ preload.js
│  ├─ update.js
│  └─ utils.js
├─ i18next-scaner.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ public
│  ├─ icon.png
│  └─ icons
│     ├─ 1024x1024.png
│     ├─ 128x128.png
│     ├─ 16x16.png
│     ├─ 24x24.png
│     ├─ 256x256.png
│     ├─ 32x32.png
│     ├─ 48x48.png
│     ├─ 512x512.png
│     ├─ 64x64.png
│     ├─ endPoint.svg
│     ├─ icon.icns
│     ├─ icon.ico
│     └─ startPoint.svg
├─ README.md
├─ src
│  ├─ api
│  │  └─ system.js
│  ├─ App.jsx
│  ├─ assets
│  ├─ components
│  ├─ config
│  │  ├─ index.js
│  ├─ i18n
│  │  ├─ en.json
│  │  └─ zh.json
│  ├─ i18n.js
│  ├─ main.jsx
│  ├─ pages
│  │  ├─ 404.jsx
│  ├─ routers
│  │  ├─ index.jsx
│  │  └─ routes.js
│  ├─ store
│  │  └─ index.jsx
│  └─ utils
│     ├─ request.js
├─ vite.config.js
└─ yarn.lock

electron 主要配置

main.js
/* eslint-disable no-undef */
// electron/main.js
const { app, BrowserWindow } = require("electron");
const { checkUpdate, handleUpdate } = require("./update");
const path = require("path");
const NODE_ENV = process.env.NODE_ENV;
let mainWindow = null;
function createWindow() {
  // 创建浏览器窗口
  mainWindow = new BrowserWindow({
    width: 1920,
    height: 1080,
    autoHideMenuBar: true, // 自动隐藏菜单栏
    frame: true, // 边框窗口
    webPreferences: {
      preload: path.join(__dirname, "preload.js"), // 隔离vite和Electron之间的状态
      nodeIntegration: false, // 使用页面中可以引入node和electron相关的API
      contextIsolation: true, // 是否在独立 JavaScript 环境中运行 Electron API和指定的preload 脚本
    },
    icon: path.join(__dirname, "../public/icons/icon.ico"),
  });
  // 主窗体要加载的url
  mainWindow.loadURL(
    NODE_ENV === "development"
      ? "http://localhost:4000"
      : `file://${path.join(__dirname, "../dist/index.html")}`
  );
  // 开发环境打开开发工具
  // if (NODE_ENV === "development") {
  //   mainWindow.webContents.openDevTools();
  // }
}

app.whenReady().then(() => {
  createWindow();
  /* 升级检测 */
  checkUpdate();
  handleUpdate();
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

upload.js
/* eslint-disable no-undef */
const { autoUpdater } = require("electron-updater");
const { ipcMain, powerMonitor, BrowserWindow } = require("electron");
const log = require("electron-log");
const { timeInRange } = require("./utils");
// 打印调试
log.transports.console.format = "{h}:{i}:{s} {text}";
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = "info";
autoUpdater.logger.transports.file.maxSize = 0;
const returnData = {
  error: { status: -2, msg: "检测更新查询异常" },
  updateNotAva: { status: -1, msg: "没有可更新版本" },
  checking: { status: 0, msg: "正在检查应用程序更新" },
  updateAva: { status: 1, msg: "检测到新版本,正在下载,请稍后" },
  updateIng: { status: 2, msg: "下载中 . . ." },
  updateFinish: { status: 3, msg: "下载完成,正在重启更新" },
};
/* ***注意空闲时间必须大于设定的空闲时间idleTime */
const isCanUpdate = (data) => {
  let flag = false;
  /* 当前应用版本号 */
  let currentVersion = autoUpdater.currentVersion.version;
  /* 获取空闲时间 */
  let idleTime = powerMonitor.getSystemIdleTime(); /* 单位秒 */
  /* 判断是否在更新时间段时分秒内 && 版本不一致 && 空闲时间大于 xx 秒 */
  if (
    timeInRange(data.range, data.time) &&
    currentVersion !== data.version &&
    idleTime > data.idleTime
  ) {
    flag = true;
  }
  return flag;
};
const update = (win, data) => {
  const contents = win.webContents;
  /* 当前应用版本号 */
  let currentVersion = autoUpdater.currentVersion.version;
  /* 获取空闲时间 */
  let idleTime = powerMonitor.getSystemIdleTime(); /* 单位秒 */
  /* 判断是否在更新时间段时分秒内 && 版本不一致 && 空闲时间大于 xx 秒 */
  if (
    !(
      timeInRange(data.range, data.time) &&
      currentVersion !== data.version &&
      idleTime > data.idleTime
    )
  ) {
    return false;
  }
  autoUpdater.checkForUpdates(); <!-- 检查更新 -->
  autoUpdater.autoDownload = false; <!-- 不允许自动下载 -->
  autoUpdater.allowDowngrade = true; <!-- 允许降级 -->
  autoUpdater.setFeedURL(data.exeUrl + data.version); <!-- 手动设置更新地址 -->
  //更新错误
  autoUpdater.on("error", function () {
    sendUpdateMessage(returnData.error);
    log.error("检测更新查询异常");
  });
  // 检测是否有新版本
  autoUpdater.on("checking-for-update", (res) => {
    sendUpdateMessage(returnData.checking);
    log.info("获取版本信息:" + JSON.stringify(res));
  });
  autoUpdater.on("update-not-available", (res) => {
    sendUpdateMessage(returnData.updateNotAva);
    log.info("没有可更新版本:" + JSON.stringify(res));
  });
  autoUpdater.on("update-available", (res) => {
    sendUpdateMessage(returnData.updateAva);
    autoUpdater.downloadUpdate();
    log.info("当有可用更新时发出:" + JSON.stringify(res));
  });
  autoUpdater.on("download-progress", (res) => {
    sendUpdateMessage({ ...returnData.updateIng, ...res });
    log.info("下载监听:" + JSON.stringify(res));
  });
  autoUpdater.on("update-downloaded", (res) => {
    if (!contents.isDestroyed()) {
      sendUpdateMessage(returnData.updateFinish);
      log.info("下载完成:" + res);
    }
    /* 防止升级重启不成功 */
    setImmediate(() => {
      autoUpdater.quitAndInstall(true, true);
    });
  });
  /* 向渲染进程发送更新消息 */
  const sendUpdateMessage = (data) => {
    win.webContents.send("download-progress", data);
  };
};

const checkUpdate = () => {
  ipcMain.handle("check-update", (event, data) => {
    /* 日志大小 大于 10M 时清空日志 */
    if (Math.ceil(log.transports.file.getFile().size / 1024 / 1024) > 10) {
      log.transports.file.getFile().clear();
    }
    return isCanUpdate(data);
  });
};
const handleUpdate = () => {
  ipcMain.on("handle-update", (event, data) => {
    const webContents = event.sender;
    const win = BrowserWindow.fromWebContents(webContents);
    update(win, data);
  });
};
module.exports = {
  checkUpdate,
  handleUpdate,
};

preload.js
/* eslint-disable no-undef */
const {
  contextBridge,
  ipcRenderer,
} = require("electron"); /* 启用上下文隔离可以使用contextBridge 给window传递信息 */
contextBridge.exposeInMainWorld("electronApi", {
  downloadProgress: (callback) => {
    ipcRenderer.on("download-progress", callback);
  },
  handleCheckUpdate: (info) => ipcRenderer.invoke("check-update", info),
  handleUpdate: (info) => ipcRenderer.send("handle-update", info),
});
utils.js
/* eslint-disable no-undef */
const moment = require("moment");
const log = require("electron-log");
// 打印调试
log.transports.console.format = "{h}:{i}:{s} {text}";
function timeInRange(range, currentTime) {
  let rangeArr = range.split("-");
  let start = "";
  let end = "";
  let flag = false;
  try {
    if (!/\d{2}:\d{2}-\d{2}:\d{2}/.test(range)) {
      throw Error("时间格式传输不正确");
    }
    rangeArr.forEach((item, index) => {
      let hourMinute = item.split(":");
      time = moment().hour(hourMinute[0]).minutes(hourMinute[1]);
      index === 0 ? (start = time) : (end = time);
    });
    if (moment(start).isSameOrAfter(end)) {
      end = moment(end).add(1, "days");
    }
    if (moment(currentTime).isBetween(start, end, "minutes", "[)")) {
      flag = true;
    }
  } catch (error) {
    log.error(error);
  }
  return flag;
}
module.exports = {
  timeInRange,
};

打包命令

"scripts": {
    "dev": "vite", <!-- 本地开发 -->
    "build:dev": "vite build --mode dev", <!-- 打包dev环境web代码 -->
    "build:test": "vite build --mode test", <!-- 打包test环境web代码 -->
    "build": "vite build", <!-- 打包生产环境web代码 -->
    "preview": "vite preview",
    "electron": "wait-on tcp:4000 && cross-env NODE_ENV=development electron .",
    "electron:dev": "concurrently -k \"npm run dev\" \"npm run electron\"", <!-- 运行本地桌面端 dev环境 -->
    "electron:build": "npm run build && electron-builder", <!-- 打包生产终端包 -->
    "scan": "i18next-scanner --config i18next-scaner.config.js",
    "build-icon": "electron-icon-builder --input=./public/icon.png --output=public --flatten",
    "prepare": "husky install"
  },

二.打包

*注意修改 package.json 中的 version 字段打包的版本*
运行 npm run electron:build 将包声称在根目录/release-build/中
打包后的目录
注意:我们只需要latest.yml和my-app Setup 1.0.0.exe放置在服务器对应的目录中
release-build
├─ builder-debug.yml
├─ builder-effective-config.yaml
├─ latest.yml
├─ my-app Setup 1.0.0.exe
├─ my-app Setup 1.0.0.exe.blockmap

三.部署

本地启动服务器 http://127.0.0.1:5500/
新建版本号对应的文件夹1.0.0将打包好的latest.yml和my-app Setup 1.0.0.exe放置在文件夹中
多个版本多个文件夹

四.更新

async function checkUpdateMe() {
  let info = {
    version: "1.0.0", <!-- 想要更新的版本 -->
    exeUrl: "http://127.0.0.1:5500/", <!-- 更新服务器地址 -->
    range: "10:00-14:00", <!-- 更新时间段 -->
    time: "2022-07-21 13:37:00", <!-- 当前时间,假设这是服务器传过来的时间放置终端时间设置不正确 -->
    idleTime: 10, <!-- 发呆时间s(机器无人操作的时间) -->
  };
  const isCanUpdate = await window.electronApi.handleCheckUpdate(info);
  if (isCanUpdate) {
    navigate("/update");
    window.electronApi.handleUpdate(info);
  }
}
/* 测试自动升级 */
setInterval(() => {
  checkUpdateMe();
}, 1000); <!-- 每隔一秒检查是否需要更新 -->

难点一:主要之前只能自动升级不能降级后面查文档资料需要注意和设置

1.pakage.json 中 url 根目录不要直接放置 latest.yml 和 xxx.exe 升级文件,需要指定目录存放
"publish": [
  {
    "provider": "generic",
    "url": "http://127.0.0.1:5500/"
  }
]
2.设置允许降级和手动设置更新地址

update.js 中

autoUpdater.checkForUpdates();
autoUpdater.autoDownload = false;
autoUpdater.allowDowngrade = true;
autoUpdater.setFeedURL(data.exeUrl + data.version);
3.打包版本服务器中存放各版本号命名的文件夹,文件夹中存放对应版本的 xxx.exe 文件和 latest.yml 文件

难点二:远程升级不稳定

1.autoDownload需要设置为false
1. autoUpdater.quitAndInstall(true, true);需要放置在setImmediate中调用

补充

项目使用 vite-react + electron 搭建 项目中用到了日志服务 (electron-log)[github.com/megahertz/e…

运行日志查看地址
  • 在 Linux 上: ~/.config/{app name}/logs/{process type}.log
  • 在 macOS 上: ~/Library/Logs/{app name}/{process type}.log
  • 在 Windows 上: %USERPROFILE%\AppData\Roaming{app name}\logs{process type}.log

总结:

因为最近公司需要做一个桌面终端项目同时对远程控制升级有要求故网上查文档查大佬们的博客总结出来了基本方案,可能有些地方写的不合理、不完美。望大佬们指正!!!