4、软件更新、下载、安装

257 阅读5分钟

软件更新、下载、安装

  • 更新插件 electron-updater
  • 安装插件 child_process
  • 此处只使用了 electron-updater 的检测功能,没有用其内置的下载,监听下载进度等功能。如有需要请前往 blog.csdn.net/daipianpian… 文章处查看,有详细步骤。

大致流程如下,你可以根据自己的需求自定义是否强制更新,自动更新等功能。

graph TD 
检测是否有可用更新 --> 有更新
检测是否有可用更新 --> 无更新
无更新 --> 结束
有更新--> 用户判断是否需要更新
用户判断是否需要更新--> 需要更新
用户判断是否需要更新--> 无需更新
无需更新 --> 取消更新
需要更新 --> 下载软件 --> 安装软件 --> 更新成功

检测更新

  1. src/main/operation.ts 检测更新函数,下载函数,安装函数
  2. 正常情况是能够自动判断文件大小的,这里测试用的是 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)
    }
}

  1. 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)
  })
  ···
})
  1. 预加载中注册
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>

看到这,最后你会发现,自己封装检测更新等功能还是比较麻烦,而且还有很多细节本文也没处理的,建议直接使用完整插件。练习的话可以试试自己封装。