说明
版本1.2.3,1变化为全量更新,会卸载重装(这里为显式安装,可以修改为静默安装),2和3变化走的增量更新,将win-unpacked\resources\app下的文件(如果没有动依赖,可以忽略node_modules)打包成app.zip,放到更新服务器。无论全量还是增量,都要将latest.yml放到更新服务器,全量需要放exe,增量可以只放zip。
使用
1.npm i -S electron-updater electron-log lodash uuid request,npm i -D electron
2.7z.exe与7z.dll放到项目根目录
3.静态资源服务器,https://myupdate.com
4.配置electron-builder
module.exports = {
asar: false,
extraFiles: ['7z.dll', '7z.exe'],
win: {
target: 'nsis',
},
publish: {
provider: 'generic',
url: 'https://myupdate.com',
},
}
5.主进程代码
import { BrowserWindow } from 'electron';
import upgrade from 'upgrade.ts';
const window = new BrowserWindow();
upgrade('https://myupdate.com', window)
upgrade.ts
import type { BrowserWindow } from 'electron';
import type { UpdateInfo } from 'electron-updater';
import { throttle } from 'lodash';
import { v4 } from 'uuid';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { execFile } from 'node:child_process';
import { app, dialog } from 'electron';
import { NsisUpdater } from 'electron-updater';
import * as ElectronLog from 'electron-log';
const APP_INST_DIR = app.isPackaged ? path.resolve(app.getAppPath(), '../..') : app.getAppPath();
const EXE_7Z = path.resolve(APP_INST_DIR, '7z.exe');
let mainWindow: BrowserWindow;
let hotUpdate: boolean;
/**
* 下载热更新包,zip包
* @param {string} url - 下载地址
* @returns {Promise<string>} - 下载完成后的文件路径
*/
function downloadZip(url: string) {
return new Promise<string>(async (resolve, reject) => {
let total = 0,
progress = 0;
const setProgressBar = throttle(() => {
const value = ((progress / total) * 100).toFixed(2);
mainWindow.title = `下载中...${value}%`;
mainWindow && mainWindow.setProgressBar(progress / total);
}, 100);
try {
const request = await import('request');
const zip = path.resolve(app.getPath('temp'), `${v4()}.zip`);
request
.default(url)
.on('error', (error: Error) => {
reject(error);
})
.on('close', () => {
resolve(zip);
})
.on('response', ({ headers }) => {
total = Number(headers['content-length']);
})
.on('data', (data) => {
progress += data.length;
setProgressBar();
})
.pipe(fs.createWriteStream(zip));
} catch (error) {
reject(error);
}
});
}
function replaceResources(zip: string, target: string) {
return new Promise<void>((resolve, reject) => {
if (!fs.existsSync(EXE_7Z)) {
reject(new Error(`${EXE_7Z}不存在`));
return;
}
if (!mainWindow.isDestroyed()) {
mainWindow.title = '解压中...';
mainWindow.setProgressBar(2);
}
try {
execFile(EXE_7Z, ['x', '-aoa', `-o${target}`, zip], { windowsHide: true }, (error) => {
if (!mainWindow.isDestroyed()) {
mainWindow.title = path.basename(app.getPath('exe')).replace('.exe', '');
mainWindow.setProgressBar(-1);
}
if (error) {
reject(error);
return;
}
resolve();
});
} catch (error) {
if (!mainWindow.isDestroyed()) {
mainWindow.title = path.basename(app.getPath('exe')).replace('.exe', '');
mainWindow.setProgressBar(-1);
}
reject(error);
}
});
}
function installAfterQuit(file: string) {
app.once('before-quit', (event) => {
ElectronLog.info('app.onBeforeQuit event.preventDefault');
event.preventDefault();
try {
dialog.showMessageBoxSync(mainWindow, {
title: '版本更新',
message: '应用程序将在后台执行更新,完成后会自动退出程序,请勿强制关闭程序',
type: 'info',
});
} catch (error) {
//
}
replaceResources(file, app.getAppPath())
.then(() => {
ElectronLog.info('热更新稍后重启替换资源成功,退出程序');
})
.catch(() => {
ElectronLog.info('热更新稍后重启替换资源错误');
})
.finally(() => {
app.quit();
});
});
}
function installAndRelaunch(file: string) {
try {
dialog.showMessageBoxSync(mainWindow, {
message: '应用程序将在后台执行更新,完成后会自动重启程序,请勿强制关闭程序',
title: '版本更新',
type: 'info',
});
} catch (error) {
//
}
replaceResources(file, app.getAppPath())
.then(() => {
ElectronLog.error('立即更新替换资源成功');
})
.catch((error) => {
ElectronLog.error(`立即更新替换资源失败,${error.message}`);
})
.finally(() => {
app.relaunch();
app.quit();
});
}
function dialogUpdateAvailable(info: UpdateInfo) {
return new Promise<boolean>((resolve) => {
dialog
.showMessageBox(mainWindow, {
title: '版本更新',
message: '有新版本发布,是否立即下载?',
detail: `当前版本:${app.getVersion()},最新版本:${info.version}`,
type: 'info',
buttons: ['取消更新', '立即下载'],
defaultId: 1,
})
.then(({ response }) => {
resolve(response === 1);
})
.catch(() => {
resolve(false);
});
});
}
function dialogUpdateDownloaded() {
return new Promise<boolean>((resolve) => {
dialog
.showMessageBox(mainWindow, {
title: '版本更新',
message: '新版本已下载完毕,是否立即重启?',
type: 'info',
buttons: ['稍后重启', '立即重启'],
defaultId: 1,
})
.then(({ response }) => {
resolve(response === 1);
})
.catch(() => {
resolve(false);
});
});
}
export default function (url: string, win: BrowserWindow) {
mainWindow = win;
const autoUpdater = new NsisUpdater({
url,
provider: 'generic',
});
autoUpdater.autoDownload = false;
autoUpdater.installDirectory = APP_INST_DIR;
// 开始检查更新
autoUpdater.on('checking-for-update', () => {
ElectronLog.info('autoUpdater.onCheckingForUpdate');
});
// 检查更新出错
autoUpdater.on('error', (error, message) => {
dialog.showMessageBox(win, {
title: '更新失败',
message: error.message,
detail: message,
type: 'error',
});
ElectronLog.info('autoUpdater.onError', error.message, message);
});
// 检查到新版本
autoUpdater.on('update-available', async (info: UpdateInfo) => {
ElectronLog.info(`autoUpdater.onUpdateAvailable,${info.version}`);
ElectronLog.info(info);
if (Number(info.version.split('.')[0]) > Number(app.getVersion().split('.')[0])) {
ElectronLog.info('全量更新');
hotUpdate = false;
} else {
ElectronLog.info('增量更新');
hotUpdate = true;
}
const isDownload = await dialogUpdateAvailable(info);
if (!isDownload) {
ElectronLog.info('取消更新');
return;
}
ElectronLog.info('立即下载');
if (hotUpdate) {
const packUrl = `${url}/app.zip`;
downloadZip(packUrl)
.then(async (file) => {
autoUpdater.emit('update-downloaded', {
downloadedFile: file,
version: '',
files: [],
path: '',
sha512: '',
releaseDate: '',
});
})
.catch((reason) => {
ElectronLog.error(`下载热更新包失败,${reason.message}`);
dialog
.showMessageBox(win, {
title: '版本更新',
message: '新版本下载失败,请检查网络是否正常',
detail: `${reason.message}`,
type: 'error',
})
.then(() => 0)
.catch(() => 0);
});
} else {
autoUpdater.downloadUpdate();
}
});
// 已经是新版本
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
ElectronLog.info(`autoUpdater.onUpdateNotAvailable,${JSON.stringify(info)}`);
});
// 更新下载中
autoUpdater.on('download-progress', (info) => {
const value = info.percent.toFixed(2);
mainWindow.setProgressBar(info.percent / 100);
mainWindow.title = `下载中...${value}%`;
});
// 更新下载完毕
autoUpdater.on('update-downloaded', async (event) => {
ElectronLog.info(`autoUpdater.onUpdateDownloaded,${JSON.stringify(event)}`);
mainWindow.title = app.getPath('exe').replace('.exe', '');
const isRelaunch = await dialogUpdateDownloaded();
if (isRelaunch) {
if (hotUpdate) {
installAndRelaunch(event.downloadedFile);
} else {
autoUpdater.quitAndInstall(false, false);
}
} else {
if (hotUpdate) {
installAfterQuit(event.downloadedFile);
} else {
autoUpdater.autoInstallOnAppQuit = true;
}
}
});
autoUpdater.checkForUpdates();
}