Реализация автообновления (Auto-Update) десктоп-приложения
Автообновление — критичная функция для десктоп-приложений. Без неё пользователи месяцами работают со старыми версиями. С плохой реализацией — теряют данные или получают сломанные обновления. Рассмотрим реализацию для Electron и Tauri.
Electron: electron-updater
electron-updater (из пакета electron-builder) — стандартный инструмент для обновлений в Electron. Поддерживает GitHub Releases, S3, собственный сервер.
npm install electron-updater
// main/updater.js
const { autoUpdater } = require('electron-updater');
const { app, BrowserWindow } = require('electron');
const log = require('electron-log');
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';
// Не загружать и устанавливать обновление автоматически
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
function setupAutoUpdater(mainWindow) {
autoUpdater.on('checking-for-update', () => {
mainWindow.webContents.send('update:checking');
});
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update:available', {
version: info.version,
releaseNotes: info.releaseNotes,
releaseDate: info.releaseDate
});
});
autoUpdater.on('update-not-available', () => {
mainWindow.webContents.send('update:not-available');
});
autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('update:progress', {
percent: Math.round(progress.percent),
transferred: progress.transferred,
total: progress.total,
bytesPerSecond: progress.bytesPerSecond
});
});
autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('update:downloaded', {
version: info.version
});
});
autoUpdater.on('error', (error) => {
mainWindow.webContents.send('update:error', error.message);
log.error('Updater error:', error);
});
// IPC handlers
const { ipcMain } = require('electron');
ipcMain.handle('updater:check', () => autoUpdater.checkForUpdates());
ipcMain.handle('updater:download', () => autoUpdater.downloadUpdate());
ipcMain.handle('updater:install', () => {
autoUpdater.quitAndInstall(false, true); // isSilent, isForceRunAfter
});
// Проверять обновления каждые 4 часа
setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000);
// Проверить при старте (после небольшой задержки)
setTimeout(() => autoUpdater.checkForUpdates(), 10000);
}
module.exports = { setupAutoUpdater };
Конфигурация для GitHub Releases
# electron-builder.yml
publish:
provider: github
owner: your-github-username
repo: your-repo-name
private: false
При сборке electron-builder создаёт файл latest.yml (Windows/Linux) или latest-mac.yml (macOS) с метаданными версии. autoUpdater читает эти файлы для проверки обновлений.
Релиз в GitHub:
# Сборка и публикация в GitHub Releases
GH_TOKEN=$GITHUB_TOKEN npm run build -- --publish always
Конфигурация для собственного сервера обновлений
Если не хотите GitHub:
# electron-builder.yml
publish:
provider: generic
url: https://updates.your-domain.com/releases/
Сервер должен отдавать latest.yml и бинарники. Структура:
/releases/
latest.yml
latest-mac.yml
MyApp-1.2.0-setup.exe
MyApp-1.2.0.dmg
MyApp-1.2.0.AppImage
Формат latest.yml:
version: 1.2.0
files:
- url: MyApp-1.2.0-setup.exe
sha512: <hash>
size: 75423821
path: MyApp-1.2.0-setup.exe
sha512: <hash>
releaseDate: '2025-03-28T10:00:00.000Z'
UI компонент обновления в renderer
// renderer/components/UpdateNotification.tsx
import { useEffect, useState } from 'react';
type UpdateState =
| { status: 'idle' }
| { status: 'checking' }
| { status: 'available'; version: string; releaseNotes: string }
| { status: 'downloading'; percent: number }
| { status: 'ready'; version: string }
| { status: 'error'; message: string };
export function UpdateNotification() {
const [state, setState] = useState<UpdateState>({ status: 'idle' });
useEffect(() => {
const unsubscribers = [
window.electronAPI.onUpdateChecking(() => setState({ status: 'checking' })),
window.electronAPI.onUpdateAvailable((info) =>
setState({ status: 'available', version: info.version, releaseNotes: info.releaseNotes ?? '' })
),
window.electronAPI.onUpdateProgress((p) =>
setState({ status: 'downloading', percent: p.percent })
),
window.electronAPI.onUpdateDownloaded((info) =>
setState({ status: 'ready', version: info.version })
),
window.electronAPI.onUpdateError((msg) =>
setState({ status: 'error', message: msg })
),
];
return () => unsubscribers.forEach(fn => fn?.());
}, []);
if (state.status === 'idle' || state.status === 'checking') return null;
if (state.status === 'available') {
return (
<div className="update-banner">
<span>Доступна версия {state.version}</span>
<button onClick={() => window.electronAPI.downloadUpdate()}>
Загрузить
</button>
</div>
);
}
if (state.status === 'downloading') {
return (
<div className="update-banner">
<span>Загрузка обновления: {state.percent}%</span>
<div className="progress-bar" style={{ width: `${state.percent}%` }} />
</div>
);
}
if (state.status === 'ready') {
return (
<div className="update-banner update-ready">
<span>Версия {state.version} загружена</span>
<button onClick={() => window.electronAPI.installUpdate()}>
Перезапустить и установить
</button>
</div>
);
}
return null;
}
Tauri: встроенное автообновление
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-updater = "2"
// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::default().build())
.invoke_handler(tauri::generate_handler![check_for_updates])
.run(tauri::generate_context!())
.expect("error running app");
}
#[tauri::command]
async fn check_for_updates(app: tauri::AppHandle) -> Result<(), String> {
let updater = app.updater().map_err(|e| e.to_string())?;
let response = updater.check().await.map_err(|e| e.to_string())?;
if let Some(update) = response {
app.emit("update:available", &update.version).unwrap();
// Скачиваем с прогрессом
let mut downloaded = 0u64;
update.download_and_install(
|chunk, total| {
downloaded += chunk as u64;
if let Some(total) = total {
let percent = (downloaded * 100 / total) as u8;
app.emit("update:progress", percent).unwrap();
}
},
|| { app.emit("update:installed", ()).unwrap(); }
).await.map_err(|e| e.to_string())?;
}
Ok(())
}
// src-tauri/tauri.conf.json
{
"plugins": {
"updater": {
"pubkey": "YOUR_PUBLIC_KEY",
"endpoints": ["https://updates.your-domain.com/{{target}}/{{arch}}/{{current_version}}"]
}
}
}
Tauri требует подпись обновлений — нельзя установить неподписанный апдейт. Генерация ключей: tauri signer generate -w ~/.tauri/myapp.key.
Обновление без перезапуска
Полностью бесшовное обновление (без перезапуска) технически невозможно для бинарника, который работает. Но можно минимизировать неудобство:
- Скачивать обновление в фоне, не беспокоя пользователя
- Предлагать установку при следующем закрытии приложения (
autoInstallOnAppQuit: true) - Показывать нотификацию «обновление готово» в трее
- Сохранять состояние приложения перед перезапуском и восстанавливать после







