Electron

第五章:更新最佳实践

第五章:更新最佳实践

目录


灰度发布完整实现

灰度策略

灰度发布(Gradual Rollout / Canary Release)是控制新版本影响范围的核心手段。

灰度发布策略分类:

  1. 百分比灰度
     ├── 10% 用户先收到更新
     ├── 确认无异常后扩大到 50%
     └── 最终 100% 全量

  2. 白名单灰度
     ├── 内部员工先用
     ├── 种子用户群体
     └── 然后全量

  3. 渠道灰度
     ├── canary 渠道(每日构建,内部测试)
     ├── beta 渠道(预发布,外部测试)
     └── stable 渠道(正式版,全量用户)

  4. 条件灰度
     ├── 特定操作系统(如先推 macOS)
     ├── 特定地区
     └── 特定用户属性

服务端实现

// update-server.js — 灰度发布服务

class GradualRollout {
  constructor() {
    this.config = {
      currentStable: '1.2.5',
      rollout: {
        version: '1.3.0',
        startedAt: '2024-01-20T00:00:00Z',
        rules: [
          // 规则按优先级排列
          {
            type: 'whitelist',
            users: ['user-001', 'user-002', 'dev-team'],
            enabled: true,
          },
          {
            type: 'percentage',
            value: 30,        // 30% 的用户
            enabled: true,
          },
          {
            type: 'platform',
            platforms: ['darwin'],  // 先推 macOS
            enabled: false,
          }
        ],
        paused: false,        // 紧急暂停开关
        metrics: {
          crashRate: 0.02,    // 当前崩溃率
          maxCrashRate: 0.05, // 超过此值自动暂停
        }
      }
    }
  }

  // 判断用户是否应该收到更新
  shouldUpdate(userId, platform, currentVersion) {
    const { rollout } = this.config
    
    // 紧急暂停
    if (rollout.paused) return false
    
    // 已经是最新版或更新版
    if (this.compareVersions(currentVersion, rollout.version) >= 0) return false
    
    // 检查崩溃率
    if (rollout.metrics.crashRate > rollout.metrics.maxCrashRate) {
      console.warn('崩溃率超标,自动暂停灰度')
      rollout.paused = true
      this.alertOps('灰度自动暂停:崩溃率超标')
      return false
    }
    
    // 按规则匹配
    for (const rule of rollout.rules) {
      if (!rule.enabled) continue
      
      switch (rule.type) {
        case 'whitelist':
          if (rule.users.includes(userId)) return true
          break
          
        case 'percentage':
          if (this.getUserBucket(userId) < rule.value) return true
          break
          
        case 'platform':
          if (rule.platforms.includes(platform)) return true
          break
      }
    }
    
    return false
  }

  // 用户分桶(确保同一用户每次得到相同结果)
  getUserBucket(userId) {
    const hash = crypto.createHash('md5').update(userId).digest('hex')
    return parseInt(hash.substring(0, 8), 16) % 100
  }

  // 版本比较
  compareVersions(v1, v2) {
    const a = v1.split('.').map(Number)
    const b = v2.split('.').map(Number)
    for (let i = 0; i < 3; i++) {
      if (a[i] > b[i]) return 1
      if (a[i] < b[i]) return -1
    }
    return 0
  }

  alertOps(message) {
    // 发送告警到运维渠道
    console.error('[ALERT]', message)
  }
}

// API 端点
app.get('/api/check-update', (req, res) => {
  const { userId, platform, version } = req.query
  const rollout = new GradualRollout()
  
  if (rollout.shouldUpdate(userId, platform, version)) {
    res.json({
      hasUpdate: true,
      version: rollout.config.rollout.version,
      url: `https://cdn.yourapp.com/releases/v${rollout.config.rollout.version}`,
    })
  } else {
    res.json({ hasUpdate: false })
  }
})

灰度推进流程

灰度推进流程(典型 5 天):

  Day 0: 内部测试
  ┌────────────────────────────────────┐
  │  白名单: 开发团队 + QA (20人)     │
  │  观察: 功能正确性、崩溃率         │
  └────────────────────────────────────┘
           │ OK?

  Day 1: 种子用户
  ┌────────────────────────────────────┐
  │  百分比: 5%                        │
  │  观察: 崩溃率 < 0.5%、用户反馈    │
  └────────────────────────────────────┘
           │ OK?

  Day 2: 扩大范围
  ┌────────────────────────────────────┐
  │  百分比: 30%                       │
  │  观察: 性能指标、错误日志         │
  └────────────────────────────────────┘
           │ OK?

  Day 3-4: 大范围推送
  ┌────────────────────────────────────┐
  │  百分比: 70%                       │
  └────────────────────────────────────┘
           │ OK?

  Day 5: 全量
  ┌────────────────────────────────────┐
  │  百分比: 100%                      │
  │  更新 stable 版本号               │
  └────────────────────────────────────┘

  任何阶段发现问题:
  ┌────────────────────────────────────┐
  │  暂停灰度 (paused: true)          │
  │  分析问题                         │
  │  修复后重新开始灰度               │
  │  或回滚到上一个 stable 版本       │
  └────────────────────────────────────┘

差分更新原理

bsdiff 算法

bsdiff 二进制差分算法:

  原理:
  1. 对旧文件进行后缀排序
  2. 找到新文件和旧文件之间的最长公共子序列
  3. 生成 diff 文件(只包含差异部分)

  流程:
  旧文件 (old.bin, 100MB)
  新文件 (new.bin, 102MB)


  bsdiff old.bin new.bin patch.bin


  补丁文件 (patch.bin, 5MB)  ← 只有差异部分


  bspatch old.bin new.bin patch.bin  ← 客户端合成


  恢复出完整的 new.bin

  优势:
  - 对可执行文件特别有效(通常压缩到 5-20%)
  - 成熟的算法,被 Chrome、Firefox 使用

  劣势:
  - 需要旧文件参与合成
  - 合成过程 CPU 密集
  - 客户端必须有完整的旧版本

blockmap(electron-updater 使用的方案)

blockmap 差分更新原理:

  将文件分割为固定大小的块(默认 64KB):

  旧版本:
  ┌──────┬──────┬──────┬──────┬──────┐
  │ B1   │ B2   │ B3   │ B4   │ B5   │
  │ h:a1 │ h:b2 │ h:c3 │ h:d4 │ h:e5 │
  └──────┴──────┴──────┴──────┴──────┘

  新版本:
  ┌──────┬──────┬──────┬──────┬──────┬──────┐
  │ B1   │ B2'  │ B3   │ B4   │ B5   │ B6   │
  │ h:a1 │ h:f6 │ h:c3 │ h:d4 │ h:e5 │ h:g7 │
  └──────┴──────┴──────┴──────┴──────┴──────┘

  对比:
  B1: a1==a1  → 跳过 ✓
  B2: b2!=f6  → 下载 ↓ (64KB)
  B3: c3==c3  → 跳过 ✓
  B4: d4==d4  → 跳过 ✓
  B5: e5==e5  → 跳过 ✓
  B6: (新增)  → 下载 ↓ (64KB)

  总下载: 128KB 而非 384KB (6 blocks × 64KB)
  节省: 67%

  blockmap 文件格式:
  {
    "version": 2,
    "files": [{
      "name": "MyApp-Setup-1.1.0.exe",
      "offset": 0,
      "checksums": [
        "a1a1a1...",   // B1 的 hash
        "f6f6f6...",   // B2' 的 hash
        "c3c3c3...",   // B3 的 hash
        ...
      ],
      "sizes": [65536, 65536, 65536, ...]
    }]
  }

强制更新策略

最低版本号机制

// 强制更新的完整实现

class ForceUpdateManager {
  constructor(mainWindow) {
    this.mainWindow = mainWindow
    this.checkInterval = null
  }

  async check() {
    try {
      // 从服务器获取最低要求版本
      const response = await fetch('https://api.yourapp.com/min-version')
      const { minVersion, message, severity } = await response.json()
      
      // severity: 'critical' | 'recommended' | 'optional'
      
      const currentVersion = app.getVersion()
      
      if (this.isOlderThan(currentVersion, minVersion)) {
        switch (severity) {
          case 'critical':
            // 安全漏洞等:强制更新,无法跳过
            this.showCriticalUpdate(message)
            break
          case 'recommended':
            // 重要更新:强烈推荐,但可延迟
            this.showRecommendedUpdate(message)
            break
          case 'optional':
            // 可选更新:温和提醒
            this.showOptionalUpdate(message)
            break
        }
      }
    } catch (err) {
      console.error('检查强制更新失败:', err)
    }
  }

  showCriticalUpdate(message) {
    // 替换窗口内容为更新界面
    this.mainWindow.loadFile('force-update.html')
    
    // 禁止关闭(除非通过任务管理器)
    this.mainWindow.on('close', (event) => {
      event.preventDefault()
      dialog.showMessageBoxSync(this.mainWindow, {
        type: 'warning',
        title: '需要更新',
        message: '当前版本存在安全问题,必须更新后才能继续使用。',
        buttons: ['知道了'],
      })
    })
    
    // 自动开始下载
    autoUpdater.checkForUpdatesAndNotify()
    
    autoUpdater.on('update-downloaded', () => {
      // 倒计时 10 秒自动安装
      setTimeout(() => {
        autoUpdater.quitAndInstall(false, true)
      }, 10 * 1000)
    })
  }

  showRecommendedUpdate(message) {
    const result = dialog.showMessageBoxSync(this.mainWindow, {
      type: 'info',
      title: '推荐更新',
      message: message || '有重要更新可用',
      buttons: ['立即更新', '稍后提醒'],
      defaultId: 0,
    })
    
    if (result === 0) {
      autoUpdater.checkForUpdatesAndNotify()
    } else {
      // 24 小时后再提醒
      setTimeout(() => this.check(), 24 * 60 * 60 * 1000)
    }
  }

  showOptionalUpdate(message) {
    // 只显示非侵入式通知
    new Notification({
      title: '有新版本可用',
      body: message || '点击查看更新详情',
    }).show()
  }

  isOlderThan(v1, v2) {
    const a = v1.split('.').map(Number)
    const b = v2.split('.').map(Number)
    for (let i = 0; i < 3; i++) {
      if (a[i] < b[i]) return true
      if (a[i] > b[i]) return false
    }
    return false
  }
}

错误回滚机制

多层回滚策略

回滚机制全景:

  Layer 1: 自动回滚(崩溃检测)
  ┌──────────────────────────────────────────────────────┐
  │  新版本启动后 30 秒内崩溃 2 次 → 自动恢复旧版本    │
  │  实现: sentinel 文件 + 崩溃计数                      │
  └──────────────────────────────────────────────────────┘

  Layer 2: 用户触发回滚
  ┌──────────────────────────────────────────────────────┐
  │  用户在设置中手动选择 "回退到上一个版本"             │
  │  实现: 保留旧版本文件 + 一键切换                     │
  └──────────────────────────────────────────────────────┘

  Layer 3: 服务端回滚
  ┌──────────────────────────────────────────────────────┐
  │  运维将 latest.yml 指向旧版本                       │
  │  所有用户下次检查更新时 "更新" 到旧版本             │
  │  实现: 修改更新服务器配置                            │
  └──────────────────────────────────────────────────────┘

  Layer 4: 紧急热修复
  ┌──────────────────────────────────────────────────────┐
  │  不回滚,而是快速发布修复版本                       │
  │  通过强制更新推送到所有用户                         │
  │  实现: 紧急发布流程 + 强制更新                       │
  └──────────────────────────────────────────────────────┘

自动回滚实现

// auto-rollback.js

const SENTINEL_PATH = path.join(app.getPath('userData'), '.update-sentinel')
const MAX_CRASHES = 2
const STABILITY_WINDOW = 30000 // 30秒

class AutoRollback {
  constructor(bundleManager) {
    this.bm = bundleManager
  }

  onAppStart() {
    const sentinel = this.readSentinel()
    
    if (!sentinel) return // 没有待验证的更新
    
    if (sentinel.status === 'verifying') {
      sentinel.launches = (sentinel.launches || 0) + 1
      
      if (sentinel.launches > MAX_CRASHES) {
        // 连续崩溃太多次,回滚
        console.error('检测到更新后连续崩溃,执行回滚')
        this.performRollback(sentinel)
        return
      }
      
      // 更新启动计数
      this.writeSentinel(sentinel)
      
      // 启动稳定性计时器
      setTimeout(() => {
        // 如果 30 秒后还在运行,标记为稳定
        sentinel.status = 'stable'
        sentinel.launches = 0
        this.writeSentinel(sentinel)
        console.log('更新验证通过,版本稳定')
        
        // 清理旧版本备份
        this.bm.cleanup(2)
      }, STABILITY_WINDOW)
    }
  }

  onUpdateApplied(version) {
    this.writeSentinel({
      version,
      status: 'verifying',
      launches: 0,
      appliedAt: Date.now(),
    })
  }

  performRollback(sentinel) {
    console.warn(`回滚: v${sentinel.version} → 上一个版本`)
    
    const previousPath = this.bm.rollback()
    
    // 清除 sentinel
    fs.unlinkSync(SENTINEL_PATH)
    
    // 上报回滚事件
    this.reportRollback(sentinel)
    
    // 重启应用
    app.relaunch()
    app.exit(0)
  }

  reportRollback(sentinel) {
    // 上报到监控系统
    fetch('https://api.yourapp.com/telemetry/rollback', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        version: sentinel.version,
        launches: sentinel.launches,
        platform: process.platform,
        arch: process.arch,
      })
    }).catch(() => {})
  }

  readSentinel() {
    try {
      return JSON.parse(fs.readFileSync(SENTINEL_PATH, 'utf-8'))
    } catch {
      return null
    }
  }

  writeSentinel(data) {
    fs.writeFileSync(SENTINEL_PATH, JSON.stringify(data))
  }
}

用户体验设计

更新 UI 设计原则

好的更新体验:
┌─────────────────────────────────────────────────────┐
│                                                      │
│  1. 不打断用户工作流                                │
│     ✅ 后台下载,完成后温和通知                     │
│     ❌ 弹窗阻断,强制用户立即决定                   │
│                                                      │
│  2. 透明告知                                        │
│     ✅ 清楚展示更新内容、大小、预计时间             │
│     ❌ 只说"有更新"不说更新了什么                   │
│                                                      │
│  3. 给予控制权                                      │
│     ✅ "稍后提醒" / "下次启动时安装"                │
│     ❌ 只有 "立即安装" 一个选项                     │
│                                                      │
│  4. 进度可见                                        │
│     ✅ 下载进度、速度、剩余时间                     │
│     ❌ 转圈圈,不知道要等多久                       │
│                                                      │
│  5. 优雅降级                                        │
│     ✅ 网络断开时静默重试,不报错                   │
│     ❌ 弹出技术性错误信息                           │
│                                                      │
└─────────────────────────────────────────────────────┘

更新通知类型

通知类型与场景:

  ┌────────────────────────────────────────────────────┐
  │ 场景 1: 后台下载完成                               │
  │                                                     │
  │  ┌─────────────────────────────────────┐           │
  │  │ 🔄 更新就绪                         │           │
  │  │                                      │           │
  │  │ v1.3.0 已下载完成                    │           │
  │  │ 包含 3 项改进和 5 个 bug 修复        │           │
  │  │                                      │           │
  │  │ [下次启动时安装]  [查看详情]          │           │
  │  └─────────────────────────────────────┘           │
  ├────────────────────────────────────────────────────┤
  │ 场景 2: 正在下载                                   │
  │                                                     │
  │  状态栏小图标:                                     │
  │  ⬇ 下载更新 45% (2.3 MB/s)                        │
  │  不弹窗,不打断                                    │
  ├────────────────────────────────────────────────────┤
  │ 场景 3: 强制更新                                   │
  │                                                     │
  │  ┌─────────────────────────────────────┐           │
  │  │ ⚠️ 需要更新                         │           │
  │  │                                      │           │
  │  │ 当前版本存在安全问题,必须更新后     │           │
  │  │ 才能继续使用。                        │           │
  │  │                                      │           │
  │  │ 正在下载... 67%                       │           │
  │  │ ████████████████░░░░░░░░             │           │
  │  └─────────────────────────────────────┘           │
  └────────────────────────────────────────────────────┘

A/B 测试集成

// A/B 测试与更新结合

// 服务端: 不同用户组收到不同版本
{
  "experiments": {
    "new-editor": {
      "control": {
        "version": "1.2.5",
        "percentage": 50
      },
      "treatment": {
        "version": "1.3.0-experiment.1",
        "percentage": 50
      }
    }
  }
}

// 客户端: 上报实验数据
class ABTestUpdater {
  constructor(userId) {
    this.userId = userId
    this.experimentGroup = this.getExperimentGroup()
  }

  getExperimentGroup() {
    // 根据 userId hash 确定分组
    const hash = crypto.createHash('md5').update(this.userId).digest('hex')
    const bucket = parseInt(hash.substring(0, 8), 16) % 100
    return bucket < 50 ? 'control' : 'treatment'
  }

  async checkUpdate() {
    const response = await fetch(
      `${serverUrl}/check-update?` +
      `userId=${this.userId}&` +
      `group=${this.experimentGroup}&` +
      `version=${app.getVersion()}`
    )
    return response.json()
  }

  // 上报指标(更新后的行为数据)
  reportMetric(event, data) {
    fetch(`${serverUrl}/telemetry`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId: this.userId,
        group: this.experimentGroup,
        version: app.getVersion(),
        event,
        data,
        timestamp: Date.now(),
      })
    }).catch(() => {})
  }
}

监控与报警

关键指标

更新相关的关键监控指标:

  ┌─────────────────────────────────────────────────────┐
  │                                                      │
  │  1. 更新覆盖率                                      │
  │     - 各版本用户数量和比例                          │
  │     - 最新版本的覆盖率随时间变化                    │
  │     - 目标: 发布后 7 天覆盖 90%                     │
  │                                                      │
  │  2. 更新成功率                                      │
  │     - 检查成功率 (网络/服务器问题)                  │
  │     - 下载成功率 (网络中断/磁盘空间)                │
  │     - 安装成功率 (权限/文件锁定)                    │
  │     - 目标: > 99%                                   │
  │                                                      │
  │  3. 崩溃率                                          │
  │     - 各版本的崩溃率                                │
  │     - 更新后 vs 更新前的崩溃率对比                  │
  │     - 目标: < 0.5%                                  │
  │                                                      │
  │  4. 回滚率                                          │
  │     - 自动回滚次数                                  │
  │     - 用户手动回滚次数                              │
  │     - 目标: < 1%                                    │
  │                                                      │
  │  5. 性能指标                                        │
  │     - 更新包下载耗时                                │
  │     - 更新安装耗时                                  │
  │     - 更新后首次启动耗时                            │
  │                                                      │
  └─────────────────────────────────────────────────────┘

遥测实现

// telemetry.js — 更新遥测

class UpdateTelemetry {
  constructor(serverUrl) {
    this.serverUrl = serverUrl
    this.sessionId = crypto.randomUUID()
  }

  // 上报事件
  async report(event, data = {}) {
    try {
      await fetch(`${this.serverUrl}/telemetry`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          sessionId: this.sessionId,
          event,
          data: {
            ...data,
            version: app.getVersion(),
            platform: process.platform,
            arch: process.arch,
            locale: app.getLocale(),
          },
          timestamp: new Date().toISOString(),
        })
      })
    } catch {
      // 遥测失败不应影响应用
    }
  }

  // 预定义事件
  updateCheckStarted() { this.report('update.check.started') }
  updateCheckFailed(error) { this.report('update.check.failed', { error }) }
  updateAvailable(version) { this.report('update.available', { version }) }
  updateNotAvailable() { this.report('update.not_available') }
  downloadStarted(version) { this.report('update.download.started', { version }) }
  downloadProgress(percent) { this.report('update.download.progress', { percent }) }
  downloadCompleted(version, duration) {
    this.report('update.download.completed', { version, duration })
  }
  downloadFailed(error) { this.report('update.download.failed', { error }) }
  installStarted(version) { this.report('update.install.started', { version }) }
  installCompleted(version) { this.report('update.install.completed', { version }) }
  installFailed(error) { this.report('update.install.failed', { error }) }
  rollbackTriggered(fromVersion, toVersion) {
    this.report('update.rollback', { fromVersion, toVersion })
  }
}

报警规则

报警规则配置:

  ┌──────────────────────────────────────────────────────┐
  │  规则 1: 崩溃率突增                                  │
  │  条件: 最新版本崩溃率 > 2% 且持续 10 分钟           │
  │  动作: 暂停灰度 + 通知开发团队                      │
  │  级别: P1 (紧急)                                     │
  ├──────────────────────────────────────────────────────┤
  │  规则 2: 更新成功率下降                              │
  │  条件: 更新成功率 < 95% 且持续 30 分钟              │
  │  动作: 检查更新服务器状态                            │
  │  级别: P2 (高)                                       │
  ├──────────────────────────────────────────────────────┤
  │  规则 3: 回滚率异常                                  │
  │  条件: 回滚率 > 5%                                   │
  │  动作: 暂停灰度 + 通知开发团队                      │
  │  级别: P1 (紧急)                                     │
  ├──────────────────────────────────────────────────────┤
  │  规则 4: 版本碎片化                                  │
  │  条件: 发布 14 天后最新版覆盖率 < 50%               │
  │  动作: 检查更新推送策略                              │
  │  级别: P3 (中)                                       │
  └──────────────────────────────────────────────────────┘

实际案例分析

案例 1: VS Code 的更新策略

VS Code 更新方案:

  1. 使用 electron-updater + GitHub Releases
  2. 稳定版月更,Insiders 版日更
  3. 背景静默下载
  4. 下载完成后在状态栏提示 "重启以更新"
  5. 用户可以随时点击重启,也可以继续工作
  6. 下次启动时自动安装

  亮点:
  - 扩展系统独立于应用更新
  - 设置同步减少重新配置成本
  - 发行说明清晰详尽

案例 2: Slack 的更新策略

Slack 更新方案:

  1. 静默自动更新
  2. 用户几乎无感知
  3. 下载完成后等待用户空闲
  4. 在空闲时自动重启更新
  5. 重启后恢复到之前的工作状态

  亮点:
  - 极低的用户打扰
  - 状态保持和恢复
  - 快速的更新节奏

案例 3: 国内应用的混合方案

典型国内 Electron 应用:

  1. 壳层更新: electron-updater
     - 低频 (每月)
     - 静默下载 + 下次启动安装

  2. 前端热更新: Web Bundle / asar 替换
     - 高频 (每周甚至每天)
     - 通过灰度逐步推送
     - 不需要重启

  3. 强制更新: 最低版本号控制
     - 安全漏洞时触发
     - 必须更新才能使用

  4. CDN 加速: 阿里云 OSS / 腾讯 COS
     - 国内访问速度快
     - 成本可控

深入理解

更新策略的总体架构

完整的更新架构:

  ┌─────────────────────────────────────────────────────────┐
  │                     更新服务                             │
  │                                                          │
  │  ┌──────────┐  ┌──────────┐  ┌───────────────────────┐ │
  │  │ 全量更新  │  │ 热补丁   │  │ 灰度/A-B 测试控制   │ │
  │  │ 服务     │  │ 服务     │  │ 服务                  │ │
  │  └────┬─────┘  └────┬─────┘  └──────────┬────────────┘ │
  │       │              │                    │              │
  │       └──────────────┴────────────────────┘              │
  │                      │                                    │
  │              CDN 分发层                                   │
  └──────────────────────┼───────────────────────────────────┘

                    HTTPS 下载

  ┌──────────────────────┼───────────────────────────────────┐
  │                      ▼                                    │
  │  ┌──────────────────────────────────────────────────┐   │
  │  │              UpdateManager                        │   │
  │  │                                                    │   │
  │  │  ┌──────────┐ ┌──────────┐ ┌──────────────────┐  │   │
  │  │  │ electron │ │ asar     │ │ web bundle       │  │   │
  │  │  │ -updater │ │ patcher  │ │ manager          │  │   │
  │  │  └──────────┘ └──────────┘ └──────────────────┘  │   │
  │  │                                                    │   │
  │  │  ┌──────────┐ ┌──────────┐ ┌──────────────────┐  │   │
  │  │  │ 回滚     │ │ 灰度     │ │ 遥测上报         │  │   │
  │  │  │ 管理器   │ │ 客户端   │ │                   │  │   │
  │  │  └──────────┘ └──────────┘ └──────────────────┘  │   │
  │  └──────────────────────────────────────────────────┘   │
  │                     Electron App                         │
  └──────────────────────────────────────────────────────────┘

常见问题

Q1: 灰度发布如何确保用户分组一致?

使用用户 ID 的 hash 分桶,而不是随机数。这样同一用户每次检查更新时都属于同一个分组。

Q2: 差分更新失败怎么办?

回退到全量下载。差分更新是优化手段,不是必须路径。

Q3: 如何处理用户长期不更新?

设置最低版本号。低于最低版本的用户在启动时会被强制更新。

Q4: 更新过程中断电/崩溃怎么办?

  • 下载中断:支持断点续传
  • 安装中断:使用原子操作(rename),要么完全成功要么不影响旧版本
  • 启动失败:sentinel 机制自动回滚

Q5: 多个更新方案如何协调?

优先级和互斥关系:

  electron-updater (全量) — 最低频,最高优先级
  asar 热补丁            — 中频,壳版本兼容时使用
  web bundle 更新        — 最高频,壳版本兼容时使用

  规则:
  - 全量更新时,清除所有热补丁和 bundle 缓存
  - asar 补丁后,需要检查 web bundle 兼容性
  - web bundle 更新独立于其他两种

实践建议

1. 起步方案

刚开始做更新?按这个顺序:

  Phase 1: 基础 (Week 1)
  └── electron-updater + GitHub Releases
      最简单,覆盖 80% 的需求

  Phase 2: 优化 (Month 1)
  └── 添加灰度发布(百分比灰度)
      添加更新遥测
      添加回滚机制

  Phase 3: 热更新 (Month 2-3)
  └── Web Bundle 独立更新
      增量下载
      CDN 加速

  Phase 4: 生产化 (Month 3+)
  └── A/B 测试
      完整监控报警
      自动灰度推进

2. 安全清单

□ 所有下载使用 HTTPS
□ 文件 SHA256 校验
□ 代码签名(macOS/Windows)
□ 不允许版本降级(防回滚攻击)
□ 更新服务器的访问控制
□ manifest 签名验证
□ 定期安全审计

3. 运维清单

发布前:
□ 构建所有平台的安装包
□ 签名校验
□ 测试升级路径(至少 2 个旧版本)
□ 准备 release notes
□ 灰度规则配置

发布中:
□ 上传到 CDN / GitHub Releases
□ 更新 manifest.json
□ 开始灰度(10%)
□ 监控崩溃率和错误日志

发布后:
□ 逐步扩大灰度
□ 持续监控 7 天
□ 确认覆盖率达标
□ 清理旧版本

本章小结

本章汇总了 Electron 应用更新的所有最佳实践:

  1. 灰度发布:百分比 + 白名单 + 渠道,逐步推进
  2. 差分更新:blockmap / bsdiff 减少下载量
  3. 强制更新:最低版本号保障安全
  4. 自动回滚:sentinel 机制检测崩溃并恢复
  5. 用户体验:后台下载、温和通知、给予控制权
  6. 监控报警:覆盖率、成功率、崩溃率、回滚率
  7. A/B 测试:数据驱动的版本决策

至此,Part 2(热更新)全部完成。掌握这些知识,你可以为 Electron 应用构建一套生产级的更新系统。


上一篇04 - Web Bundle 更新
下一篇Part 3: OpenClaw Desktop 架构