Electron自制翻译工具:自动更新

前言

之前要更新自己做的electron软件,只能到github重新下载release包安装,太麻烦了。所以就决定添加一个自动更新的功能,正好来试一下electron如何实现自动更新。

因为我的项目是通过electron-vite脚手架搭建的,所以自带了electron-updaterelectron-builder,只要配置一下,写一下更新逻辑就可以了实现了。

先来看看效果,下载过程就跳过了。

后面有源码仓库地址

正文

自动更新原理: 检查安装的版本号和远程的版本号是否一致。打包后build文件夹里会有一个latest.yml文件,里面有一个version字段。

流程

  1. 安装electron-updaterelectron-builder
  2. 配置更新地址

为了在开发环境中好测试功能,编写以下代码,不然会出现Skip checkForUpdates because application is not packed and dev update config is not forced

js 复制代码
//update.ts
if (!app.isPackaged) {
  Object.defineProperty(app, 'isPackaged', {
    get: () => true
  })
  autoUpdater.forceDevUpdateConfig = true
  autoUpdater.updateConfigPath = path.join(__dirname, '../../dev-app-update.yml')
}

dev-app-update.yml配置,为了开发环境使用

yml 复制代码
provider: generic
url: http://www.hewkq.top/auto-updates
updaterCacheDirName: screenshot-translation-updater

在根目录下electron-builder.yml中配置,为了生产环境使用。

yml 复制代码
publish:
  provider: generic
  url: http://www.hewkq.top/auto-updates
  1. 主进程更新逻辑
ts 复制代码
//update.ts
import { app, ipcMain, BrowserWindow } from 'electron'
import { autoUpdater } from 'electron-updater'
import path from 'path'
import { SendEnum } from '../type/ipc-constants'
import { UpdateProgress } from '../type/update'
import { showNotification } from './utils/notification'
import { NoticeType } from '../type/notice'

export const registerAutoUpdate = (mainWindow: BrowserWindow) => {
  /* 开发环境 */
  if (!app.isPackaged) {
    Object.defineProperty(app, 'isPackaged', {
      get: () => true
    })
    autoUpdater.forceDevUpdateConfig = true
    autoUpdater.updateConfigPath = path.join(__dirname, '../../dev-app-update.yml')
  }

  /* 不允许自动下载更新 */
  autoUpdater.autoDownload = false
  /* 允许降级更新(应付回滚的情况) */
  autoUpdater.allowDowngrade = true

  /* 开始检查更新 */
  autoUpdater.on('checking-for-update', () => {
    console.log('start checking for update')
  })
  /* 发现更新 */
  autoUpdater.on('update-available', (info) => {
    console.log('find update version', info)
  })
  /* 没有更新 */
  autoUpdater.on('update-not-available', (info) => {
    console.log('no need to update', info.version)
  })
  /* 更新下载进度 */
  autoUpdater.on('download-progress', (progressInfo) => {
    console.log('update progress', progressInfo)
    mainWindow.webContents.send(SendEnum.DOWNLOAD_PROGRESS, progressInfo)
  })
  /* 更新下载完成 */
  autoUpdater.on('update-downloaded', () => {
    console.log('update downloaded')
    showNotification('更新下载完成,请等待重启', NoticeType.SUCCESS)
    autoUpdater.quitAndInstall()
  })
  /* 更新失败 */
  autoUpdater.on('error', (errorMessage) => {
    console.log('update error', errorMessage.message)
    showNotification(errorMessage.message, NoticeType.ERROR)
    mainWindow.webContents.send(SendEnum.DOWNLOAD_FAIL)
  })


  /** 获取应用版本 */
  ipcMain.handle(SendEnum.GET_APP_VERSION, (event) => {
    return app.getVersion()
  })

  /* 检查更新 */
  ipcMain.on(SendEnum.CHECK_UPDATE, (event) => {
    autoUpdater.checkForUpdates().then((result) => {
      event.reply(SendEnum.CHECK_UPDATE_RESULT, result)
      event.reply(SendEnum.CHECK_UPDATE_COMPLETE, result)
    })
  })

  /* 下载更新 */
  ipcMain.on(SendEnum.DOWNLOAD_UPDATE, (event) => {
    autoUpdater.downloadUpdate()
  })
}

registerAutoUpdat引入主进程入口文件main.ts中。

  1. 渲染进程代码
tsx 复制代码
//APP.tsx
  useEffect(() => {
    window.electron.ipcRenderer.send(SendEnum.CHECK_UPDATE)
    return () => {
      window.electron.ipcRenderer.removeAllListeners(SendEnum.CHECK_UPDATE_RESULT)
    }
  }, [])
tsx 复制代码
//UpdateDialog.tsx
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle
} from '@renderer/components/ui/dialog'
import { Button } from '@renderer/components/ui/button'
import { useCallback, useEffect, useState } from 'react'
import { SendEnum } from '../../../type/ipc-constants'
import { UpdateProgress } from '../../../type/update'
import { Progress } from '@renderer/components/ui/progress'
import { X } from 'lucide-react'
import { toast } from 'sonner'

export default function UpdateDialog() {
  const [isOpen, setIsOpen] = useState(false)
  const [updateData, setUpdateData] = useState<UpdateProgress | null>(null)
  const [isUpdating, setIsUpdating] = useState(false)

  /* 立即下载 */
  const onDownload = () => {
    setIsUpdating(true)
    window.electron.ipcRenderer.send(SendEnum.DOWNLOAD_UPDATE)
  }

  /* 下次再说 */
  const onNextTime = () => {
    setIsOpen(false)
  }

  const onClose = useCallback(() => {
    setIsOpen(false)
  }, [])

  useEffect(() => {
    // 监听检查更新的结果
    window.electron.ipcRenderer.on(SendEnum.CHECK_UPDATE_RESULT, (_event, result) => {
      const { isUpdateAvailable, versionInfo } = result
      if (isUpdateAvailable) {
        setUpdateData(versionInfo)
        setIsOpen(true)
      }
    })

    // 监听更新失败
    window.electron.ipcRenderer.on(SendEnum.DOWNLOAD_FAIL, (_event) => {
      setIsUpdating(false)
      setIsOpen(false)
    })

    return () => {
      window.electron.ipcRenderer.removeAllListeners(SendEnum.CHECK_UPDATE_RESULT)
    }
  }, [])

  useEffect(() => {
    if (!isUpdating) {
      return
    }

    // 监听下载进度
    window.electron.ipcRenderer.on(SendEnum.DOWNLOAD_PROGRESS, (_event, progressInfo) => {
      setUpdateData(progressInfo)
      if (progressInfo.percent === 100) {
        setIsUpdating(false)
        setIsOpen(false)
      }
    })

    return () => {
      window.electron.ipcRenderer.removeAllListeners(SendEnum.DOWNLOAD_PROGRESS)
    }
  }, [isUpdating])

  return (
    <Dialog open={isOpen}>
      <DialogContent className="sm:max-w-md bg-slate-50" showCloseButton={false}>
        <DialogHeader>
          <DialogTitle className="flex items-center justify-between text-lg font-semibold text-slate-800">
            更新提示
            <Button variant="ghost" onClick={onClose} disabled={isUpdating}>
              <X className="w-4 h-4" />
            </Button>
          </DialogTitle>
          <DialogDescription asChild>
            <div>
              <p className="pt-4 text-base text-slate-700">
                {isUpdating ? '正在下载更新,请稍后...' : '发现新版本,是否需要立即下载?'}
              </p>
              {isUpdating && (
                <div className="flex items-center mt-2 ">
                  <Progress value={updateData?.percent} />
                  <span className="text-sm text-slate-700 ml-2">
                    {Math.floor(updateData?.percent || 0)}%
                  </span>
                </div>
              )}
            </div>
          </DialogDescription>
        </DialogHeader>
        <DialogFooter className="gap-2">
          <Button variant="outline" onClick={onNextTime} disabled={isUpdating}>
            下次再说
          </Button>
          <Button onClick={onDownload} disabled={isUpdating}>
            {isUpdating ? '正在下载' : '立即下载'}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
  1. 配置服务器nginx

没有配置服务器nginx的话,将无法访问到文件,也就无法自动更新

conf 复制代码
server {
     # 监听 HTTPS 端口
     listen 80;
     server_name hewkq.top;
  
      # Electron 自动更新的目录
      location /auto-updates {
          # 指定存放更新文件的本地目录
          root /var/www;
          
          # 确保 Nginx 能够正确处理 `latest.yml` 和其他文件
          try_files $uri $uri/ =404;
      }
  }

打包更新

自动更新的实现基本就是这样,需要发版的时候,只需要更改package.json里面的版本号,然后打包。将打包后的xxx.exexxx.blockmaplatest.yml放到服务器中electron访问的更新目录中就行了。

有一个点需要注意,为了让 electron-updater增量更新功能正常工作,服务器上不能删除 旧版本的 .blockmap 文件。因为electron-updater 需要同时访问两个版本的.blockmap 文件。 它将这两个文件进行对比,才能计算出增量更新所需的数据块。

当然你删除了.blockmap文件也没有事,electron-updater会自动回退到完整更新去下载.exe文件。

结语

本人使用Windows系统,其他系统是否需要别的配置还是调整不清楚。

源码地址:github.com/lzt-T/scree...

谢谢观看!!

相关推荐
sixgod_h16 分钟前
Threejs源码系列- MathUtils(1)
前端·webgl
lichenyang45317 分钟前
从0开始的中后台管理系统-6(添加用户以及绑定角色给用户动态添加权限,以及在layout父路由组件去进行路径跳转判断)
前端
小高00717 分钟前
协商缓存和强缓存
前端·javascript·面试
用户479492835691518 分钟前
你真的很了解eslint吗?(代码检查工具的历史变革及底层原理)
前端
前端Hardy19 分钟前
HTML&CSS&JS:超酷炫的一键登录页面
前端·javascript·css
七十二時_阿川23 分钟前
React上下文之useContext
前端·程序员
sorryhc31 分钟前
CSR秒开有可能么?(附AI驱动学习实践推理过程)
前端·javascript·ai编程
龙井>_<44 分钟前
vue项目封装axios请求,支持判断当前环境及判断token是否过期等等(详细教程,可复制粘贴代码)
前端·javascript·vue.js·前端框架
Hashan1 小时前
微信小程序:实现证件OCR识别
前端·vue.js·微信小程序
vaelcy1 小时前
css3实现登录框动画特效效果
前端·css