electron-builder的json或着yml配置文件中配置一下beforePack文件
beforePack: './packed/beforePack.js' // 换成对应的目录文件
需要安装 adm-zip和asar这两个库
npm i adm-zip asar
beforePack.js文件内容如下
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AdmZip = require('adm-zip')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs')
function getFileZip(context, folder, name) {
// 项目当前目录
const targetPath = context.packager.info.projectDir
const zip = new AdmZip()
folder.map((item) => {
const unpacked = path.join(targetPath, item)
const stat = fs.lstatSync(unpacked)
if (stat.isFile()) {
zip.addLocalFile(unpacked)
} else {
const parent = item.split('/').pop()
zip.addLocalFolder(unpacked, parent)
}
})
zip.writeZip(path.join(context.outDir, name))
}
exports.default = async function (context) {
// 将electron 编译后的文件夹以及package.json文件打包进压缩包,作为增量更新包
// update.zip 为增量包名称
getFileZip(context, ['./out', './package.json'], 'update.zip')
}
增量包内容已经准备完毕后,剩下的就是封装自动更新
需要安装axios,js-yaml,semver这三个库
npm i axios js-yaml semver
以下为自动更新class内容
import axios from 'axios'
import yaml from 'js-yaml'
import semver from 'semver'
import { join } from 'path'
import fs from 'fs'
import { downloadFile } from './downloadFile'
import asar from 'asar'
import AdmZip from 'adm-zip'
// 获取安装路径
export function getAppPath() {
const appBind = getApp()
return appBind.getPath('exe')
}
// 获取 AppData 路径
export function getAppDataPath() {
return join(
process.env.APPDATA ||
(process.platform === 'darwin'
? process.env.HOME + '/Library/Application Support'
: '/var/local'),
Package.name
)
}
class AutoUpdaterMode {
// 是否自动更新
static autoDownload = true
// 是否是测试状态
static isUpdateTest = false
// 更新url
private static updateUlr?: string = undefined
// 增量包名称
private static updateFileName?: string = undefined
// 事件监听
private static EventListener: Recordable<((data?, message?: string) => void)[]> = {}
// 应用信息
private static appInfo: Recordable = {
currentVersion: app.getVersion()
}
// 设置检测更新url
static setFeedURL(updateUlr) {
this.updateUlr = updateUlr
}
// 设置增量包名称
static setUpdateFile(fileName: string) {
this.updateFileName = fileName
}
// 添加事件监听
static on(eventName: string, handler: (data?, message?: string) => void) {
if (this.appInfo[eventName]) {
this.EventListener[eventName].push(handler)
} else {
this.EventListener[eventName] = [handler]
}
}
// 获取最新版本号
static async getLastVersion(rootUrl: string) {
const response = await axios.get(`${rootUrl}/latest.yml`)
const data = yaml.load(response.data)
return data.version
}
// 检测更新
static checkForUpdates() {
if (!this.updateUlr) return undefined
this.getLastVersion(this.updateUlr as string).then((lastVersion) => {
const { currentVersion } = this.appInfo
this.appInfo.lastVersion = lastVersion
if (semver.lt(currentVersion, lastVersion)) {
this.updateAvailable()
const { onlineFile, path } = this.getSavePath()
// 检查文件夹是否存在
if (!fs.existsSync(path)) {
// 如果不存在,创建文件夹
fs.mkdirSync(path)
}
if (this.autoDownload) {
this.updateDownloaded(onlineFile)
}
}
})
}
// 安装并退出
static quitAndInstall() {
this.elevatePrivileges()
.then(async () => {
if (!this.autoDownload) {
const { onlineFile } = this.getSavePath()
await this.updateDownloaded(onlineFile)
}
this.updateProcess(80, '解压文件中')
this.extractAsar()
this.unZip()
this.updateProcess(90, '安装应用中')
this.compressAsar().then(() => {
app.exit()
app.relaunch()
})
})
.catch((error) => {
this.updateError(error)
})
}
// 提升权限
private static elevatePrivileges() {
return new Promise((resolve) => {
// 提权的这一步目前还未实现,使用sudo这个库提权不生效,目前在换别的方案
// 不过可以在electron打包的时候提高权限来解决,这个需要用管理员权限运行应用
resolve(true)
// const { root } = this.getSavePath()
// const options = {
// name: 'name'
// }
// sudo.exec(
// `icacls "${root}" /grant:r %USERNAME%:(OI)(CI)F`,
// options,
// function (error, stdout) {
// if (error) {
// reject(error)
// return
// }
// resolve(stdout)
// }
// )
})
}
// 更新进度
private static updateProcess(process, message) {
this.EventListener['update-process']?.map((item) => {
item?.(process, message)
})
}
// 更新失败
private static updateError(error) {
this.EventListener['update-error']?.map((item) => {
item?.(error)
})
}
// 检测到更新
private static updateAvailable() {
this.EventListener['update-available']?.map((item) => {
item?.(this.appInfo)
})
}
// 下载更新包
private static updateDownloaded(fileUrl) {
return downloadFile(fileUrl, this.getSavePath().filePath, (process) => {
this.updateProcess((process * 0.8).toFixed(0), '下载文件中')
}).then(() => {
// 下载完成
this.EventListener['update-downloaded']?.map((item) => {
item?.(this.appInfo)
})
return true
})
}
// 解压Zip
private static unZip() {
const { filePath, app } = this.getSavePath()
const zip = new AdmZip(filePath)
zip.extractAllTo(app, /*overwrite*/ true)
}
// 解压Asar文件
private static extractAsar() {
const { app, appAsar } = this.getSavePath()
asar.extractAll(appAsar, app)
}
// 压缩成Asar文件
private static compressAsar() {
return new Promise((resolve) => {
const { app, appAsar } = this.getSavePath()
asar.createPackage(app, appAsar).then(() => {
fs.rmSync(app, { recursive: true })
resolve(true)
})
})
}
// 获取保存目录
private static getSavePath() {
const path = join(getAppDataPath(), './pending')
const resources = !this.isUpdateTest
? join(getAppPath(), '../resources')
// 这个为开发环境下自己安装的环境,没找到获取电脑上已安装路径的变量和方法,如果有人知道也可以讲一下
: join('C:/Program Files (x86)/odin-mes', '/resources')
// : join('D:/Program Files (x86)/odin-mes', '/resources')
return {
root: join(getAppPath(), '..'),
path,
onlineFile: `${this.updateUlr}/${this.updateFileName}`,
filePath: join(path, `/${this.updateFileName}`),
resources,
app: join(resources, '/app'),
appAsar: join(resources, '/app.asar')
}
}
}
downloadFile.ts文件内容
import axios from 'axios'
import fs from 'fs'
// 文件下载方法
export async function downloadFile(
url: string,
destinationPath: string,
onProgress?: (process) => void
): Promise<void> {
const response = await axios({
method: 'GET',
url: url,
responseType: 'stream', // 设置响应类型为流
onDownloadProgress: (progressEvent) => {
const totalLength = progressEvent.total ?? 0
const downloadedBytes = progressEvent.loaded
const percentage = (downloadedBytes / totalLength) * 100
onProgress?.(percentage.toFixed(2))
}
})
const writer = fs.createWriteStream(destinationPath)
// 使用管道将响应的流写入文件
response.data.pipe(writer)
return new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
}
现在自动更新的库封装好了,我们就来使用一下,先封装一个钩子函数,以方便使用
import { app, ipcMain } from 'electron'
export function useAutoUpdate() {
const { UpdateServiceUrl } = '你的服务器url'
const mainWindow = getMainWindow() // 换成你获取主进程的方法
let updateUlr = ''
// 设置要检测更新的路径
if (process.platform == 'darwin') {
updateUlr = `${UpdateServiceUrl}/darwin` // ios
} else {
updateUlr = `${UpdateServiceUrl}/win32` // win
}
function checkUpdate() {
AutoUpdaterMode.setFeedURL(updateUlr)
// 设置你的环境是否是开发环境
// AutoUpdaterMode.isUpdateTest = true
AutoUpdaterMode.setUpdateFile('update.zip')
// 更新进度
AutoUpdaterMode.on('update-process', (process, message) => {
mainWindow.webContents.send('update-process', { process, message })
})
// 监听'update-error'事件,更新失败
AutoUpdaterMode.on('update-error', (error) => {
mainWindow.webContents.send('update-error', error)
})
// 监听'update-available'事件,发现有新版本时触发
AutoUpdaterMode.on('update-available', () => {
setTimeout(() => {
// 通知渲染进程提示更新
mainWindow.webContents.send('update-app', true)
}, 1000)
})
// 默认会自动下载新版本,如果不想自动下载,设置
AutoUpdaterMode.autoDownload = false
// 监听'update-downloaded'事件,新版本下载完成时触发
AutoUpdaterMode.on('update-downloaded', ({ lastVersion }) => {
console.log('下载完成')
})
// 检测更新
AutoUpdaterMode.checkForUpdates()
// 监听渲染进程的更新回调
ipcMain.on('restart-update', () => {
AutoUpdaterMode.quitAndInstall()
})
}
return {
checkUpdate
}
}
最后再主应用是调用一下检测更新即可
app.whenReady().then(() => {
const { checkUpdate } = useAutoUpdate()
checkUpdate()
})
大功告成!!!!庆祝一下
网上找了很多的增量更新的方案,看了之后感觉没有真正的解决我的问题,最后只能手搓一个
还有一个提权的问题没有找到好的方案,如果你们有好的方案可以告诉一下
目前是设置的electron-builder的requestedExecutionLevel属性来临时解决
不过这个是主要针对的是全用户安装C盘情况下的权限问题,如果你为个人用户安装就可以不考虑这些
"requestedExecutionLevel": "highestAvailable"