主题切换
第 9 章:自动更新与崩溃监控
应用发布后,你需要持续迭代和修复问题。本章将学习如何实现自动更新机制,让用户始终使用最新版本,以及集成崩溃监控系统,及时发现和修复生产环境的问题。
9.1 自动更新概述
Electron 应用不像 Web 应用那样刷新就能获得更新。你需要实现自动更新机制,在后台下载新版本并提示用户安装。electron-updater 是社区维护的自动更新库,支持 Windows(NSIS)、macOS(DMG)和 Linux(AppImage)。
给前端开发者的建议
自动更新就像前端项目的 CI/CD 流水线——只不过发布的目标是用户的桌面,而不是服务器。你需要考虑下载进度、更新策略、以及用户拒绝更新的处理。
更新流程
text
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ 检查更新 │───▶│ 发现新版本 │───▶│ 后台下载 │───▶│ 提示安装 │
└──────────┘ └──────────────┘ └──────────────┘ └──────────┘
│ │ │
▼ ▼ ▼
无更新 → 结束 下载失败 → 重试 用户取消 → 稍后提醒9.2 electron-updater 配置与实践
安装与基础配置
bash
npm install electron-updaterjavascript
const { autoUpdater } = require('electron-updater')
const { app, BrowserWindow, ipcMain } = require('electron')
function setupAutoUpdater(mainWindow) {
// 配置更新源(通常为 GitHub Releases)
autoUpdater.setFeedURL({
provider: 'github',
owner: 'your-username',
repo: 'your-repo-name',
// 可选:使用私有仓库
// token: process.env.GH_TOKEN,
})
// 发现新版本
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update:available', {
version: info.version,
releaseDate: info.releaseDate,
releaseNotes: info.releaseNotes,
})
})
// 下载进度
autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('update:progress', {
percent: Math.round(progress.percent),
transferred: progress.transferred,
total: progress.total,
})
})
// 下载完成
autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('update:downloaded', {
version: info.version,
})
})
// 发生错误
autoUpdater.on('error', (error) => {
mainWindow.webContents.send('update:error', {
message: error.message,
})
})
// 应用启动后自动检查更新
if (app.isPackaged) {
autoUpdater.checkForUpdatesAndNotify()
}
}
// 渲染进程可以通过 IPC 触发手动检查
ipcMain.handle('update:check', async () => {
try {
const result = await autoUpdater.checkForUpdates()
return { success: true, updateInfo: result.updateInfo }
} catch (err) {
return { success: false, error: err.message }
}
})
ipcMain.handle('update:install', () => {
// 退出并安装更新
autoUpdater.quitAndInstall()
})更新通知 Vue 组件
vue
<template>
<Teleport to="body">
<Transition name="slide-up">
<div v-if="visible" class="update-notification">
<div class="update-header">
<span class="update-title">发现新版本</span>
<button class="close-btn" @click="dismiss">✕</button>
</div>
<div class="update-body">
<p class="version-info">
新版本:<strong>v{{ updateVersion }}</strong>
</p>
<!-- 下载进度条 -->
<div v-if="isDownloading" class="download-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: downloadProgress + '%' }" />
</div>
<span class="progress-text">{{ downloadProgress }}%</span>
</div>
<!-- 操作按钮 -->
<div class="update-actions">
<template v-if="!isDownloading && !isDownloaded">
<button class="btn btn-primary" @click="startDownload">
立即更新
</button>
<button class="btn btn-secondary" @click="dismiss">
稍后提醒
</button>
</template>
<template v-if="isDownloaded">
<button class="btn btn-primary" @click="installUpdate">
重启并安装
</button>
</template>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
const visible = ref(false)
const updateVersion = ref('')
const isDownloading = ref(false)
const isDownloaded = ref(false)
const downloadProgress = ref(0)
function onUpdateAvailable(event, data) {
updateVersion.value = data.version
visible.value = true
}
function onUpdateProgress(event, data) {
isDownloading.value = true
downloadProgress.value = data.percent
}
function onUpdateDownloaded(event, data) {
isDownloading.value = false
isDownloaded.value = true
updateVersion.value = data.version
}
function onUpdateError(event, data) {
ElMessage.error(`更新失败: ${data.message}`)
visible.value = false
}
function startDownload() {
isDownloading.value = true
// electron-updater 在检测到更新后会自动开始下载
// 此处主要切换 UI 状态
}
function installUpdate() {
window.electronAPI.update.install()
}
function dismiss() {
visible.value = false
}
onMounted(() => {
window.electronAPI.on('update:available', onUpdateAvailable)
window.electronAPI.on('update:progress', onUpdateProgress)
window.electronAPI.on('update:downloaded', onUpdateDownloaded)
window.electronAPI.on('update:error', onUpdateError)
})
onUnmounted(() => {
window.electronAPI.off('update:available', onUpdateAvailable)
window.electronAPI.off('update:progress', onUpdateProgress)
window.electronAPI.off('update:downloaded', onUpdateDownloaded)
window.electronAPI.off('update:error', onUpdateError)
})
</script>
<style scoped>
.update-notification {
position: fixed;
bottom: 20px;
right: 20px;
width: 360px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-border);
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 1000;
}
.update-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.update-title {
font-weight: 600;
font-size: 16px;
}
.close-btn {
border: none;
background: none;
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 16px;
}
.version-info {
margin-bottom: 12px;
color: var(--vp-c-text-2);
}
.download-progress {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.progress-bar {
flex: 1;
height: 6px;
background: var(--vp-c-bg-mute);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #58a6ff, #3fb950);
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 13px;
color: var(--vp-c-text-2);
min-width: 36px;
}
.update-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 8px 16px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 13px;
font-weight: 500;
}
.btn-primary {
background: #58a6ff;
color: #fff;
}
.btn-primary:hover {
background: #4090e0;
}
.btn-secondary {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-1);
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from,
.slide-up-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>macOS 代码签名配合
在 macOS 上使用 electron-updater 需要配合代码签名:
javascript
// 需要在 electron-builder.yml 中配置 afterSign 钩子
// 用于 macOS 公证
mac:
hardenedRuntime: true
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
afterSign: 'scripts/notarize.js'javascript
// macOS 公证脚本
const { notarize } = require('@electron/notarize')
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') return
const appName = context.packager.appInfo.productFilename
return await notarize({
tool: 'notarytool',
appBundleId: 'com.yourcompany.yourapp',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
})
}差分更新(Blockmap)
electron-updater 默认支持差分更新(blockmap),即仅下载文件变更的部分而非完整安装包,显著减少下载量:
yaml
win:
target:
- target: nsis
arch: [x64]
nsis:
differentialPackage: true # 启用差分更新对于典型应用(安装包约 150MB),小版本更新的差分包通常仅 5-20MB。验证差分更新是否生效:
bash
# 构建后检查 dist 目录是否生成了 .blockmap 文件
ls dist/*.blockmap
# 预期输出:YourApp-1.0.0.exe.blockmap更新频道(Channel)
通过 channel 配置可以实现多发布频道(stable/beta/canary):
javascript
// beta 频道 — 仅检查标记为 beta 的发布
autoUpdater.channel = 'beta'
autoUpdater.setFeedURL({
provider: 'github',
owner: 'your-username',
repo: 'your-repo-name'
})
// 将检查 GitHub Releases 中包含 "beta" 标签的版本text
发布流程示例:
1. v1.1.0-beta.1 → 推送到 canary 频道(内部测试)
2. v1.1.0-beta.2 → 推送到 beta 频道(公开测试)
3. v1.1.0 → 推送到 stable 频道(全量发布)Sentry Source Maps 上传
生产环境堆栈追踪的可读性依赖 source maps。配合 @sentry/vite-plugin 自动上传:
bash
npm install --save-dev @sentry/vite-plugintypescript
import { sentryVitePlugin } from '@sentry/vite-plugin'
export default defineConfig({
renderer: {
plugins: [
vue(),
sentryVitePlugin({
org: 'your-org',
project: 'your-project',
authToken: process.env.SENTRY_AUTH_TOKEN,
release: {
name: process.env.npm_package_version
},
// Source maps 上传后自动删除
sourcemaps: {
filesToDeleteAfterUpload: ['**/*.js.map']
}
})
]
}
})常见更新问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 检查更新无响应 | feed URL 配置错误 | 确认 GitHub repo owner/name 正确,手动 GET https://api.github.com/repos/{owner}/{repo}/releases/latest 验证 |
| 下载完成后未安装 | 代码签名不匹配 | Windows 需 EV/OV 证书签名;macOS 需 Apple Developer ID |
| 一直显示"已是最新" | 本地版本号 > 远程版本 | 检查 package.json 的 version 字段是否正确 |
| 网络代理环境失败 | 代理配置未传递 | 设置 autoUpdater.netSession 或使用系统代理 |
9.3 更新策略
根据产品需求,可以选择不同的更新策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 强制更新 | 发现新版本必须更新才能使用 | 安全修复、重大 Bug |
| 可选更新 | 提示用户,用户可选择稍后 | 功能更新、优化 |
| 静默更新 | 后台下载,下次启动时自动安装 | 小修复、非紧急更新 |
| 手动检查 | 用户点击"检查更新"按钮 | 节省服务器资源 |
强制更新实现
javascript
autoUpdater.on('update-available', (info) => {
// 判断是否为强制更新(根据版本号或服务器配置)
const isForceUpdate = info.releaseNotes?.includes('[FORCE]')
mainWindow.webContents.send('update:available', {
version: info.version,
force: isForceUpdate
})
if (isForceUpdate) {
// 强制更新:立即开始下载,不提供"稍后"选项
autoUpdater.downloadUpdate()
}
})
autoUpdater.on('update-downloaded', () => {
// 强制更新:立即退出并安装
autoUpdater.quitAndInstall()
})9.4 崩溃监控(Sentry 集成)
生产环境的应用难免会出现崩溃和错误。集成 Sentry 可以实时监控应用健康状况,收集崩溃报告和错误日志。
安装 Sentry SDK
bash
npm install @sentry/electron主进程配置
javascript
const Sentry = require('@sentry/electron/main')
Sentry.init({
dsn: 'https://your-dsn@sentry.io/project-id',
// 环境标识
environment: app.isPackaged ? 'production' : 'development',
// 发布版本(用于关联源码映射)
release: `rpa-assistant@${app.getVersion()}`,
// 采样率(生产环境建议 1.0)
sampleRate: 1.0,
// 启用性能监控
tracesSampleRate: 0.1,
// 附加数据
initialScope: {
tags: {
platform: process.platform,
arch: process.arch,
},
},
})
// 手动捕获异常
try {
riskyOperation()
} catch (error) {
Sentry.captureException(error)
}
// 添加面包屑(操作轨迹)
Sentry.addBreadcrumb({
category: 'user-action',
message: '用户点击了开始录制按钮',
level: 'info',
})渲染进程配置
javascript
import * as Sentry from '@sentry/electron/renderer'
Sentry.init({
dsn: 'https://your-dsn@sentry.io/project-id',
// 启用 Vue 集成
integrations: [
Sentry.vueIntegration({
app, // Vue 应用实例
attachProps: true,
}),
],
// 跟踪 Vue 组件性能
tracesSampleRate: 0.1,
})
// 在 Vue 组件中使用
function handleError() {
Sentry.captureException(new Error('用户操作失败'))
}
// 设置用户信息
Sentry.setUser({
id: 'user-123',
username: '张三',
email: 'zhangsan@example.com',
})9.5 日志收集与分析
除了崩溃监控,还需要收集应用运行日志,帮助诊断用户遇到的问题。
javascript
const log = require('electron-log')
const fs = require('fs').promises
const path = require('path')
const { app, dialog } = require('electron')
class LogManager {
constructor() {
this.logDir = app.getPath('logs')
this.setupLogTransport()
}
setupLogTransport() {
// 文件日志
log.transports.file.resolvePath = () =>
path.join(this.logDir, 'app.log')
log.transports.file.maxSize = 10 * 1024 * 1024 // 10MB
// 控制台日志(仅开发环境)
log.transports.console.level = app.isPackaged ? false : 'debug'
}
// 获取最近的日志内容
async getRecentLogs(lines = 100) {
try {
const logPath = path.join(this.logDir, 'app.log')
const content = await fs.readFile(logPath, 'utf-8')
return content.split('\n').slice(-lines).join('\n')
} catch (err) {
return '无法读取日志文件'
}
}
// 上传日志到服务器
async uploadLogs() {
try {
const logs = await this.getRecentLogs(500)
const response = await fetch('https://your-api.com/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
version: app.getVersion(),
platform: process.platform,
logs,
timestamp: new Date().toISOString(),
}),
})
if (response.ok) {
log.info('日志上传成功')
return true
}
} catch (err) {
log.error('日志上传失败:', err)
return false
}
}
// 导出日志文件(供用户手动发送)
async exportLogs() {
const result = await dialog.showSaveDialog({
defaultPath: `logs-${Date.now()}.txt`,
filters: [{ name: '日志文件', extensions: ['txt'] }],
})
if (!result.canceled) {
const logs = await this.getRecentLogs(1000)
await fs.writeFile(result.filePath, logs, 'utf-8')
return result.filePath
}
}
}
module.exports = { LogManager }9.6 性能监控
监控应用性能指标,及时发现性能瓶颈。
javascript
const { app } = require('electron')
class PerformanceMonitor {
constructor() {
this.metrics = {
startupTime: 0,
memoryUsage: [],
}
this.startMonitoring()
}
// 记录启动时间
recordStartupTime() {
this.metrics.startupTime = Date.now() - global.appStartTime
console.log(`应用启动时间: ${this.metrics.startupTime}ms`)
}
// 监控内存使用
startMonitoring() {
setInterval(() => {
const usage = process.memoryUsage()
this.metrics.memoryUsage.push({
timestamp: Date.now(),
rss: Math.round(usage.rss / 1024 / 1024), // MB
heapUsed: Math.round(usage.heapUsed / 1024 / 1024),
heapTotal: Math.round(usage.heapTotal / 1024 / 1024),
})
// 只保留最近 100 条记录
if (this.metrics.memoryUsage.length > 100) {
this.metrics.memoryUsage.shift()
}
// 内存告警(超过 500MB)
if (usage.rss > 500 * 1024 * 1024) {
console.warn('内存使用过高:', Math.round(usage.rss / 1024 / 1024), 'MB')
}
}, 30000) // 每 30 秒采样一次
}
// 获取性能报告
getReport() {
const memStats = this.metrics.memoryUsage
const avgMemory = memStats.length > 0
? memStats.reduce((sum, m) => sum + m.rss, 0) / memStats.length
: 0
return {
startupTime: this.metrics.startupTime,
averageMemoryMB: Math.round(avgMemory),
maxMemoryMB: memStats.length > 0
? Math.max(...memStats.map(m => m.rss))
: 0,
platform: process.platform,
electronVersion: process.versions.electron,
nodeVersion: process.versions.node,
}
}
}
module.exports = { PerformanceMonitor }本章小结
本章涵盖了 Electron 应用运维的核心能力:
- 自动更新:使用
electron-updater实现跨平台自动更新,支持更新进度通知和强制/可选/静默三种更新策略 - 更新 UI:通过 IPC 将更新状态同步到渲染进程,提供下载进度条和重启安装交互
- 崩溃监控:集成
@sentry/electron,覆盖主进程和渲染进程(含 Vue 集成),收集异常和面包屑 - 日志系统:使用
electron-log构建分级日志,支持上传和导出,帮助诊断用户问题 - 性能监控:监控启动时间、内存使用等关键指标,设置告警阈值
- macOS 签名:配合
afterSign钩子和公证脚本,确保更新机制在 macOS 上正常运行
运维最佳实践
- 版本追踪:每个错误报告都要包含应用版本号,便于定位问题
- 分级告警:根据错误频率和影响范围设置不同级别的告警
- 灰度发布:新版本先小范围推送,确认稳定后再全量发布
- 回滚机制:保留旧版本安装包,紧急情况下可快速回滚
- 用户反馈:提供"发送反馈"功能,让用户主动报告问题
下一章我们将综合运用全部所学,从零构建一个 RPA 桌面助手——这是整个教程的集大成之作。