electron 自动更新和手动检查更新

项目基于electron 22.3.34 + vue3+ vite, windows安装包适配win7、win8、win10、win11

项目目录:

package.json文件

"scripts": {
    "format": "prettier --write .",
    "lint": "eslint --cache .",
    "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
    "typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
    "typecheck": "npm run typecheck:node && npm run typecheck:web",
    "start": "electron-vite preview",
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "postinstall": "electron-builder install-app-deps",
    "build:unpack": "npm run build && electron-builder --dir",
    "build:win": "npm run build && electron-builder --win",
    "build:mac": "npm run build && electron-builder --mac",
    "build:linux": "npm run build && electron-builder --linux",
    "electron:generate-icons": "electron-icon-builder --input=./resources/icon.png --output=./build --flatten"
  },
  "dependencies": {
    "@electron-toolkit/preload": "^2.0.0",
    "@electron-toolkit/utils": "3.0.0",
    "electron-updater": "5.3.0"
  },
  "devDependencies": {
    "@electron-toolkit/eslint-config-prettier": "3.0.0",
    "@electron-toolkit/eslint-config-ts": "^3.0.0",
    "@electron-toolkit/tsconfig": "^1.0.1",
    "@types/node": "16.18.38",
    "@vitejs/plugin-vue": "3.2.0",
    "electron": "22.3.24",
    "electron-builder": "23.6.0",
    "electron-icon-builder": "2.0.1",
    "electron-vite": "1.0.10",
    "eslint": "8.56.0",
    "eslint-plugin-vue": "9.18.1",
    "prettier": "2.8.8",
    "typescript": "4.9.5",
    "vite-plugin-static-copy": "0.13.0",
    "vite": "3.2.5",
    "vue": "3.2.47",
    "vue-router": "4.0.12",
    "vue-tsc": "1.0.13"
  }

主进程main/index.ts文件:

import {app, shell, BrowserWindow, ipcMain, screen, Tray, nativeImage, Menu, dialog, protocol, globalShortcut} from 'electron'
import {join, dirname} from 'path'
import {electronApp, optimizer, is} from '@electron-toolkit/utils'
import AutoUpdater from './auto-updater';
import {exec} from "child_process";
import * as fs from "fs";
protocol.registerSchemesAsPrivileged([
  {
    scheme: 'app',
    privileges: {
      standard: true,
      secure: true,
      supportFetchAPI: true
    }
  }
])
// import icon from '../../resources/icon.png'

let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null
let autoUpdater: AutoUpdater
// 获取主显示器信息
// 创建托盘图标
function createTray() {
  const iconPath = app.isPackaged
    ? join(process.resourcesPath, 'app.asar.unpacked/resources/tray.png')
    : join(__dirname, '../../resources/tray.png')
  const icon = nativeImage.createFromPath(iconPath)
    .resize({width: 16, height: 16}) // 适配不同DPI

  tray = new Tray(icon)

  // 托盘菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '打开',
      click: () => {
        mainWindow?.show()
      }
    },
    {
      label: '检查更新',
      click: () => {
        if (autoUpdater) {
          autoUpdater.initializeAutoCheck()
        }
      }
    },
    {
      label: '退出',
      click: () => {
        autoUpdater.updateWindow?.destroy()
        mainWindow?.destroy()
        app.quit()
      }
    },
    {
      label: '卸载',
      click: () => {
        confirmAndRunUninstaller()
      }
    }
  ])

  tray.setContextMenu(contextMenu)
  tray.setToolTip('自己的标题')
}

function confirmAndRunUninstaller() {
  const appPath = process.execPath

// 推导卸载程序路径(假设卸载程序与应用同级目录)
  const uninstallerPath = join(
    dirname(appPath), // 应用安装目录
    'Uninstall 危急值AI提示.exe'       // Windows 卸载程序名称
  )
  // 1. 显示确认对话框
  dialog.showMessageBox({
    type: 'question',
    buttons: ['取消', '确认卸载'],
    title: '卸载应用',
    message: '确定要彻底卸载此应用吗?'
  }).then(({ response }) => {
    if (response === 1) {
      // 2. 关闭应用进程
      app.quit()

      // 3. 执行卸载程序(Windows 示例)
      if (process.platform === 'win32') {
        exec(`"${uninstallerPath}" /S`, (error) => {
          if (error) console.error('卸载失败:', error)
        })
      }
      // macOS 处理(需要更复杂的权限逻辑)
      else if (process.platform === 'darwin') {
        exec(`sh "${uninstallerPath}"`, )
      }
    }
  })
}

function createWindow(): void {
  const iconPath = join(__dirname, '../../resources/icon.png')
  mainWindow = new BrowserWindow({
    width: 400,
    height: 120,
    show: false,
    frame: false,
    resizable: false,
    alwaysOnTop: true,
    title: "危急值AI提示",
    skipTaskbar: true,
    ...(process.platform === 'linux' ? {iconPath} : {}),
    webPreferences: {
      preload: join(__dirname, '../preload/index.js'),
      sandbox: false
    }
  })

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url)
    return {action: 'deny'}
  })
// 初始化自动更新
  autoUpdater = new AutoUpdater(mainWindow);
  if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
    mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
  } else {
    mainWindow.loadURL('app://./index.html#/')
  }

  ipcMain.on('adjust-window-height', (_, height: number) => {
    console.log(height, '--------------->')
    adjustWindowSize(height)
  })
  // IPC 通信处理
  ipcMain.on('window-minimize', () => {
    mainWindow?.hide()
  })

  ipcMain.on('window-close', () => {
    mainWindow?.hide()
  })

  mainWindow.on('ready-to-show', positionWindow)
  mainWindow.on('show', positionWindow)
  mainWindow.on('close', (e) => {
    e.preventDefault()
    mainWindow?.hide()
  })
}

function positionWindow() {
  if (!mainWindow) return

  const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint())
  const {width, height} = display.workArea
  const [winWidth, winHeight] = mainWindow.getSize()

  mainWindow.setPosition(
    width - winWidth - 10,
    height - winHeight - 10
  )
}

// 调整窗口尺寸
function adjustWindowSize(contentHeight: number) {
  if (!mainWindow) return
  mainWindow.setContentSize(mainWindow.getContentBounds().width, contentHeight)
  // 保持右下角位置
  positionWindow()
  mainWindow.show()
}

app.whenReady().then(() => {
  // Set app user model id for windows
  protocol.registerFileProtocol('app', (request, callback) => {
    const url = new URL(request.url).pathname; // 解析路径
    const decodedUrl = decodeURIComponent(url); // 处理中文路径
    const baseDir = join(__dirname, '../renderer');
    const filePath = join(baseDir, decodedUrl);

    // 返回文件或错误
    if (fs.existsSync(filePath)) {
      callback(filePath);
    } else {
      console.error('文件未找到:', filePath);
      callback({ error: -6 }); // 错误码 -6 表示文件不存在
    }
  });
  electronApp.setAppUserModelId('com.electron')
  app.on('browser-window-created', (_, window) => {
    optimizer.watchWindowShortcuts(window)
  })

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

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

主进程auto-update.ts文件:(可以复制直接使用)

import { autoUpdater, UpdateInfo } from 'electron-updater';
import {BrowserWindow, ipcMain, app, globalShortcut} from 'electron';
import {is} from '@electron-toolkit/utils'
import * as path from 'path';

export default class AutoUpdater {
  mainWindow: BrowserWindow;
  public updateWindow: BrowserWindow | null = null;
  private updateVersionInfo: any

  constructor(mainWindow: BrowserWindow) {
    this.mainWindow = mainWindow;
    this.configure();
    this.setupListeners();
    this.setupIPC();
    this.initializeAutoCheck()
  }

  // 新增初始化自动检查方法
  public initializeAutoCheck() {
    if (!app.isPackaged) {
      // 开发环境延迟3秒检查,避免干扰调试
      setTimeout(() => {
        autoUpdater.checkForUpdates().catch(err => {
          console.error('Dev update check failed:', err);
        });
      }, 3000);
    } else {
      // 生产环境立即检查
      autoUpdater.checkForUpdates();
    }
  }

  private configure(): void {
    autoUpdater.autoDownload = false;
    autoUpdater.allowDowngrade = false;
    // 开发环境配置
    if (!app.isPackaged) {
      autoUpdater.forceDevUpdateConfig = true;
      autoUpdater.updateConfigPath = path.join(__dirname, '../../dev-app-update.yml');
      autoUpdater.setFeedURL({
        provider: 'generic',
        url: 'http://101.42.117.183:3000',
        channel: 'latest'
      });
    } else {
      autoUpdater.setFeedURL({
        provider: 'generic',
        url: 'http://192.170.78.131/updateApp',
        channel: 'latest'
      });
    }
  }

  private setupListeners(): void {
    autoUpdater.on('update-available', (info: UpdateInfo) => {
      const currentVersion = app.getVersion();
      if (info.version === currentVersion) {
        this.sendToUpdateWindow('update-not-available');
        return;
      }
      this.updateVersionInfo = {...info, oldVersion: currentVersion}
      this.showUpdateWindow();
    });

    autoUpdater.on('download-progress', (progress) => {
      this.sendToUpdateWindow('download-progress', {
        percent: Math.floor(progress.percent),
        speed: `${(progress.bytesPerSecond / 1024 / 1024).toFixed(1)}MB/s`,
        downloaded: `${(progress.transferred / 1024 / 1024).toFixed(1)}MB`,
        total: `${(progress.total / 1024 / 1024).toFixed(1)}MB`
      });
    });

    autoUpdater.on('update-downloaded', () => {
      this.sendToUpdateWindow('update-downloaded');
    });

    autoUpdater.on('error', (err) => {
      this.sendToUpdateWindow('update-error', err.message);
    });
  }

  private setupIPC(): void {
    ipcMain.handle('start-download', () => autoUpdater.downloadUpdate());
    ipcMain.handle('install-update', () => autoUpdater.quitAndInstall());
    ipcMain.handle('check-update', () => autoUpdater.checkForUpdates());
    ipcMain.handle("close-window", () => {
      if (this.updateWindow) this.updateWindow.close()
    })
    ipcMain.handle("install-app", () => autoUpdater.quitAndInstall())
  }

  private showUpdateWindow(): void {
    if (!this.updateWindow) {
      this.updateWindow = new BrowserWindow({
        width: 400,
        height: 400,
        show: false,
        frame: false,
        resizable: false,
        transparent: true,
        alwaysOnTop: true,
        webPreferences: {
          preload: path.join(__dirname, '../preload/index.js'),
          nodeIntegration: false,
          contextIsolation: true
        }
      });
      if (process.platform === 'win32') {
        this.updateWindow.setHasShadow(true)
      }
      if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
        this.updateWindow.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/update`)
      } else {
        // this.updateWindow.loadFile(join(__dirname, '../renderer/index.html/update'))
        this.updateWindow.loadURL(`app://./index.html#/update`)
      }
      this.updateWindow.webContents.on('did-finish-load', () => {
        this.sendToUpdateWindow('update-available', {
          oldVersion: this.updateVersionInfo.oldVersion,
          version: this.updateVersionInfo.version,
          size: (this.updateVersionInfo.files[0].size / 1024 / 1024).toFixed(1) + 'MB'
        });
      });
      globalShortcut.register('Alt+CommandOrControl+I', () => {
        this.updateWindow?.webContents.openDevTools()
      })
      this.updateWindow.on('ready-to-show', () => {
        this.updateWindow!.show();
      });

      this.updateWindow.on('closed', () => {
        this.updateWindow = null;
      });
    }
  }

  private sendToUpdateWindow(channel: string, ...args: any[]): void {
    console.log(`[Main] 发送事件 "${channel}" 到更新窗口,窗口状态:`, {
      exists: !!this.updateWindow,
      destroyed: this.updateWindow?.isDestroyed()
    });

    if (this.updateWindow && !this.updateWindow.isDestroyed()) {
      this.updateWindow.webContents.send(channel, ...args);
    } else {
      console.error(`[Main] 无法发送事件 "${channel}":窗口未初始化或已销毁`);
    }
  }
}

preload/idnex.ts文件:

import { contextBridge, ipcRenderer } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'

if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld('electron', electronAPI)
    contextBridge.exposeInMainWorld('electronAPI', {
      onUpdateAvailable: (callback) => {
        // 确保事件名称与主进程发送的完全一致
        ipcRenderer.on('update-available', (_, info) => {
          callback(info);
        });
      },
      onDownloadProgress: (callback) => {
        ipcRenderer.on('download-progress', (_, info) => {
          callback(info)
        })
      },
      onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),
      startDownload: () => ipcRenderer.invoke('start-download'),
      closeWindow: () => ipcRenderer.invoke("close-window"),
      installApp: () => ipcRenderer.invoke("install-app")
    });
  } catch (error) {
    console.error(error)
  }
} else {
  // @ts-ignore (define in dts)
  window.electron = electronAPI
}

我这里采用的是vue 路由hash方式

renderer下面的update.vue文件:

<script setup lang="ts">
import {onMounted, ref} from "vue"
import {IUpdateProgress} from "../../../types/auto-updater";

const versionTitleRef = ref(null)
const progressBarRef = ref(null)
const progressRingRef = ref(null)
const speedRef = ref(null)
const fileSizeRef = ref(null)
const totalRef = ref(null)
const isDownloading = ref(false)
const fileInfo = ref({})
const version = ref("V0.0.1")
const fileSize = ref("0MB")
const total = ref("0MB")
const speed = ref("0MB/s")
const versionTitle = ref("发现新版本可用")
const isDownloaded = ref(false)
const electronAPI: any = (window as any).electronAPI
const circumference = ref(0)
const setProgress = (percent: any) => {
  if (!isDownloading.value) {
    // 初次开始下载时添加downloading类
    progressRingRef.value.classList.add('downloading');
    isDownloading.value = true;
  }

  const offset = circumference.value - (percent / parseFloat(fileInfo.value.size)) * circumference.value;
  progressBarRef.value.style.strokeDashoffset = offset;
}


const onDownload = () => {
  if (isDownloading.value) return
  isDownloading.value = true;
  electronAPI.startDownload()
}

const onInstall = () => {
  electronAPI.installApp()
}

const onSkip = () => {
  if (isDownloading.value) return
  electronAPI.closeWindow()
}

const onDownloaded = () => {
  isDownloaded.value = true
  versionTitle.value = `下载完成`;
}

const setupListener = () => {
  if (!electronAPI) {
    console.error('electronAPI 未定义!');
  } else {
    console.log('electronAPI 已正确挂载:', Object.keys(electronAPI), electronAPI.onUpdateAvailable);
  }
  dealDefaultProgress()
  electronAPI.onDownloadProgress((progress: IUpdateProgress) => {
    console.log(progress, '下载进度')
    if (progress) {
      if (progress.speed) speed.value = `${progress.speed}`;
      if (progress.downloaded) {
        fileSize.value = `${progress.downloaded}`;
        setProgress(parseFloat(progress.downloaded))
      }
    }
  });

  electronAPI.onUpdateAvailable((info: any) => {
    console.log(info, 'eeeeeeee')
    version.value = `V ${info.oldVersion}`
    versionTitle.value = `发现新版本 ${info.version} (${info.size})`;
    total.value = info.size;
    fileInfo.value = info
  });

  electronAPI.onUpdateDownloaded((info: any) => {
    console.log("下载完成", info)
    fileSize.value = `${fileInfo.value.size}`;
    speed.value = "0MB/s"
    setProgress(parseFloat(fileInfo.value.size))
    onDownloaded()
  })
}

const dealDefaultProgress = () => {
  console.log(progressBarRef.value)
  const radius = progressBarRef.value.r.baseVal.value;
  circumference.value = radius * 2 * Math.PI;
  progressBarRef.value.style.strokeDasharray = circumference.value;
  progressBarRef.value.style.strokeDashoffset = circumference.value;
}
onMounted(() => {
  setupListener()
})
</script>

<template>
  <div class="container">
    <div ref="progressRingRef" class="progress-ring">
      <svg class="progress-circle">
        <circle class="progress-background" cx="100" cy="100" r="90"/>
        <circle ref="progressBarRef" class="progress-bar" cx="100" cy="100" r="90" stroke-dasharray="439.6"/>
      </svg>
      <div class="version-info">
        <div class="version-number" ref="versionRef" id="version-number">{{ version }}</div>
        <div class="download-info">
          <div ref="fileSizeRef" class="file-size" id="file-size">{{ fileSize }}</div>
          /
          <span ref="totalRef" id="total">{{ total }}</span>
        </div>
        <div ref="speedRef" class="speed" id="speed">{{ speed }}</div>
      </div>
    </div>

    <div ref="versionTitleRef" id="update-status">{{ versionTitle }}</div>

    <div class="button-group">
      <button v-if="!isDownloaded" :class="`btn download-btn ${isDownloading ? 'disabled' : ''}`"
              id="download-btn" @click="onDownload">{{isDownloading ? "下载中..." : "立即下载"}}</button>
      <button v-if="!isDownloaded" :class="`btn skip-btn ${isDownloading ? 'disabled' : ''}`" id="skip-btn" @click="onSkip">跳过此版本</button>
      <button v-if="isDownloaded" class="btn install-btn" id="install-btn" @click="onInstall">开始安装</button>
    </div>
  </div>
</template>

<style scoped>
:root {
  --primary: #3498db;
  --secondary: #95a5a6;
}

body {
  background: transparent;
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.container {
  background: rgba(255, 255, 255, 0.95);
  border-radius: 24px;
  padding: 40px;
  //box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
  //margin: 10px;
  width: 400px;
  text-align: center;
  -webkit-app-region: drag;
  overflow: hidden;
}

.progress-ring {
  position: relative;
  width: 200px;
  height: 200px;
  margin: 0 auto 25px;
}

.progress-circle {
  transform: rotate(-90deg);
  width: 100%;
  height: 100%;
}

.progress-circle circle {
  fill: none;
  stroke-width: 6;
  stroke-linecap: round;
}

.progress-background {
  stroke: #eee;
}

.progress-bar {
  stroke: #3498db;
  transition: stroke-dashoffset 0.3s ease;
}

.version-info {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
}

.version-number {
  font-size: 24px;
  font-weight: 600;
  color: #2c3e50;
}

.download-info {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #7f8c8d;
  font-size: 14px;
  margin-top: 8px;
}

.file-size, .speed {
  color: #7f8c8d;
  font-size: 14px;
}

.file-size {
  margin-right: 5px;
}

#total {
  margin-left: 5px;
}

.button-group {
  display: flex;
  gap: 15px;
  margin-top: 30px;
  -webkit-app-region: no-drag;
}

.btn {
  flex: 1;
  padding: 12px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 500;
  transition: all 0.2s ease;
}

.disabled{
  opacity: .5;
  cursor: not-allowed;
}

.download-btn, .install-btn {
  background: #3498db;
  color: white;
}

.skip-btn {
  background: #ecf0f1;
  color: #95a5a6;
}

.btn:hover {
  transform: translateY(-1px);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
}

#update-status {
  color: #7f8c8d;
  margin: 20px 0;
}
</style>

实现效果:

托盘右键可以点击检查更新

<think>好的,我现在要解决的问题是如何在Electron应用中手动触发应用更新,而不是依赖自动检测机制。用户希望了解具体的实现方式。我需要先回顾Electron自动更新的常规方法,然后思考如何调整这些方法来实现手动触发。 首先,根据引用[4],electron-updater库是常用的自动更新工具。通常,自动更新是通过自动检测版本并下载安装包来实现的。例如,在main.js中,可能使用autoUpdater.checkForUpdatesAndNotify()来自动检查更新。但用户需要手动触发,比如通过按钮点击来启动更新流程。 接下来,我需要确定手动触发的关键步骤。常规流程包括检查更新、下载更新安装更新手动触发的话,应该将这些步骤拆分成可由用户操作触发的函数。例如,先调用checkForUpdates,然后在回调中处理下载安装。 然后,需要确保在渲染进程中能够触发主进程的操作。这可以通过Electron的IPC(进程间通信)来实现。比如,在渲染进程中点击按钮时,发送一个IPC消息到主进程,主进程接收到消息后执行更新检查。 另外,用户可能希望有自定义的UI来显示更新状态,而不是使用默认的通知。这需要监听autoUpdater的事件,如'update-available'、'download-progress'、'update-downloaded',并将这些事件通过IPC传递到渲染进程,更新UI状态。 需要注意的是,手动触发时,可能需要禁用自动检查,例如设置autoUpdater.autoCheckForUpdates = false,避免自动检测干扰手动操作。同时,确保在手动触发时处理错误情况,比如网络问题或版本检查失败,给用户适当的反馈。 还要考虑代码的具体实现。例如,在main.js中,设置手动检查的函数,绑定到IPC事件。在渲染进程中,添加按钮并绑定点击事件,通过IPC调用主进程的检查函数。同时,处理下载进度完成后的提示,让用户决定何时安装更新。 最后,测试整个流程是否顺畅,确保手动触发能正确执行检查、下载安装步骤。如果有问题,可能需要调试IPC通信或事件监听的逻辑。</think>### Electron手动触发应用更新实现方式 在Electron中实现**手动触发应用更新**的关键在于控制`electron-updater`的检测与下载流程,并通过进程间通信(IPC)绑定用户操作。以下是具体实现步骤: --- #### 一、主进程配置 1. **禁用自动检测** 在`main.js`中关闭自动检测功能: ```javascript const { autoUpdater } = require('electron-updater') autoUpdater.autoCheckForUpdates = false // 关闭自动检测 ``` 2. **暴露手动更新方法** 通过IPC暴露手动触发接口: ```javascript const { ipcMain } = require('electron') ipcMain.on('manual-check-update', () => { autoUpdater.checkForUpdates().then(result => { if (result?.updateInfo.version) { // 发现新版本后自动下载 autoUpdater.downloadUpdate() } }).catch(err => { console.error('检查更新失败:', err) }) }) ``` --- #### 二、渲染进程触发 在页面中添加手动触发按钮,并通过IPC调用主进程方法: ```javascript // 渲染进程(如React/Vue) const { ipcRenderer } = window.require('electron') function handleManualUpdate() { ipcRenderer.send('manual-check-update') } // 监听下载进度 ipcRenderer.on('download-progress', (event, progress) => { console.log(`下载进度: ${progress.percent}%`) }) // 监听下载完成 ipcRenderer.on('update-downloaded', () => { if (confirm('新版本已下载,立即安装?')) { ipcRenderer.send('quit-and-install') } }) ``` --- #### 三、事件监听与反馈 在主进程中监听更新事件,并转发到渲染进程: ```javascript // main.js autoUpdater.on('update-available', () => { mainWindow.webContents.send('update-available') }) autoUpdater.on('download-progress', (progress) => { mainWindow.webContents.send('download-progress', progress) }) autoUpdater.on('update-downloaded', () => { mainWindow.webContents.send('update-downloaded') }) // 安装更新 ipcMain.on('quit-and-install', () => { autoUpdater.quitAndInstall() }) ``` --- #### 四、流程对比 | 步骤 | 自动更新 | 手动更新 | |------------|------------------------------|------------------------------| | 检测触发 | 启动时自动检测[^4] | 用户点击按钮触发 | | 下载控制 | 发现更新后自动下载 | 可自定义下载时机 | | UI反馈 | 默认系统通知 | 完全自定义界面 | | 安装决策 | 自动提示安装 | 用户确认后安装 | --- #### 五、注意事项 1. **更新服务器配置** 需确保`electron-builder`配置了正确的发布地址(`publish`字段)[^2]。 2. **错误处理** 添加`error`事件监听,处理网络故障或签名问题: ```javascript autoUpdater.on('error', (error) => { mainWindow.webContents.send('update-error', error.message) }) ``` 3. **代码签名** macOS/Windows必须配置代码签名,否则安装会失败[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值