Tiven

Tiven

博观而约取,厚积而薄发

天问的个人网站(天问博客),专注于Node.js、Vue.js、React、Vite、Npm、Nginx等大前端技术。不断学习新技术,记录日常开发问题,持续分享coding,极客开源,共同进步。生命不息,奋斗不止... [ hexo blog ]

electron-updater 自动更新升级应用


electron 内置了 autoUpdater 自动更新功能,但是服务配置有些复杂,最后选择了 electron-updater 工具插件,这里就讲讲如何配置 electron-updater 来自动更新升级应用。

electron-updater

一、项目依赖和 scripts

安装 electron-updaterelectron-log

pnpm add -D electron-updater electron-log

package.json 完整配置如下:

{
  "name": "post-tools",
  "productName": "Post Tools",
  "version": "3.0.0",
  "description": "一个基于electron和node开发,用于http/https接口测试的工具",
  "main": "./out/main/index.js",
  "author": "Tiven",
  "homepage": "https://tiven.cn",
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
    "start": "electron-vite preview",
    "dev": "electron-vite dev",
    "dev:debug": "nodemon --watch ./src/main/index.js --exec \" electron-vite dev\" ",
    "build": "electron-vite build",
    "postinstall": "electron-builder install-app-deps",
    "build:win": "npm run build && electron-builder --win --config",
    "build:mac": "npm run build && electron-builder --mac --config",
    "build:linux": "npm run build && electron-builder --linux --config",
    "git": "tive git -c tive.git.config.cjs"
  },
  "dependencies": {
    "@electron-toolkit/preload": "^2.0.0",
    "@electron-toolkit/utils": "^1.0.2"
  },
  "devDependencies": {
    "@ant-design/icons": "4.0.0",
    "@electron/notarize": "^1.2.3",
    "@vitejs/plugin-react": "^4.0.0",
    "about-window": "^1.15.2",
    "ahooks": "^3.7.7",
    "antd": "^5.6.2",
    "axios": "^1.4.0",
    "electron": "^24.4.1",
    "electron-builder": "^23.6.0",
    "electron-builder-squirrel-windows": "^24.5.0",
    "electron-log": "^4.4.8",
    "electron-updater": "^5.3.0",
    "electron-vite": "^1.0.23",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-react": "^7.32.2",
    "jsoneditor": "8",
    "prettier": "^2.8.8",
    "prop-types": "^15.8.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "vite": "^4.3.9"
  }
}

二、配置打包参数

  • electron-builder.yml
appId: cn.tiven.app
productName: Post Tools
copyright: Copyright © 2023 ${author}
directories:
  buildResources: build
  output: dist
files:
  - '!**/.vscode/*'
  - '!src/*'
  - '!dist2/*'
  - '!node_modules/*'
  - '!electron.vite.config.{js,ts,mjs,cjs}'
  - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
asarUnpack:
  - resources/**
afterSign: build/notarize.js
win:
  executableName: Post Tools
  icon: resources/icon.ico
  publisherName: tiven
  verifyUpdateCodeSignature: false
  target:
    - nsis
    - squirrel
nsis:
  oneClick: false
  artifactName: ${name}-${version}-setup.${ext}
  shortcutName: ${productName}
  uninstallDisplayName: ${productName}
  createDesktopShortcut: always
  perMachine: true
  allowToChangeInstallationDirectory: true
  guid: 2cf313e9-0f05-xxxx-1006-e278272e9b2a
squirrelWindows:
  loadingGif: resources/loading.gif
  iconUrl: https://tiven.cn/static/img/net-stats.ico
mac:
  category: public.app-category.developer-tools
  entitlementsInherit: build/entitlements.mac.plist
  extendInfo:
    - NSCameraUsageDescription: Application requests access to the device's camera.
    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
    - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
    - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
dmg:
  artifactName: ${name}-${version}.${ext}
linux:
  target:
    - AppImage
    - snap
    - deb
  maintainer: electronjs.org
  category: Utility
appImage:
  artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
  provider: generic
  url: http://localhost:3000/

其中最后三行的 public 参数配置是 electron-updater 更新的关键。 http://localhost:3000/ 本地调试用,下载速度很快,上线了可以换成正式域名服务。

  • dev-app-update.yml
provider: generic
url: http://localhost:3000/
updaterCacheDirName: post-tools-updater

三、主要更新逻辑

新建文件 src/main/autoUpdater.js

// src/main/autoUpdater.js

import { app, dialog } from 'electron'
import { join } from 'path'
import { autoUpdater } from 'electron-updater'
import logger from 'electron-log'
import { getLocalData, setLocalData, sleep } from './helper'
import { productName } from '@package'

export async function autoUpdateInit() {
  //打印log到本地
  logger.transports.file.maxSize = 1002430 // 10M
  logger.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}]{scope} {text}'
  logger.transports.file.resolvePath = () => join(app.getPath('appData'), 'logs/main.log')

  await sleep(5000)
  //每次启动自动更新检查 更新版本 --可以根据自己方式更新,定时或者什么
  autoUpdater.checkForUpdates()

  autoUpdater.logger = logger
  autoUpdater.disableWebInstaller = false
  autoUpdater.autoDownload = false //这个必须写成false,写成true时,我这会报没权限更新,也没清楚什么原因
  autoUpdater.on('error', (error) => {
    logger.error(['检查更新失败', error])
  })
  //当有可用更新的时候触发。 更新将自动下载。
  autoUpdater.on('update-available', (info) => {
    logger.info('检查到有更新,开始下载新版本')
    logger.info(info)
    const { version } = info
    askUpdate(version)
  })
  //当没有可用更新的时候触发。
  autoUpdater.on('update-not-available', () => {
    logger.info('没有可用更新')
  })
  // 在应用程序启动时设置差分下载逻辑
  autoUpdater.on('download-progress', async (progress) => {
    logger.info(progress)
  })
  //在更新下载完成的时候触发。
  autoUpdater.on('update-downloaded', (res) => {
    logger.info('下载完毕!提示安装更新')
    logger.info(res)
    //dialog 想要使用,必须在BrowserWindow创建之后
    dialog
      .showMessageBox({
        title: '升级提示!',
        message: '已为您下载最新应用,点击确定马上替换为最新版本!',
      })
      .then(() => {
        logger.info('退出应用,安装开始!')
        //重启应用并在下载后安装更新。 它只应在发出 update-downloaded 后方可被调用。
        autoUpdater.quitAndInstall()
      })
  })
}

async function askUpdate(version) {
  logger.info(`最新版本 ${version}`)
  let { updater } = getLocalData()
  let { auto, version: ver, skip } = updater || {}
  logger.info(
    JSON.stringify({
      ...updater,
      ver: ver,
    })
  )
  if (skip && version === ver) return
  if (auto) {
    // 不再询问 直接下载更新
    autoUpdater.downloadUpdate()
  } else {
    const { response, checkboxChecked } = await dialog.showMessageBox({
      type: 'info',
      buttons: ['关闭', '跳过这个版本', '安装更新'],
      title: '软件更新提醒',
      message: `${productName} 最新版本是 ${version},您现在的版本是 ${app.getVersion()},现在要下载更新吗?`,
      defaultId: 2,
      cancelId: -1,
      checkboxLabel: '以后自动下载并安装更新',
      checkboxChecked: false,
      textWidth: 300,
    })
    if ([1, 2].includes(response)) {
      let updaterData = {
        version: version,
        skip: response === 1,
        auto: checkboxChecked,
      }
      setLocalData({
        updater: {
          ...updaterData,
        },
      })
      if (response === 2) autoUpdater.downloadUpdate()
      logger.info(['更新操作', JSON.stringify(updaterData)])
    } else {
      logger.info(['更新操作', '关闭更新提醒'])
    }
  }
}

其中 helper.js 是封装的持久化数据相关操作方法。

// src/main/helper.js

import { join } from 'path'
import fs from 'fs'
import { app } from 'electron'
const dataPath = join(app.getPath('userData'), 'data.json')

export function getLocalData(key) {
  if (!fs.existsSync(dataPath)) {
    fs.writeFileSync(dataPath, JSON.stringify({}), { encoding: 'utf-8' })
  }
  let data = fs.readFileSync(dataPath, { encoding: 'utf-8' })
  let json = JSON.parse(data)
  return key ? json[key] : json
}

export function setLocalData(key, value) {
  let args = [...arguments]
  let data = fs.readFileSync(dataPath, { encoding: 'utf-8' })
  let json = JSON.parse(data)
  if (args.length === 0 || args[0] === null) {
    json = {}
  } else if (args.length === 1 && typeof key === 'object' && key) {
    json = {
      ...json,
      ...args[0],
    }
  } else {
    json[key] = value
  }
  fs.writeFileSync(dataPath, JSON.stringify(json), { encoding: 'utf-8' })
}

export async function sleep(ms) {
  return new Promise((resolve) => {
    const timer = setTimeout(() => {
      resolve()
      clearTimeout(timer)
    }, ms)
  })
}

在主进程 app.whenReady 中调用封装的 autoUpdateInit 初始化方法

app.whenReady().then(() => {
    // ...

    // 版本更新初始化
    autoUpdateInit()
})

四、打包调试

因为 electron-updater 在本地开发环境不会去检测更新,所以需要打包后进行操作。 electron-updater 更新检测主要是检测服务端的 latest.yml (Mac软件是生成的是 latest-mac.yml ) 文件中的 version 信息,而这个版本号就是根据 package.jsonversion 生成的。

假定 package.jsonversion: 1.0.0 ,系统为 MacOS。

调试步骤如下:

  1. 执行打包
npm run build:mac
  1. 在生成的安装包文件在 dist 目录,找到 .dmg 后缀的文件
  2. 安装
  3. 修改 package.json ,升级版本号改为 version: 1.1.0
  4. 再次执行打包命令
  5. 启动本地的静态文件服务,这里推荐使用 serve 工具包,全局安装 serve 。
pnpm i -g serve 
  1. 在项目根目录下执行 serve dist 命令,这个命令作用就是把 dist 目录下所有的文件变成静态资源,通过 http 请求拿到对应的资源。
  2. serve 提供的静态服务默认在 3000 端口,如果被占用,会给出一个随机端口,记得修改上边 electron-builder.ymldev-app-update.yml 对应的地址。一切正常的话,访问 http://localhost:3000/latest-mac.yml 就能看到对应资源。
  3. 一切就绪后,启动步骤3安装的应用。等待几秒就能看到上图所示的更新提醒。
  • 注意:MacOS 中需要配置证书,不然检测更新的时候可能会出现问题,这个可以自己生成代码签名证书。可参考:Mac 配置自建证书

欢迎访问:天问博客