背景
公司有个项目,需要做个桌面端应用,把某一个后台系统嵌入进去,同时不用花一分钱去上架应用商店和签名,只给内部人员mac或者win电脑上使用,要求能自动更新最新的版本
方案
问题分析
- 需要嵌入一个后台系统意思是功能和界面长的和web后台系统一样,还得跨端,好像只能使用是
electron了 - mac下自动更新需要签名才可以,这里不想签名那就只能自己实现一个伪自动更新
然后分析了下在
electron中的自动更新是依赖于另外一个执行文件操作, 安装包下载完成后,不是用node去执行安装的,会调用其他语言的脚本如window下的bat脚本执行校验,解压,替换文件夹,重启。为啥不用node呢,因为解压完后要替换,node的执行依赖于当前项目下的node_modules包,替换命令执行的时候,是先删除node执行环境目录才move的,这时候就会报node不存在的错误。
mac下的electron应用构成
可以看到
Resource中的应用代码,Resource之外基本都是electron要运行的框架代码,基本不变,所以在mac下可以考虑把Resource和应用信息的Info.plist替换即可,或者你也可以像官网一样,用另外一种脚本语言去执行校验,解压,替换文件夹,重启应用。接下来的实现只用node去实现
方案实现
技术选型
- 框架
electron - 打包
electron-builder - 自动更新
electron-updater
首先打包时候需要在配置文件electron-builder.js中加上
releaseInfo: { // 更新日志
releaseNotes: '更新XXXXXX功能'
},
publish: [
{
provider: 'generic',
// 安装包的下载和检查基础路径
url: 'http://localost/update/'
}
],
这样electron-updater默认就会使用url去获取需要更新的文件
然后在项目中新建一个autoUpdate.js文件,在主进程的app.on('activate', cb)事件回调中触发这个文件中的initUpdater方法
const { autoUpdater } = require('electron-updater')
const AdmZip = require('adm-zip')
const { app } = require('electron')
function unZip(zipPath, extractionPath) {
return new Promise((resolve, reject) => {
try {
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractionPath, true)
resolve()
} catch (err) {
reject(err)
}
})
}
function execMd(md) {
return new Promise((resolve, reject) => {
const { exec } = require('node:child_process')
exec(md, (error, stdout, stderr) => {
if (error) {
reject(error)
return
}
resolve()
})
})
}
async function startInstall (res) {
try {
if (!isMac) {
// windows安装不需要证书,直接走框架的默认安装
autoUpdater.quitAndInstall()
return
}
await customInstallMacApp(res)
} catch (err) {
log.error('安装失败 => ', err)
}
}
async function customInstallMacApp(res) {
const appPath = app.getAppPath() // 包路径,如xxx/asar
const appFile = appPath.match(/(?<=\/)\W+(?=\.app)/, '')[0] + '.app' // app名称,如demo.app
const installDir = appPath.replace(new RegExp(`${appFile}.*`), '') // 安装路径,比如/Applications
const unzipPath = res.downloadedFile.replace(res.path, '') // 安装包下载路径
const installPath = installDir + appFile // 安装的文件路径,比如/Applications/demo.app
// 第一步,解压zip压缩包
await unZip(res.downloadedFile, unzipPath)
// 安装mac应用, 只替换原有应用下的Resources目录
await execMd(`rm -rf ${installPath}/Contents/Resources`)
await execMd(`mv -f ${unzipPath}${appFile}/Contents/Resources ${installPath}/Contents/Resources`)
app.quit()
app.relaunch()
}
function initUpdater() {
autoUpdater.disableWebInstaller = false // 这个写成 false,写成 true 时,可能会报没权限更新
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.autoDownload = false
autoUpdater.on('update-available', (info) => {
const { version } = info
// 这里做的是检测到更新,直接就下载
autoUpdater.downloadUpdate()
})
// 在更新下载完成的时候触发。
autoUpdater.on('update-downloaded', async (res) => {
// 下载完毕, 退出应用,安装开始
startInstall(res)
})
// 让应用跑一会儿,在检查更新,避免UI冲突
setTimeout(() => {
autoUpdater.checkForUpdates()
}, 5000)
}
验证阶段
对于正常electron开发者来说,update-downloaded这个事件肯定是能走到的,只是卡在安装这个问题上,这里为了验证结果,可以模拟下update-downloaded事件的返回
const res = {
version: '1.0.1',
files: [
{
url: 'xxx-1.0.1-arm64-mac.zip',
sha512:
'w2WBWauiXCBa6QP13596RgREoGN5jlAOsNzUYKBhK/6EYTWL4c+5qh3hSSrTcMPDPS0A5AxGssao/rOo41remg==',
size: 203435203
},
{
url: 'xxx-1.0.1-arm64.dmg',
sha512:
'dWtzvrTkWeAlGBjkVkAU8E9F+8uiBGEfFmmD/r6Syx0l8+1l/shNHQ9yLTUAz1dnnQuymX/DPQumyM17zsp8gg==',
size: 210364932
}
],
path: 'xxx-1.0.1-arm64-mac.zip',
sha512:
'w2WBWauiXCBa6QP13596RgREoGN5jlAOsNzUYKBhK/6EYTWL4c+5qh3hSSrTcMPDPS0A5AxG3bao/uOo41remg==',
releaseNotes: '<ul><li>更新了xxx</li><li>更新了xxx</li></ul>',
releaseDate: '2025-05-11T05:23:26.014Z',
downloadedFile:
'/Users/xxxx/Library/Caches/yourproject-updater/pending/xxx-1.0.0-arm64-mac.zip'
}
setTimeout(() => {
startInstall(res)
autoUpdater.checkForUpdates()
}, 5000)
结语
这种更新模式适合内部人员使用,不想频繁去链接上下载。