Electron应用全量升级:从检测到下载的完整实现方案

91 阅读13分钟

在桌面应用开发领域,Electron凭借"一次开发、跨平台运行"的特性占据重要地位,而应用升级能力作为保障用户体验与功能迭代的核心环节,直接影响产品生命力。全量升级作为最基础、最稳定的升级方式,适用于中小规模应用、核心功能迭代场景,其"检测新版本-用户确认-完整包下载-重启安装"的链路清晰可控。本文将从技术选型、原理剖析、实操实现到优化实践,完整呈现Electron应用全量升级的落地方案。

一、核心技术栈与升级原理

Electron应用的全量升级本质是"获取最新安装包-替换本地应用"的过程,需解决三个核心问题:如何精准检测版本差异、如何安全下载完整安装包、如何无缝完成安装替换。业界成熟的解决方案是基于electron-builderelectron-updater的组合,二者分工明确且深度兼容。

1.1 核心工具定位

  • electron-builder:跨平台打包工具,负责将Electron源码编译为各系统可执行安装包(Windows的.exe、macOS的.dmg、Linux的.AppImage等),同时生成版本元数据文件,为升级检测提供依据。

  • electron-updater:更新核心模块,封装了版本检测、包下载、校验、重启安装等能力,通过监听主进程事件实现全链路控制,支持自定义交互逻辑。

1.2 全量升级核心原理

全量升级的核心依赖"版本元数据校验+完整包替换"机制,具体流程如下:

  1. 版本锚点定义:通过项目package.json中的version字段定义当前应用版本,遵循语义化版本规范(如1.0.0、1.1.0)。

  2. 元数据生成:electron-builder打包时会生成latest.yml(或平台专属的latest-mac.yml等)元数据文件,记录最新版本号、安装包下载地址、文件哈希值、大小等关键信息。

  3. 版本检测:应用启动或用户手动触发时,electron-updater从配置的服务器地址拉取最新latest.yml,与本地版本号对比,判断是否存在更新。

  4. 全量包下载:用户确认升级后,下载元数据中指向的完整安装包,同时通过哈希值实时校验文件完整性,避免下载损坏。

  5. 重启安装:下载完成后,electron-updater调用系统接口关闭当前应用,执行安装包完成更新,最后重启应用加载新版本。

二、完整实现:从配置到落地

本文以"通用HTTP服务器"作为更新源(适用于企业内网、私有部署场景),结合自定义更新UI,实现"自动检测+手动触发+进度展示"的全量升级功能,覆盖Windows、macOS、Linux全平台。

2.1 环境准备与项目初始化

首先完成基础环境搭建,确保Node.js(v16+)与npm/yarn已安装,随后初始化项目并安装核心依赖:


# 1. 初始化项目
mkdir electron-full-update && cd electron-full-update
npm init -y

# 2. 安装核心依赖
npm install --save-dev electron@^28.0.0 electron-builder@^24.9.1
npm install --save electron-updater@^6.1.7

2.2 核心配置:package.json关键设置

package.json是electron-builder与electron-updater的配置核心,需重点配置build字段,明确打包规则与更新源信息:

{
  "name": "electron-full-update-demo",
  "version": "1.0.0", // 当前应用版本,升级判断的核心依据
  "main": "main.js", // 主进程入口
  "scripts": {
    "start": "electron .", // 开发环境启动
    "build": "electron-builder", // 仅打包不发布
    "publish": "electron-builder --publish always" // 打包并发布到更新源
  },
  "build": {
    "appId": "com.example.electronupdate", // 应用唯一标识,跨版本必须一致
    "productName": "Electron全量升级演示", // 应用名称
    "directories": {
      "output": "dist" // 打包产物输出目录
    },
    "files": [
      // 需打包的核心文件
      "main.js",
      "index.html",
      "preload.js",
      "renderer.js",
      "package.json",
      "node_modules/"
    ],
    "asar": true, // 源码压缩为asar包,提升安全性
    // 各平台打包配置
    "win": {
      "target": "nsis", // Windows生成exe安装包
      "icon": "build/icon.ico", // 应用图标
      "signingHashAlgorithms": ["sha256"] // 配置签名哈希算法
    },
    "mac": {
      "target": "dmg", // macOS生成dmg镜像
      "icon": "build/icon.icns",
      "signingIdentity": "Developer ID Application: Your Company (XXXXXX)" // 配置签名身份
    },
    "linux": {
      "target": "AppImage" // Linux生成自执行应用
    },
    // 关键:更新源配置(通用HTTP服务器)
    "publish": [
      {
        "provider": "generic",
        "url": "http://your-update-server.com/electron-updates/", // 更新文件存放地址
        "channel": "latest" // 更新渠道
      }
    ]
  },
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.9.1"
  },
  "dependencies": {
    "electron-updater": "^6.1.7"
  }
}

若使用GitHub作为更新源,可将publish字段改为GitHub配置,只需替换provider"github",并补充owner(用户名)、repo(仓库名)即可,无需自建服务器。

2.3 主进程逻辑:升级全链路控制

主进程(main.js)是升级功能的核心载体,负责初始化electron-updater、监听更新事件、与渲染进程通信,实现"检测-下载-安装"的自动化控制:

const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
const path = require('path')
const log = require('electron-log') // 推荐添加日志模块

// 配置日志
log.transports.file.level = 'info'
autoUpdater.logger = log

let mainWindow

// 初始化应用窗口
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // 预加载脚本,保障通信安全
      contextIsolation: true, // 开启上下文隔离,符合Electron安全规范
      nodeIntegration: false // 禁用渲染进程Node集成
    }
  })

  // 加载渲染进程页面
  mainWindow.loadFile('index.html')

  // 应用就绪后自动检测更新(可选:改为用户手动触发更灵活)
  app.whenReady().then(() => {
    // 开发环境指定更新配置文件,方便测试
    if (!app.isPackaged) {
      try {
        autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml')
      } catch (error) {
        log.error('开发环境更新配置文件错误:', error)
      }
    }

    // 初始化更新检测(不自动弹窗,由自定义UI控制)
    checkForUpdatesSafe()
  })
}

// 安全检查更新,增加错误处理
function checkForUpdatesSafe() {
  try {
    autoUpdater.checkForUpdates()
  } catch (error) {
    log.error('检查更新失败:', error)
    if (mainWindow) {
      mainWindow.webContents.send('update:error', error.message)
    }
  }
}

// 1. 监听渲染进程"手动检查更新"请求
ipcMain.handle('update:check', async () => {
  try {
    return await autoUpdater.checkForUpdates()
  } catch (error) {
    log.error('手动检查更新失败:', error)
    throw error
  }
})

// 2. 监听渲染进程"开始下载"请求
ipcMain.on('update:download', () => {
  try {
    autoUpdater.downloadUpdate()
  } catch (error) {
    log.error('开始下载更新失败:', error)
    if (mainWindow) {
      mainWindow.webContents.send('update:error', error.message)
    }
  }
})

// 3. 监听渲染进程"立即安装"请求
ipcMain.on('update:install', () => {
  try {
    // 可选:在退出前保存用户数据
    saveUserDataBeforeUpdate()
      .then(() => {
        autoUpdater.quitAndInstall(false, true) // 关闭应用并执行安装
      })
      .catch((error) => {
        log.error('保存用户数据失败:', error)
        // 即使保存失败也继续安装
        autoUpdater.quitAndInstall(false, true)
      })
  } catch (error) {
    log.error('安装更新失败:', error)
    if (mainWindow) {
      mainWindow.webContents.send('update:error', error.message)
    }
  }
})

// 保存用户数据(示例方法)
async function saveUserDataBeforeUpdate() {
  // 实际应用中实现保存逻辑
  log.info('正在保存用户数据...')
  return Promise.resolve()
}

// 4. 检测到新版本:通知渲染进程显示更新提示
autoUpdater.on('update-available', (info) => {
  log.info('检测到新版本:', info.version)
  if (mainWindow) {
    mainWindow.webContents.send('update:available', {
      version: info.version, // 新版本号
      releaseNotes: info.releaseNotes || '优化用户体验,修复已知问题', // 更新日志
      pub_date: info.pub_date // 发布日期
    })
  }
})

// 5. 无新版本:通知渲染进程反馈结果
autoUpdater.on('update-not-available', () => {
  log.info('当前已是最新版本')
  if (mainWindow) {
    mainWindow.webContents.send('update:no-available')
  }
})

// 6. 下载进度更新:实时同步到渲染进程展示
autoUpdater.on('download-progress', (progress) => {
  if (mainWindow) {
    mainWindow.webContents.send('update:progress', {
      percent: Math.round(progress.percent), // 下载进度百分比
      speed: (progress.bytesPerSecond / 1024 / 1024).toFixed(2), // 下载速度(MB/s)
      transferred: progress.transferred, // 已传输字节数
      total: progress.total // 总字节数
    })
  }
})

// 7. 下载完成:通知渲染进程切换安装状态
autoUpdater.on('update-downloaded', () => {
  log.info('更新包下载完成')
  if (mainWindow) {
    mainWindow.webContents.send('update:downloaded')
  }
})

// 8. 升级错误:捕获异常并提示用户
autoUpdater.on('error', (err) => {
  log.error('升级异常:', err.message, err.stack)
  if (mainWindow) {
    mainWindow.webContents.send('update:error', err.message)
    // 可选:通过系统对话框提示错误
    dialog
      .showMessageBox(mainWindow, {
        type: 'error',
        title: '升级失败',
        message: `更新过程出错:${err.message}`,
        buttons: ['确定']
      })
      .catch((dialogError) => {
        log.error('显示错误对话框失败:', dialogError)
      })
  }
})

// 应用生命周期管理
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow()
})

// 启动应用
app.whenReady().then(createWindow)

2.4 预加载脚本:安全通信桥梁

为符合Electron安全规范,需通过预加载脚本(preload.js)实现主进程与渲染进程的通信,避免渲染进程直接访问Electron核心模块:

const { contextBridge, ipcRenderer } = require('electron')

// 存储事件监听器,以便后续清理
const eventListeners = new Map()

// 向渲染进程暴露安全的通信接口
contextBridge.exposeInMainWorld('updateAPI', {
  // 手动检查更新
  checkUpdate: () => ipcRenderer.invoke('update:check'),
  // 开始下载更新包
  startDownload: () => ipcRenderer.send('update:download'),
  // 立即安装更新
  installUpdate: () => ipcRenderer.send('update:install'),
  // 监听更新事件(通过回调函数接收数据)
  onUpdateAvailable: (callback) => {
    const channel = 'update:available'
    const listener = (e, data) => callback(data)
    ipcRenderer.on(channel, listener)
    eventListeners.set(channel, listener)
  },
  onUpdateNoAvailable: (callback) => {
    const channel = 'update:no-available'
    const listener = callback
    ipcRenderer.on(channel, listener)
    eventListeners.set(channel, listener)
  },
  onUpdateProgress: (callback) => {
    const channel = 'update:progress'
    const listener = (e, data) => callback(data)
    ipcRenderer.on(channel, listener)
    eventListeners.set(channel, listener)
  },
  onUpdateDownloaded: (callback) => {
    const channel = 'update:downloaded'
    const listener = callback
    ipcRenderer.on(channel, listener)
    eventListeners.set(channel, listener)
  },
  onUpdateError: (callback) => {
    const channel = 'update:error'
    const listener = (e, msg) => callback(msg)
    ipcRenderer.on(channel, listener)
    eventListeners.set(channel, listener)
  },
  // 清理所有事件监听器,避免内存泄漏
  cleanup: () => {
    eventListeners.forEach((listener, channel) => {
      ipcRenderer.removeListener(channel, listener)
    })
    eventListeners.clear()
  }
})

// 当窗口关闭时清理事件监听器
window.addEventListener('beforeunload', () => {
  if (window.updateAPI?.cleanup) {
    window.updateAPI.cleanup()
  }
})

2.5 渲染进程:自定义更新UI

渲染进程(index.html+renderer.js)负责展示更新交互界面,通过预加载脚本提供的接口与主进程通信,实现"手动检查-进度展示-安装确认"的用户交互:

2.5.1 页面结构(index.html)

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Electron全量升级演示</title>
    <style>
      body {
        font-family:
          -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        line-height: 1.6;
        color: #333;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
        background-color: #f5f5f5;
      }
      .container {
        background-color: white;
        padding: 30px;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
      }
      h1 {
        color: #2c3e50;
        margin-bottom: 30px;
      }
      .update-section {
        margin-bottom: 30px;
      }
      .btn {
        padding: 10px 20px;
        background-color: #3498db;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 16px;
        transition: background-color 0.3s;
      }
      .btn:hover:not(:disabled) {
        background-color: #2980b9;
      }
      .btn:disabled {
        background-color: #bdc3c7;
        cursor: not-allowed;
      }
      #checkBtn {
        margin-bottom: 20px;
      }
      #updateInfo {
        margin: 15px 0;
        padding: 10px;
        background-color: #ecf0f1;
        border-radius: 4px;
        min-height: 20px;
      }
      .hidden {
        display: none;
      }
      #updateAvailable {
        padding: 20px;
        background-color: #e8f4f8;
        border-radius: 4px;
        border-left: 4px solid #3498db;
      }
      #releaseNotes {
        margin-top: 10px;
        font-style: italic;
        color: #7f8c8d;
      }
      #downloadProgress {
        padding: 20px;
        background-color: #e8f8f5;
        border-radius: 4px;
      }
      .progress-bar {
        width: 100%;
        height: 20px;
        background-color: #ecf0f1;
        border-radius: 10px;
        overflow: hidden;
        margin: 15px 0;
      }
      .progress-fill {
        height: 100%;
        background-color: #2ecc71;
        transition: width 0.3s ease;
      }
      .progress-info {
        display: flex;
        justify-content: space-between;
        font-size: 14px;
        color: #7f8c8d;
      }
      #installReady {
        padding: 20px;
        background-color: #fff3cd;
        border-radius: 4px;
        border-left: 4px solid #ffc107;
      }
      .install-buttons {
        display: flex;
        gap: 10px;
        margin-top: 15px;
      }
      #installNowBtn {
        background-color: #e67e22;
      }
      #installLaterBtn {
        background-color: #95a5a6;
      }
      #installNowBtn:hover {
        background-color: #d35400;
      }
      #installLaterBtn:hover {
        background-color: #7f8c8d;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Electron应用全量升级演示</h1>

      <div class="update-section">
        <button id="checkBtn" class="btn" onclick="checkUpdate()">检查更新</button>
        <div id="updateInfo"></div>

        <!-- 新版本可用提示 -->
        <div id="updateAvailable" class="hidden">
          <h3>发现新版本 <span id="newVersion"></span></h3>
          <div id="releaseNotes"></div>
          <button id="downloadBtn" class="btn" onclick="startDownload()">立即下载</button>
        </div>

        <!-- 下载进度 -->
        <div id="downloadProgress" class="hidden">
          <h3>正在下载更新...</h3>
          <div class="progress-bar">
            <div id="progressFill" class="progress-fill"></div>
          </div>
          <div class="progress-info">
            <span>进度: <span id="progressPercent">0</span>%</span>
            <span>速度: <span id="downloadSpeed">0</span> MB/s</span>
          </div>
        </div>

        <!-- 安装准备完成 -->
        <div id="installReady" class="hidden">
          <h3>更新已下载完成</h3>
          <p>应用将在重启后更新到新版本。是否现在重启应用?</p>
          <div class="install-buttons">
            <button id="installNowBtn" class="btn" onclick="installUpdate()">立即安装</button>
            <button id="installLaterBtn" class="btn" onclick="hideInstallReady()">稍后再说</button>
          </div>
        </div>
      </div>
    </div>

    <script src="renderer.js"></script>
  </body>
</html>

2.5.2 交互逻辑(renderer.js)

// 获取DOM元素
const checkBtn = document.getElementById('checkBtn')
const updateInfo = document.getElementById('updateInfo')
const updateAvailable = document.getElementById('updateAvailable')
const newVersion = document.getElementById('newVersion')
const releaseNotes = document.getElementById('releaseNotes')
const downloadBtn = document.getElementById('downloadBtn')
const downloadProgress = document.getElementById('downloadProgress')
const progressPercent = document.getElementById('progressPercent')
const progressFill = document.getElementById('progressFill')
const downloadSpeed = document.getElementById('downloadSpeed')
const installReady = document.getElementById('installReady')
const installNowBtn = document.getElementById('installNowBtn')
const installLaterBtn = document.getElementById('installLaterBtn')

// 初始化更新API监听
function initUpdateListener() {
  // 检测到新版本
  window.updateAPI.onUpdateAvailable((data) => {
    checkBtn.disabled = false
    updateInfo.textContent = ''
    updateAvailable.classList.remove('hidden')
    newVersion.textContent = data.version
    releaseNotes.textContent = data.releaseNotes

    // 可选:记录更新事件
    console.log('新版本可用:', data.version)
  })

  // 无新版本
  window.updateAPI.onUpdateNoAvailable(() => {
    checkBtn.disabled = false
    updateInfo.textContent = '当前已是最新版本'
    hideAllUpdateSections()
  })

  // 下载进度更新
  window.updateAPI.onUpdateProgress((data) => {
    downloadProgress.classList.remove('hidden')
    progressPercent.textContent = data.percent
    progressFill.style.width = `${data.percent}%`
    downloadSpeed.textContent = data.speed
  })

  // 下载完成
  window.updateAPI.onUpdateDownloaded(() => {
    downloadProgress.classList.add('hidden')
    installReady.classList.remove('hidden')

    // 可选:保存更新状态
    localStorage.setItem('updateDownloaded', 'true')
  })

  // 升级错误
  window.updateAPI.onUpdateError((msg) => {
    checkBtn.disabled = false
    updateInfo.textContent = `更新失败:${msg}`
    hideAllUpdateSections()

    // 可选:记录错误
    console.error('更新错误:', msg)
  })
}

// 手动检查更新
function checkUpdate() {
  checkBtn.disabled = true
  updateInfo.textContent = '正在检查更新,请稍候...'
  hideAllUpdateSections()

  // 设置超时处理,避免长时间无响应
  const timeoutId = setTimeout(() => {
    checkBtn.disabled = false
    updateInfo.textContent = '检查更新超时,请检查网络连接后重试'
  }, 30000) // 30秒超时

  window.updateAPI
    .checkUpdate()
    .catch((err) => {
      clearTimeout(timeoutId)
      checkBtn.disabled = false
      updateInfo.textContent = `检查更新失败:${err.message || '未知错误'}`
    })
    .finally(() => {
      clearTimeout(timeoutId)
    })
}

// 开始下载更新
function startDownload() {
  updateAvailable.classList.add('hidden')
  downloadProgress.classList.remove('hidden')
  window.updateAPI.startDownload()
}

// 立即安装更新
function installUpdate() {
  // 可选:显示确认对话框
  if (confirm('确定要立即安装更新吗?应用将重启。')) {
    window.updateAPI.installUpdate()
  }
}

// 隐藏安装提示
function hideInstallReady() {
  installReady.classList.add('hidden')
  // 可选:记录用户选择
  localStorage.setItem('updateDismissed', new Date().toISOString())
}

// 隐藏所有更新相关区域
function hideAllUpdateSections() {
  updateAvailable.classList.add('hidden')
  downloadProgress.classList.add('hidden')
  installReady.classList.add('hidden')
}

// 页面加载时检查是否有已下载但未安装的更新
function checkPendingUpdate() {
  if (localStorage.getItem('updateDownloaded') === 'true') {
    const dismissedTime = localStorage.getItem('updateDismissed')
    // 如果用户上次选择稍后再说已经超过24小时,再次提示
    if (!dismissedTime || new Date() - new Date(dismissedTime) > 24 * 60 * 60 * 1000) {
      installReady.classList.remove('hidden')
    }
  }
}

// 清理资源,避免内存泄漏
function cleanup() {
  if (window.updateAPI?.cleanup) {
    window.updateAPI.cleanup()
  }
}

// 初始化
window.addEventListener('DOMContentLoaded', () => {
  initUpdateListener()
  checkPendingUpdate()

  // 监听页面卸载事件,清理资源
  window.addEventListener('beforeunload', cleanup)
})

三、服务器部署与发布流程

全量升级的稳定性依赖更新源的可靠性,本节以通用HTTP服务器(Nginx)为例,讲解更新文件的部署与版本发布流程。

3.1 服务器配置与目录结构

首先在服务器上创建更新文件存放目录,然后配置Nginx实现文件访问:


# 1. 创建目录(Linux为例)
mkdir -p /var/www/electron-updates

# 2. 配置Nginx(新建electron-update.conf)
server {
  listen 80;
  server_name your-update-server.com; # 服务器域名或IP

  location /electron-updates/ {
    root /var/www/;
    autoindex on; # 可选,开启目录浏览便于调试
    # 配置缓存控制,避免旧文件缓存
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
  }

  # 配置CORS,允许跨域访问
  add_header Access-Control-Allow-Origin *;
  add_header Access-Control-Allow-Methods 'GET, OPTIONS';
  add_header Access-Control-Allow-Headers 'Content-Type';

  # 错误日志配置
  error_log /var/log/nginx/electron-update-error.log;
  access_log /var/log/nginx/electron-update-access.log;
}

重启Nginx使配置生效:sudo systemctl restart nginx

3.1.1 HTTPS配置(推荐)

为提高安全性,强烈建议配置HTTPS:

# 安装Certbot获取SSL证书
sudo apt update
sudo apt install certbot python3-certbot-nginx

# 为域名生成证书
sudo certbot --nginx -d your-update-server.com

Nginx配置将自动更新为HTTPS,修改后的配置示例:

server {
  listen 443 ssl;
  server_name your-update-server.com;

  ssl_certificate /etc/letsencrypt/live/your-update-server.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/your-update-server.com/privkey.pem;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';

  # 其他配置保持不变...
  location /electron-updates/ {
    # 同上...
  }

  # 自动重定向HTTP到HTTPS
  if ($scheme != "https") {
    return 301 https://$host$request_uri;
  }
}

3.2 发布流程:从打包到部署

  1. 首次发布(版本1.0.0): 确认package.jsonversion为1.0.0。

  2. 执行打包发布命令:npm run publish

  3. 将项目dist目录下的所有文件(安装包、latest.yml等)上传至服务器/var/www/electron-updates/目录。

  4. 发布新版本(版本1.1.0): 修改package.jsonversion为1.1.0(需高于旧版本)。

  5. 执行npm run publish生成新版本文件。

  6. 将新版本安装包与最新生成的latest.yml上传至服务器,覆盖旧的latest.yml

服务器最终目录结构如下,确保latest.yml始终指向最新版本:


/var/www/electron-updates/
├─ latest.yml                # 最新版本元数据
├─ latest-mac.yml           # macOS专用元数据(如果有)
├─ latest-linux.yml         # Linux专用元数据(如果有)
├─ Electron全量升级演示-1.0.0.exe  # 旧版本Windows安装包
├─ Electron全量升级演示-1.1.0.exe  # 新版本Windows安装包
├─ Electron全量升级演示-1.1.0.dmg  # 新版本macOS安装包
└─ Electron全量升级演示-1.1.0.AppImage # 新版本Linux安装包

3.2.1 自动化发布流程(CI/CD)

使用GitHub Actions实现自动化发布:

# .github/workflows/build-and-publish.yml
name: Build and Publish

on:
  push:
    tags:
      - 'v*.*.*' # 当推送版本标签时触发

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest, windows-latest, ubuntu-latest]

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build and publish
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npm run publish

四、测试验证与优化实践

升级功能的可靠性需通过多场景测试验证,同时结合工程优化提升用户体验与安全性。

4.1 完整测试流程

  1. 基础功能测试:安装1.0.0版本,启动后自动检测到1.1.0版本,点击下载完成后重启安装,验证版本是否更新成功。

  2. 异常场景测试:模拟网络中断(下载过程中断开网络)、文件损坏(手动修改安装包),验证错误捕获与提示是否正常。

  3. 跨平台测试:在Windows 10/11、macOS Ventura、Ubuntu 22.04上分别测试,确保各平台兼容性。

  4. 手动触发测试:点击"手动检查更新"按钮,验证是否能正常触发检测流程。

  5. 代理环境测试:在企业代理环境下测试更新功能,确保代理配置正确。

4.2 关键优化方向

4.2.1 安全性优化

  • 启用HTTPS:将服务器升级为HTTPS(配置SSL证书),避免更新包被中间人篡改,electron-updater会自动校验HTTPS证书有效性。

  • 文件校验强化:依赖electron-builder自动生成的SHA512哈希值,下载过程中实时校验文件完整性,确保安装包未被篡改。

  • 代码签名:为Windows安装包配置数字签名(通过electron-buildersigningHashAlgorithms字段),避免系统报"未知发布者"警告。

  • 代理配置安全:如果在企业代理环境下,确保代理配置正确且不泄露敏感信息。

4.2.2 体验优化

  • 后台下载:下载过程中不阻塞应用主功能,通过进度条实时反馈状态,避免用户等待焦虑。

  • 断点续传:electron-updater默认支持断点续传,网络恢复后可继续下载,无需重新开始。

  • 更新日志优化:在latest.yml中补充详细的releaseNotes,通过Markdown格式展示,提升用户感知。

  • 更新提醒策略:根据应用使用频率调整更新提醒频率,避免频繁打扰用户。

  • 静默更新选项:对于非关键更新,提供静默更新选项,减少用户操作。

  • 更新状态持久化:记录用户的更新选择(如"稍后再说"),避免重复提示。

4.2.3 工程化优化

  • 自动化发布:结合CI/CD工具(如GitHub Actions、Jenkins),实现"代码提交-自动打包-自动部署"全流程自动化,减少人工操作失误。

  • 包体压缩:通过electron-buildercompression字段配置LZMA压缩,减小安装包体积,提升下载速度。

  • 旧版本清理:服务器端定期清理历史版本安装包,避免存储空间占用过大。

  • 日志系统集成:集成专业的日志系统,记录升级过程中的关键事件,便于问题排查。

  • 多渠道更新源:配置主备更新源,当主更新源不可用时自动切换到备用源。

  • 代理支持:自动检测系统代理设置,支持企业环境下的更新。

  • 更新策略配置:支持通过配置文件调整更新策略,如检查频率、强制更新等。

五、常见问题与解决方案

问题现象可能原因解决方案
无法检测到新版本1. latest.yml未更新;2. 版本号不符合语义化规范;3. 服务器地址配置错误1. 确认服务器latest.yml为最新版本;2. 版本号使用x.y.z格式;3. 检查package.json的publish.url配置
下载完成后无法安装1. 应用权限不足;2. 安装包损坏;3. appId与旧版本不一致1. 以管理员身份运行应用;2. 重新上传安装包并校验哈希值;3. 确保跨版本appId不变
macOS提示"无法打开应用"应用未签名或签名无效使用Apple开发者证书对应用签名,配置electron-builder的mac.signingIdentity字段
更新速度慢1. 服务器带宽不足;2. 无CDN加速;3. 包体过大1. 升级服务器带宽;2. 配置CDN加速更新文件;3. 优化包体(剔除无用依赖)
Windows Defender误报应用行为被安全软件误认为恶意1. 提交应用到Windows Defender排除列表;2. 完善应用签名和认证
特殊网络环境下更新失败代理设置不正确或网络限制1. 自动检测并应用系统代理设置;2. 提供手动代理配置选项;3. 实现网络连接检测机制
应用权限问题导致安装失败1. 安装路径权限不足;2. 系统权限限制1. 调整应用安装路径;2. 在安装前请求管理员权限
更新过程中应用崩溃1. 错误处理不完善;2. 资源清理不及时1. 加强错误捕获和恢复机制;2. 确保资源及时释放;3. 实现状态持久化
版本回滚需求新版本存在严重问题1. 在服务器保留多个版本;2. 实现版本选择机制;3. 提供紧急回滚功能
更新配置变更需求更新源地址或策略需要调整1. 实现动态配置加载;2. 支持远程配置更新

六、总结与展望

本文基于electron-builder与electron-updater实现了Electron应用的全量升级方案,通过"配置-编码-部署-测试"的完整链路,解决了版本检测、安全下载、无缝安装等核心问题。全量升级方案的优势在于实现简单、兼容性强,适合中小规模应用或核心功能迭代场景。

对于大规模应用或频繁更新场景,可在全量升级基础上引入增量升级(仅下载文件差异部分),通过electron-updater的blockmap功能实现;未来还可结合应用内反馈系统,收集升级过程中的异常信息,进一步提升升级可靠性。

Electron应用的升级能力并非孤立模块,需与产品迭代节奏、用户体验设计深度结合,才能真正发挥其价值,为用户提供"无感升级、持续优化"的使用体验。