用electron框架打包的软件,如何能让它实现自动更新软件版本能力呢?
1、electron-builder打包出来的文件会生成一个latest.yml文件,用来判断当前软件版本
2、electron-updater实现应用自动监听版本是否需要更新,需要依赖electron-builder打包出来的latest.yml文件
首先先看一下latest.yml文件内有什么内容
1 2 3 4 5 6 7 8 |
version: 1.1.0 files: - url: Toolbox-web-1.1.0.exe sha512: jHGYait0kQj0tsUVGHbYy6bAcuWrAFXKeCZAXPQx5T52WYsCOns/EsVVVPfuzaevljs7J09SI0P46W3xTw9SRQ== size: 80561783 path: Toolbox-web-1.1.0.exe sha512: jHGYait0kQj0tsUVGHbYy6bAcuWrAFXKeCZAXPQx5T52WYsCOns/EsVVVPfuzaevljs7J09SI0P46W3xTw9SRQ== releaseDate: '2022-09-05T01:59:15.443Z' |
PS: 主要标记了版本号,文件下载地址等内容
下面我们开始实现一个自动更新软件的功能,本案例是通过部署一台服务器存放静态资源方式
第一步:配置打包参数
在package.json中添加一个build参数,里面配置如下:
PS:win包一定要配置安装模式,如果是免安装打开的不会生成latest.yml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
{ "version": "1.5.0", // 填写一个比较大的版本号,用于测试 // ...省略其它参数 "build": { "appId": "com.tec.etoolbox", "productName": "Toolbox", // 软件名 // 软件更新地址 "publish": { "provider": "generic", // 私有化部署服务 "url": "http://localhost:7000/update", // 服务器地址,用来判断是否需要更新,里面存放打包好的文件 "channel": "latest" }, "nsis": { "oneClick": false, // 是否一键安装 "deleteAppDataOnUninstall": true, // 是否可卸载 "allowToChangeInstallationDirectory": true // 自定义安装目录 }, "mac": { "target": "dmg", "icon": "build/icon.icns", "category": "public.app-category.developer-tools", "type": "distribution", "hardenedRuntime": true }, "dmg": { "contents": [ { "x": 130, "y": 220 }, { "x": 410, "y": 220, "type": "link", "path": "/Applications" } ] }, "win": { "target": "nsis", "icon": "build/icon.png", "artifactName": "${productName}-web-${version}.${ext}", "verifyUpdateCodeSignature": false }, "linux": { "target": [ "AppImage" ], "category": "Development" }, "extends": null, "files": [ "build/**/*" ], "directories": { "buildResources": "public" } }, } |
第二步:构建打包
在第一步中已经配置了一个比较大的"version"版本号,执行一下构建electron命令,2、,查看是生会生成latest.yml文件
以下几个打包命令参考:
1 2 3 |
electron-builder # 构建打包,默认会打包win的 electron-builder -p always # 用于构建后,更新到发版服务器 # 默认情况下都会生成latest.yml文件 |
如果会生成latest.yml文件,接着就往下跟着步骤走
第三步:准备一个静态服务环境
这里以前端用node启一个静态服务目录的能力(自己搭建一个)!
这了为了方便我直接搭建好了一个node服务,只需要把文件抛到public目录即可!github仓库地址
执行npm run dev启动,访问地址127.0.0:6789/update 就能访问此目录内所有静态文件
PS:update目录是我自己创建的
第四步:实现软件更新逻辑代码
创建一个updateApp.ts文件,用来写更新应用的逻辑,代码内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
// utils/updateApp.ts文件 import { app, dialog, ipcMain } from 'electron' import * as isDev from 'electron-is-dev' import log from 'electron-log' import { mainWindow } from '../main' // 此为main入口的主窗口BrowserWindow对象 import { RECEIVE_CHANNLE, SEND_CHANNLE } from '../consts/channleName' const { autoUpdater } = require('electron-updater') const enviroment = process.platform === 'darwin' ? 'mac' : 'win' // 定义返回给渲染层的相关提示文案 const message = { error: '检查更新出错', checking: '正在检查更新……', updateAva: '检测到新版本,正在下载……', updateNotAva: '现在使用的就是最新版本,不用更新', } export const updateApp = async () => { const updateUrl = 'http://127.0.0.1:6789/update' // 这里是为了在本地做应用升级测试使用 if (isDev) { // 处理开发环境被跳过更新问题,临时使用,使用完注释掉 Object.defineProperty(app, 'isPackaged', { get() { return true }, }) // 如果不想启node服务,使用本地测试yml文件,测试更新 // const updatePath = path.join(__dirname, '../../', 'dev-app-update.yml') // autoUpdater.updateConfigPath = updatePath } if (enviroment === 'win') { // 本地模拟更新的端口 autoUpdater.setFeedURL(updateUrl) } // 主进程跟渲染进程通信 const sendUpdateMessage = (text: string) => { // 发送消息给渲染进程 mainWindow?.webContents.send('message', text) } // 检测下载错误 autoUpdater.on('error', (error: any) => { sendUpdateMessage(<code>${message.error}:${error}</code>) }) // 检测是否需要更新 autoUpdater.on('checking-for-update', () => { sendUpdateMessage(message.checking) }) // 检测到可以更新时 autoUpdater.on('update-available', () => { // 这里我们可以做一个提示,让用户自己选择是否进行更新 dialog .showMessageBox({ type: 'info', title: '应用有新的更新', message: '发现新版本,是否现在更新?', buttons: ['是', '否'], }) .then(({ response }) => { if (response === 0) { // 下载更新 autoUpdater.downloadUpdate() sendUpdateMessage(message.updateAva) } }) // 也可以默认直接更新,二选一即可 // autoUpdater.downloadUpdate(); // sendUpdateMessage(message.updateAva); }) // 检测到不需要更新时 autoUpdater.on('update-not-available', () => { // 这里可以做静默处理,不给渲染进程发通知,或者通知渲染进程当前已是最新版本,不需要更新 sendUpdateMessage(message.updateNotAva) }) // 更新下载进度 autoUpdater.on('download-progress', (progress: any) => { log.warn('触发下载。。。') log.warn(progress) // 直接把当前的下载进度发送给渲染进程即可,有渲染层自己选择如何做展示 mainWindow?.webContents.send(RECEIVE_CHANNLE.downloadProgress, progress) }) // 当需要更新的内容下载完成后 autoUpdater.on('update-downloaded', () => { // 给用户一个提示,然后重启应用;或者直接重启也可以,只是这样会显得很突兀 log.warn('开始更新') dialog .showMessageBox({ title: '安装更新', message: '更新下载完毕,应用将重启并进行安装', }) .then(() => { // 退出并安装应用 setImmediate(() => autoUpdater.quitAndInstall()) }) }) // 我们需要主动触发一次更新检查 ipcMain.on(SEND_CHANNLE.checkForUpdate, () => { log.warn('执行自动更新检查') // 当我们收到渲染进程传来的消息,主进程就就进行一次更新检查 autoUpdater.checkForUpdates() }) // 当前引用的版本告知给渲染层 ipcMain.on(SEND_CHANNLE.checkAppVersion, () => { mainWindow?.webContents.send(RECEIVE_CHANNLE.version, app.getVersion()) }) } |
channleName内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// channleName文件内容 /** 主进程发出的通知事件 */ export const SEND_CHANNLE = { /** 版本更新通知 */ checkForUpdate: 'checkForUpdate', /** 获取app新版本号 */ checkAppVersion: 'checkAppVersion', } /** 渲染进程发出的通知,监听回调事件 */ export const RECEIVE_CHANNLE = { /** 最新版本通知 */ version: 'version', /** 下载进度通知 */ downloadProgress: 'downloadProgress', } |
然后在main入口文件内添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import log from 'electron-log' import { updateApp } from './utils/updateApp' const { autoUpdater } = require('electron-updater') // 设置日志打印 autoUpdater.logger = log autoUpdater.logger.transports.file.level = 'info' // 是否自动下载更新,设置为 false 时将通过 API 触发更新下载 autoUpdater.autoDownload = false // 是否允许版本降级,也就是服务器版本低于本地版本时,依旧以服务器版本为主 autoUpdater.allowDowngrade = true updateApp() // 检查自动更新,或是手动触发 |
此时打开软件就能检测自动更新功能软件了,但是还不太友好,接下来我们和view视图层做一下交互通信
第五步:实现与view视图层交互,通知更新
下面实现软件更新,进行在view页面视图层进行简单的交互,可以基于以下实现扩展更新体验交互逻辑,6、首先我们要给view视图层暴露electron API参数(直接调用是会报错的),需要利用中间层去做这件事件,创建一个preload.js文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// preload文件 const { contextBridge, ipcRenderer } = require('electron') interface Ipc { render: { send: string[] receive: string[] } } const ipc: Ipc = { render: { // 主进程发出的通知 send: ['checkForUpdate', 'checkAppVersion'], // 渲染进程发出的通知 receive: ['version', 'downloadProgress'], }, } // 通过contextBridge将electron注入到渲染进程的window上面,我们只需要访问window.electron,即可访问到相关的内容 contextBridge.exposeInMainWorld('electron', { ipcRenderer, ipcRender: { // 主进程发送通知给渲染进程 send: (channel: any, data: any) => { const validChannels = ipc.render.send if (validChannels.includes(channel)) { ipcRenderer.send(channel, data) } }, // 渲染进程监听到主进程发来的通知,执行相关的操作 receive: (channel: any, func: any) => { const validChannels = ipc.render.receive if (validChannels.includes(channel)) { ipcRenderer.on(<code>${channel}</code>, (event, ...args) => func(...args)) } }, }, }) // 把api暴露给view视图层使用 window.electron = require('electron') window.ipcRenderer = require('electron').ipcRenderer |
然后到main入口文件内,添加刚刚的preload.js文件,利用预加载文件能力去读取preload文件,把参数暴露给view视图层
1 2 3 4 5 6 7 8 9 10 11 12 |
// 关键代码处 const mainWindow = new BrowserWindow({ title: app.name, width: 1200, height: 900, webPreferences: { nodeIntegration: false, contextIsolation: true, // 预加载preload.js文件,此文件地址一定是编译后的地址 preload: path.join(__dirname, 'preload.js'), }, }) |
在视图层react页面中,创建一个组件Update,并且在页面中引用此组件
Update组件代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import React, { useState, useEffect } from 'react' const { ipcRender } = window.electron const Update = () => { // 页面上的提示信息 const [text, setText] = useState('') // 当前应用版本信息 const [version, setVersion] = useState('0.0.0') // 当前下载进度 const [progress, setProgress] = useState(0) useEffect(() => { // 给主进程发通知,让主进程告诉我们当前应用的版本是多少 ipcRender.send('checkAppVersion') // 接收主进程发来的通知,检测当前应用版本 ipcRender.receive('version', (v: React.SetStateAction<string>) => { setVersion(v) }) // 给主进程发通知,检测当前应用是否需要更新 ipcRender.send('checkForUpdate') // 接收主进程发来的通知,告诉用户当前应用是否需要更新 ipcRender.receive('message', (data: React.SetStateAction<string>) => { setText(data) }) // 如果当前应用有新版本需要下载,则监听主进程发来的下载进度 ipcRender.receive('downloadProgress', (data: { percent: string }) => { const newProgress = parseInt(data.percent, 10) setProgress(newProgress) }) }, []) return ( <div style={{ marginTop: 30 }}> <p>current app version: {version}</p> <p>{text}</p> {progress ? <p>下载进度:{progress}%</p> : null} </div> ) } export default Update |
此时就可以进行测试更新,会与当前页面进行交互展示了