软件更新、下载、安装
- 更新插件 electron-updater
- 安装插件 child_process
- 此处只使用了 electron-updater 的检测功能,没有用其内置的下载,监听下载进度等功能。如有需要请前往 blog.csdn.net/daipianpian… 文章处查看,有详细步骤。
大致流程如下,你可以根据自己的需求自定义是否强制更新,自动更新等功能。
graph TD
检测是否有可用更新 --> 有更新
检测是否有可用更新 --> 无更新
无更新 --> 结束
有更新--> 用户判断是否需要更新
用户判断是否需要更新--> 需要更新
用户判断是否需要更新--> 无需更新
无需更新 --> 取消更新
需要更新 --> 下载软件 --> 安装软件 --> 更新成功
检测更新
- src/main/operation.ts 检测更新函数,下载函数,安装函数
- 正常情况是能够自动判断文件大小的,这里测试用的是
unicloud的支付宝云免费提供的云存储,上传的文件下载时无法判断大小,尚未找到原因,于是就手动指定文件大小。
// download.js
import { dialog, clipboard, BrowserWindow } from 'electron'
import fs from 'fs'
import axios from 'axios'
import wallpaper from 'wallpaper'
// 引入项目
const { autoUpdater } = require('electron-updater')
const { spawn } = require('child_process')
const path = require('path')
// 配置项
autoUpdater.forceDevUpdateConfig = true //开发环境下强制更新
autoUpdater.autoDownload = false // 自动下载
autoUpdater.autoInstallOnAppQuit = false // 应用退出后自动安装
let isStartCheckForUpdates = false
// 检查更新 url 需要下载文件的保存路径
// https://env-00jxh5vvj39r.normal.cloudstatic.cn/wallpaper/wallpaper.exe 这是文件的完整路径,
// url 为: https://env-00jxh5vvj39r.normal.cloudstatic.cn/wallpaper 只需要到具体文件下面
export const checkForUpdates = (url: string) => {
return new Promise((resolve, reject) => {
if (isStartCheckForUpdates) {
reject(new Error('更新检查已在进行中'))
return
}
isStartCheckForUpdates = true
// 指定更新地址
autoUpdater.setFeedURL(url)
autoUpdater.autoDownload = false
autoUpdater.checkForUpdates()
// 开始检查更新事件 提示语: '正在检查更新'
autoUpdater.on('checking-for-update', function () {
})
// 发现可更新版本 提示语: 检测到新版本,准备下载
autoUpdater.on('update-available', function (e) {
// 这里获取新版本信息,具体获取方式取决于autoUpdater的实现
// 假设它有一个方法可以获取到新版本的详细信息对象,比如版本号、更新内容等
isStartCheckForUpdates = false
resolve(e)
})
// 没有可更新版本事件 提示语: '已经是最新版本'
autoUpdater.on('update-not-available', function () {
isStartCheckForUpdates = false
resolve(null)
})
// 更新发生错误时事件 提示语: '软件更新异常,请重试'
autoUpdater.on('error', function () {
isStartCheckForUpdates = false
reject(new Error('软件更新异常,请重试'))
})
})
}
// 软件下载
// currentWindow 当前窗口:主进程向当前窗口发现下载进度和下载结果
// downloadUrl:文件下载地址
// fileName:文件保存名
// savePath:文件下载后的保存地址
export const downloadSoftware = (currentWindow: BrowserWindow, downloadUrl: string, fileName: string, savePath: string) => {
const targetFolder = path.join(savePath)
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder, { recursive: true })
}
const stream = fs.createWriteStream(path.join(targetFolder, fileName))
// let totalBytes = parseInt(response.headers['content-length'], 10);
let totalBytes = 93 * 1024 * 1024 // 手动指定文件大小为93MB
let downloadedBytes = 0
axios({
method: 'GET',
url: downloadUrl,
responseType: 'stream'
}).then((response) => {
response.data.on('data', (chunk) => {
downloadedBytes += chunk.length
let progress = Math.floor((downloadedBytes / totalBytes) * 100)
// 只通知当前窗口
if (currentWindow) {
if (progress > 100) progress = 100
currentWindow.webContents.send('download-progress', progress)
}
})
response.data.pipe(stream)
return new Promise((resolve, _) => {
stream.on('close', () => {
if (currentWindow) {
const obj = {
type: 'success',
message: '下载成功'
}
currentWindow.webContents.send('download-complete', obj)
resolve(true)
}
// 下载成功后 安装软件
installSoftware(currentWindow, path.join(targetFolder, fileName))
})
stream.on('error', (err) => {
stream.close()
if (currentWindow) {
const obj = {
type: 'error',
message: '下载失败' + err
}
currentWindow.webContents.send('download-complete', obj)
resolve(true)
}
})
})
}).catch((_) => {
stream.close()
// 通知渲染进程下载失败
if (currentWindow) {
const obj = {
type: 'error',
message: '下载失败'
}
currentWindow.webContents.send('download-complete', obj)
}
})
}
// 安装软件函数
const installSoftware = (currentWindow: BrowserWindow, filePath: string) => {
// 判断是否为Windows可执行文件(.exe)
const ext = path.extname(filePath)
if (ext === '.exe') {
try {
// 使用 spawn 启动安装程序,以便更好地处理输出和错误信息
const installerProcess = spawn(filePath, [], { shell: true })
// 监听安装进程的输出信息,可用于在控制台显示安装进度等相关信息(可选)
installerProcess.stdout.on('data', (data) => {
console.log(`安装程序输出: ${data}`)
})
// 监听安装进程的错误信息
installerProcess.stderr.on('data', (data) => {
console.log(`安装程序错误: ${data}`)
})
// 当安装进程结束时
installerProcess.on('close', (code) => {
if (code === 0) {
const obj = {
type: 'success',
message: '软件安装成功'
}
currentWindow.webContents.send('download-complete', obj)
} else {
const obj = {
type: 'error',
message: '安装失败'
}
currentWindow.webContents.send('download-complete', obj)
}
})
} catch (err: any) {
const obj = {
type: 'error',
message: `无法执行文件`
}
currentWindow.webContents.send('download-complete', obj)
}
} else {
const obj = {
type: 'error',
message: '不支持的文件类型,无法安装软件'
}
currentWindow.webContents.send('download-complete', obj)
}
}
- src/main/index.ts 主程序注册ipc消息
app.whenReady().then(() => {
···
// 检测软件更新
ipcMain.handle('update-software', (_, url: string) => {
return checkForUpdates(url)
})
// 下载更新
ipcMain.handle('download-update', (event, param: any) => {
// 获取当前窗口
const currentWindow = BrowserWindow.fromWebContents(event.sender)!
return downloadSoftware(currentWindow, param.download, param.fileName, param.savePath)
})
···
})
- 预加载中注册
const api = {
···
updateSoftware: (url: string) => {
return ipcRenderer.invoke('update-software', url)
},
downloadUpdate: (params: any) => {
return ipcRenderer.invoke('download-update', params)
}
···
}
配置项
- 配置安装选项
- 配置 electron-updater
// package.json
{
···
"build": {
···
"nsis": {
"oneClick": false, // 关闭一键式安装
"allowToChangeInstallationDirectory": true // 允许安装时用户自定义路径
},
// 重要配置,配置该选项后,打包才会生成 electron-updater 需要的 latest.yml 文件
"publish": [
{
"provider": "generic",
"url": "https://env-00jxh5vvj39r.normal.cloudstatic.cn/wallpaper/",
"channel": "latest"
}
]
}
}
- 安装配置
// electron-builder.yml
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
oneClick: false
allowToChangeInstallationDirectory: true
使用
- src/renderer/src/views/Setting/sets/about.vue
这里为什么不用检测信息中的文件名而是使用wallpaper.exe,因为该软件名称带有中文,安装过程中会出现无法解析中文的情况,换成英文就没问题了。
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { settingStore } from '@renderer/store/setting'
import { storeToRefs } from 'pinia'
import { ElMessage } from 'element-plus'
import Progress from '@renderer/components/Progress.vue'
const { ipcRenderer } = require('electron')
const set = settingStore()
const { downloadLocation } = storeToRefs(set)
const baseUrl = 'https://env-00jxh5vvj39r.normal.cloudstatic.cn/wallpaper'
let myApi
const version = ref('')
const downProgress = ref(0)
const centerDialogVisible = ref(false)
const isDownloading = ref(false)
const versionInfo: any = ref({})
// 检测更新
const update = async () => {
const res = await myApi.updateSoftware(baseUrl)
versionInfo.value = res
if (isDownloading.value) {
ElMessage({
message: '正在下载中,请稍后',
type: 'warning',
plain: true
})
return
}
if (res && res.version) {
// 开启弹窗
centerDialogVisible.value = true
}
if (res == null) {
ElMessage({
message: '当前已是最新版本',
type: 'success',
plain: true
})
}
}
// 更新app
const updateApp = () => {
isDownloading.value = true
const obj = {
download: 'https://env-00jxh5vvj39r.normal.cloudstatic.cn/wallpaper/wallpaper.exe',
fileName: 'wallpaper.exe',
savePath: downloadLocation.value
}
myApi.downloadUpdate(obj)
centerDialogVisible.value = false
}
const getVersion = async () => {
const res = await myApi.getVersion()
version.value = res
}
onMounted(() => {
myApi = window.api
// 监听下载完成事件
ipcRenderer.removeAllListeners('download-complete')
ipcRenderer.on('download-complete', (_, info) => {
downProgress.value = 0
isDownloading.value = false
ElMessage({
message: info.message,
type: info.type,
plain: true
})
})
// 监听下载进度事件
ipcRenderer.on('download-progress', (_, progress) => {
downProgress.value = progress
})
getVersion()
})
</script>
看到这,最后你会发现,自己封装检测更新等功能还是比较麻烦,而且还有很多细节本文也没处理的,建议直接使用完整插件。练习的话可以试试自己封装。