Electron 应用打包与自动更新

📋 目录

1. 打包基础概念

1.1 Electron 打包流程

┌─────────────────────────────────────────────────────────────┐
│                    Electron 打包流程                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 开发阶段                                                │
│     src/main → src/preload → src/renderer                   │
│         ↓              ↓              ↓                    │
│     ┌─────────────────────────────────────────┐            │
│     │           electron-vite build            │            │
│     └─────────────────────────────────────────┘            │
│                          ↓                                  │
│     ┌─────────────────────────────────────────┐            │
│     │  out/main/    out/preload/   out/renderer/│            │
│     └─────────────────────────────────────────┘            │
│                          ↓                                  │
│  2. 打包阶段                                                │
│     ┌─────────────────────────────────────────┐            │
│     │         electron-builder 打包           │            │
│     └─────────────────────────────────────────┘            │
│                          ↓                                  │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐            │
│  │  Windows   │  │   macOS    │  │   Linux    │            │
│  │  .exe/.msi │  │  .dmg/.pkg │  │ .AppImage  │            │
│  └────────────┘  └────────────┘  └────────────┘            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 打包工具对比

工具特性适用场景学习曲线
electron-builder功能全面、生态成熟通用场景⭐⭐
electron-forge官方推荐、模块化新项目⭐⭐
electron-packager简单轻量快速打包

1.3 打包产物类型

平台产物格式说明
Windows.exe (NSIS)Windows 安装向导
.msiMicrosoft Installer
.portable.exe便携版,无需安装
macOS.dmg磁盘镜像,最常用
.pkgmacOS 安装包
.zip压缩包
Linux.AppImage通用格式
.debDebian/Ubuntu
.rpmRedHat/Fedora

2. electron-builder 配置详解

2.1 安装依赖

# 安装 electron-builder
npm install electron-builder --save-dev

# 可选:安装代码签名工具
npm install electron-builder-sodium --save-dev
npm install @electron/osx-sign --save-dev

2.2 基础配置

{
  "name": "gamebox-app",
  "version": "1.0.0",
  "description": "GameBox - 跨平台游戏盒子",
  "main": "./out/main/index.js",
  "author": "erick",
  "license": "MIT",

  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "build:win": "npm run build && electron-builder --win",
    "build:mac": "npm run build && electron-builder --mac",
    "build:linux": "npm run build && electron-builder --linux",
    "build:all": "npm run build && electron-builder --win --mac --linux"
  },

  "build": {
    "appId": "com.erick.gamebox",
    "productName": "GameBox",
    "copyright": "Copyright © 2026 erick",

    "directories": {
      "output": "dist",
      "buildResources": "build",
      "outputVariation": "published"
    },

    "files": [
      "out/**/*",
      "!node_modules/**/*",
      "package.json"
    ],

    "extraResources": [
      {
        "from": "resources/",
        "to": "resources/",
        "filter": ["**/*"]
      }
    ],

    "win": {
      "target": ["nsis"],
      "icon": "build/icon.ico"
    },

    "mac": {
      "target": ["dmg", "zip"],
      "icon": "build/icon.icns",
      "category": "public.app-category.games",
      "hardenedRuntime": true,
      "gatekeeperAssess": false
    },

    "linux": {
      "target": ["AppImage", "deb"],
      "icon": "resources/icon.png",
      "category": "Game"
    }
  }
}

2.3 files 配置详解

{
  "files": [
    // 包含 out 目录下的所有文件
    "out/**/*",

    // 排除特定文件
    "!out/**/*.map",
    "!out/**/*.tmp",

    // 包含特定文件
    "package.json",

    // 使用 glob 模式
    "!**/*.ts",
    "!**/*.map",

    // 包含 node_modules 中的特定包
    "!node_modules/**/*",
    "node_modules/electron-store/**/*",
    "node_modules/conf/**/*"
  ]
}

2.4 extraResources 配置

{
  "extraResources": [
    // 复制整个目录
    {
      "from": "resources/",
      "to": "resources/",
      "filter": ["**/*"]
    },

    // 复制单个文件
    {
      "from": "build/version.txt",
      "to": "version.txt"
    },

    // 根据平台条件复制
    {
      "from": "build/",
      "to": "build/",
      "filter": ["**/*"],
      "context": {
        "electronPlatformName": "darwin"
      }
    }
  ]
}

3. 多平台打包

3.1 Windows 打包配置

{
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      },
      {
        "target": "msi",
        "arch": ["x64"]
      },
      {
        "target": "portable",
        "arch": ["x64"]
      }
    ],
    "icon": "build/icon.ico",
    "artifactName": "${productName}-${version}-${arch}-setup.${ext}",
    "publisherName": "erick",
    "requestedExecutionLevel": "asInvoker",
    "applicationId": "GameBox"
  },

  "nsis": {
    "oneClick": false,
    "perMachine": false,
    "allowToChangeInstallationDirectory": true,
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "shortcutName": "GameBox",
    "installerIcon": "build/installer.ico",
    "uninstallerIcon": "build/uninstaller.ico",
    "installerHeaderIcon": "build/installer.ico",
    "license": "LICENSE.txt",
    "runAfterFinish": true,
    "deleteAppDataOnUninstall": false,
    "include": "build/installer.nsh"
  },

  "portable": {
    "artifactName": "${productName}-${version}-portable.${ext}"
  }
}

3.2 macOS 打包配置

{
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "zip",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "pkg",
        "arch": ["x64"]
      }
    ],
    "icon": "build/icon.icns",
    "category": "public.app-category.games",
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.plist",
    "entitlementsInherit": "build/entitlements.plist",
    "extendInfo": {
      "NSHumanReadableCopyright": "Copyright © 2026 erick",
      "NSPrincipalClass": "NSApplication"
    },
    "artifactName": "${productName}-${version}-${arch}.${ext}"
  },

  "dmg": {
    "title": "${productName} ${version}",
    "contents": [
      {
        "x": 130,
        "y": 220,
        "type": "file"
      },
      {
        "x": 410,
        "y": 220,
        "type": "link",
        "path": "/Applications"
      }
    ],
    "window": {
      "x": 100,
      "y": 100,
      "width": 540,
      "height": 380
    }
  }
}

3.3 Linux 打包配置

{
  "linux": {
    "target": [
      {
        "target": "AppImage",
        "arch": ["x64"]
      },
      {
        "target": "deb",
        "arch": ["x64"]
      },
      {
        "target": "rpm",
        "arch": ["x64"]
      }
    ],
    "icon": "resources/icon.png",
    "category": "Game",
    "maintainer": "erick@example.com",
    "vendor": "GameBox",
    "synopsis": "A cross-platform game launcher",
    "description": "GameBox is a cross-platform game launcher application",
    "artifactName": "${productName}-${version}-${arch}.${ext}",
    "executableName": "gamebox"
  },

  "deb": {
    "depends": ["libgtk-3-0", "libnotify4", "libnss3"],
    "recommends": ["libgnome-keyring0", "libasound2"]
  },

  "rpm": {
    "depends": ["libgtk-3-0", "libnotify", "nss"],
    "suggests": ["libgnome-keyring", "alsa-lib"]
  }
}

3.4 跨平台打包命令

# Windows 打包
npm run build:win

# macOS 打包
npm run build:mac

# Linux 打包
npm run build:linux

# 全部平台打包
npm run build:all

# 指定架构
npx electron-builder --win --x64
npx electron-builder --mac --arm64

# 仅生成安装包(跳过应用包)
npx electron-builder --dir

# 查看所有可用选项
npx electron-builder --help

4. 安装包定制

4.1 NSIS 脚本定制

; build/installer.nsh
; NSIS 安装脚本自定义

!macro customHeader
  ; 自定义安装程序头部
!macroend

!macro preInit
  ; 安装前初始化
  SetRegView 64
!macroend

!macro customInit
  ; 自定义初始化
!macroend

!macro customInstall
  ; 自定义安装逻辑

  ; 创建桌面快捷方式
  CreateShortCut "$DESKTOP\GameBox.lnk" "$INSTDIR\GameBox.exe"

  ; 创建开始菜单快捷方式
  CreateDirectory "$SMPROGRAMS\GameBox"
  CreateShortCut "$SMPROGRAMS\GameBox\GameBox.lnk" "$INSTDIR\GameBox.exe"
  CreateShortCut "$SMPROGRAMS\GameBox\卸载.lnk" "$INSTDIR\Uninstall GameBox.exe"

  ; 注册表配置
  WriteRegStr HKCU "Software\GameBox" "" "$INSTDIR"
  WriteRegStr HKCU "Software\GameBox" "Version" "${VERSION}"

  ; 关联文件类型(可选)
  WriteRegStr HKCR ".gbx" "GameBox Game File" ""
  WriteRegStr HKCR ".gbx\DefaultIcon" "" "$INSTDIR\GameBox.exe,0"
  WriteRegStr HKCR ".gbx\shell\open\command" "" '"$INSTDIR\GameBox.exe" "%1"'
!macroend

!macro customUnInstall
  ; 自定义卸载逻辑

  ; 删除快捷方式
  Delete "$DESKTOP\GameBox.lnk"
  Delete "$SMPROGRAMS\GameBox\GameBox.lnk"
  Delete "$SMPROGRAMS\GameBox\卸载.lnk"
  RMDir "$SMPROGRAMS\GameBox"

  ; 清理注册表
  DeleteRegKey HKCU "Software\GameBox"
  DeleteRegKey HKCR ".gbx"

  ; 清理用户数据(可选)
  RMDir /r "$LOCALAPPDATA\GameBox"
!macroend

4.2 macOS DMG 自定义

{
  "dmg": {
    "title": "${productName} ${version}",
    "background": "build/dmg-background.png",
    "icon": "build/dmg-icon.icns",
    "iconSize": 100,
    "iconTextSize": 12,
    "window": {
      "x": 100,
      "y": 100,
      "width": 600,
      "height": 450
    },
    "contents": [
      {
        "x": 180,
        "y": 170,
        "type": "file",
        "path": "dist/mac/GameBox.app"
      },
      {
        "x": 410,
        "y": 170,
        "type": "link",
        "path": "/Applications"
      },
      {
        "x": 180,
        "y": 360,
        "type": "file",
        "path": "../../LICENSE"
      }
    ],
    "sign": true
  }
}

4.3 应用图标准备

# 创建图标目录
mkdir -p build resources

# 图标尺寸要求:
# Windows: .ico (256x256)
# macOS: .icns (16, 32, 64, 128, 256, 512, 1024)
# Linux: .png (最小 256x256)

# 使用工具转换图标(安装 imagemagick)
# convert source.png -resize 256x256 icon.png

# 生成多尺寸图标脚本
# icns 格式需要特殊处理,通常使用:
# - electron-icon-builder
# - icon-gen

4.4 图标生成工具

# 安装图标生成工具
npm install --save-dev electron-icon-builder

# 生成所有尺寸图标
npx electron-icon-builder --source=build/icon.png --output=build --flatten
// 或使用 JS 脚本
const { execSync } = require('child_process')

function generateIcons() {
  const sizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]

  sizes.forEach(size => {
    console.log(`生成 ${size}x${size} 图标...`)
    // 使用 sharp 或 jimp 处理
  })
}

generateIcons()

5. 代码签名

5.1 Windows 代码签名

方式一:使用 Azure SignTool

# 安装 Azure SignTool
npm install --save-dev azure-signing-tool

# Windows 代码签名配置
{
  "win": {
    "sign": {
      "tool": "azure-signing-tool",
      " AzureVaultUrl": "https://your-keyvault.vault.azure.net",
      "AzureClientId": "your-client-id",
      "AzureClientSecret": "your-client-secret",
      "AzureTenantId": "your-tenant-id",
      "AzureCertificateName": "your-certificate-name",
      "timestampServerUrl": "http://timestamp.digicert.com",
      "digestAlgorithm": "sha256",
      "description": "GameBox Application",
      "descriptionUrl": "https://gamebox.example.com"
    }
  }
}

方式二:使用 osslsigncode

# 安装 osslsigncode(需要预装)
# 下载地址: https://github.com/mtrofin/osslsigncode/releases

# 签名脚本
const { execSync } = require('child_process')

function signWindowsExe(exePath, certPath, certPassword) {
  const cmd = [
    'osslsigncode',
    '-ts', 'http://timestamp.digicert.com',
    '-n', 'GameBox',
    '-i', 'https://gamebox.example.com',
    '-h', 'sha256',
    '-pkcs12', certPath,
    '-pass', certPassword,
    '-in', exePath,
    '-out', exePath.replace('.exe', '-signed.exe')
  ].join(' ')

  execSync(cmd, { stdio: 'inherit' })
}

5.2 macOS 代码签名

签名配置文件

<!-- build/entitlements.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 应用沙箱权限 -->
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>

    <!-- 网络访问 -->
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>

    <!-- 文件访问 -->
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.files.downloads.read-write</key>
    <true/>

    <!-- 游戏手柄 -->
    <key>com.apple.security.game-center</key>
    <true/>
</dict>
</plist>

签名配置

{
  "mac": {
    "hardenedRuntime": true,
    "gatekeeperAssess": false,
    "entitlements": "build/entitlements.plist",
    "entitlementsInherit": "build/entitlements.plist",

    "sign": {
      "tool": "electron-builder-sodium",
      "identity": "Developer ID Application: Your Name (TEAM_ID)",
      "keychain": "~/Library/Keychains/login.keychain-db",
      "timestampServerUrl": "http://timestamp.apple.com/ts0416",
      "digestAlgorithm": "sha256"
    }
  }
}

手动签名命令

# 1. 设置钥匙串访问
security unlock-keychain ~/Library/Keychains/login.keychain-db

# 2. 签名应用
codesign --force --deep --sign "Developer ID Application: Your Name (TEAM_ID)" \
  --entitlements build/entitlements.plist \
  --options runtime \
  "dist/mac/GameBox.app"

# 3. 验证签名
codesign --verify --verbose=2 "dist/mac/GameBox.app"
spctl --assess --type exec --verbose=4 "dist/mac/GameBox.app"

# 4. 创建 DMG 并签名
hdiutil create -volname "GameBox" -srcfolder "dist/mac/GameBox.app" -ov -format UDZO "GameBox.dmg"

# 5. 签名 DMG
codesign --sign "Developer ID Application: Your Name (TEAM_ID)" "GameBox.dmg"

5.3 Linux 代码签名

# Linux 应用通常不需要代码签名
# 但可以添加 GPG 签名

# 创建 GPG 密钥
gpg --full-generate-key

# 导出签名密钥
gpg --armor --export your@email.com > public.key

# 签名打包文件
gpg --armor --detach-sign GameBox-1.0.0.AppImage

6. 自动更新机制

6.1 更新架构

┌─────────────────────────────────────────────────────────────┐
│                    自动更新流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 检查更新                                                │
│     ┌──────────┐     ┌─────────────┐                       │
│     │ 客户端   │────→│ 更新服务器   │                       │
│     │ 检测版本  │←────│ 返回新版本   │                       │
│     └──────────┘     └─────────────┘                       │
│                                                             │
│  2. 下载更新                                                │
│     ┌──────────┐     ┌─────────────┐                       │
│     │ 客户端   │────→│ GitHub Releases│                      │
│     │ 下载包    │←────│ 下载文件     │                       │
│     └──────────┘     └─────────────┘                       │
│                                                             │
│  3. 安装更新                                                │
│     ┌──────────┐                                           │
│     │ 客户端   │                                           │
│     │ 安装重启  │                                           │
│     └──────────┘                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.2 安装 electron-updater

# 安装更新依赖
npm install electron-updater --save
npm install electron-log --save

6.3 主进程更新配置

// src/main/update.ts
import { autoUpdater } from 'electron-updater'
import { BrowserWindow, ipcMain, dialog } from 'electron'
import log from 'electron-log'

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

// 自动更新配置
const updateConfig = {
  // 允许预发布版本
  allowPreRelease: false,

  // 自动下载(false=手动触发下载)
  autoDownload: false,

  // 下载重试次数
  downloadRetryCount: 3,

  // 下载超时(毫秒)
  downloadTimeout: 30000,

  // 代理设置
  proxy: undefined
}

export function setupAutoUpdater(mainWindow: BrowserWindow): void {
  // 应用更新配置
  autoUpdater.autoDownload = updateConfig.autoDownload
  autoUpdater.allowPreRelease = updateConfig.allowPreRelease

  // 监听进度
  autoUpdater.on('checking-for-update', () => {
    log.info('🔍 检查更新中...')
    mainWindow.webContents.send('update-status', {
      status: 'checking',
      message: '检查更新中...'
    })
  })

  // 发现新版本
  autoUpdater.on('update-available', (info) => {
    log.info('🎉 发现新版本:', info.version)
    mainWindow.webContents.send('update-status', {
      status: 'available',
      message: `发现新版本 ${info.version}`,
      version: info.version,
      releaseDate: info.releaseDate,
      releaseNotes: info.releaseNotes
    })
  })

  // 没有可用更新
  autoUpdater.on('update-not-available', (info) => {
    log.info('✅ 当前已是最新版本')
    mainWindow.webContents.send('update-status', {
      status: 'not-available',
      message: '当前已是最新版本'
    })
  })

  // 下载进度
  autoUpdater.on('download-progress', (progressObj) => {
    const percent = Math.round(progressObj.percent)
    log.info(`📥 下载进度: ${percent}%`)
    mainWindow.webContents.send('update-status', {
      status: 'downloading',
      message: `下载中... ${percent}%`,
      percent
    })
  })

  // 下载完成
  autoUpdater.on('update-downloaded', (info) => {
    log.info('✅ 下载完成:', info.version)
    mainWindow.webContents.send('update-status', {
      status: 'downloaded',
      message: '下载完成,可以安装更新',
      version: info.version
    })

    // 提示用户安装
    dialog.showMessageBox(mainWindow, {
      type: 'info',
      title: '更新下载完成',
      message: `新版本 ${info.version} 已下载完成`,
      detail: '是否立即安装更新并重启应用?',
      buttons: ['立即重启', '稍后再说'],
      defaultId: 0,
      cancelId: 1
    }).then(result => {
      if (result.response === 0) {
        autoUpdater.quitAndInstall()
      }
    })
  })

  // 更新错误
  autoUpdater.on('error', (error) => {
    log.error('❌ 更新错误:', error)
    mainWindow.webContents.send('update-status', {
      status: 'error',
      message: `更新失败: ${error.message}`
    })
  })

  // IPC 处理程序
  ipcMain.handle('check-for-updates', async () => {
    try {
      const result = await autoUpdater.checkForUpdates()
      return result
    } catch (error) {
      log.error('检查更新失败:', error)
      return null
    }
  })

  ipcMain.handle('download-update', async () => {
    try {
      const result = await autoUpdater.downloadUpdate()
      return result
    } catch (error) {
      log.error('下载更新失败:', error)
      return null
    }
  })

  ipcMain.handle('install-update', () => {
    autoUpdater.quitAndInstall()
  })
}

6.4 渲染进程更新组件

<!-- src/renderer/src/components/UpdateDialog.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const updateStatus = ref<'checking' | 'available' | 'downloading' | 'downloaded' | 'error' | 'idle'>('idle')
const updateMessage = ref('')
const downloadPercent = ref(0)
const newVersion = ref('')
const showDialog = ref(false)

onMounted(() => {
  // 监听更新状态
  window.electron.on('update-status', (status: any) => {
    updateStatus.value = status.status
    updateMessage.value = status.message

    if (status.percent !== undefined) {
      downloadPercent.value = status.percent
    }

    if (status.version) {
      newVersion.value = status.version
    }

    if (status.status === 'available' || status.status === 'downloaded') {
      showDialog.value = true
    }
  })
})

onUnmounted(() => {
  window.electron.removeAllListeners('update-status')
})

async function checkUpdate() {
  updateStatus.value = 'checking'
  updateMessage.value = '检查更新中...'
  await window.electron.checkForUpdates()
}

async function downloadUpdate() {
  await window.electron.downloadUpdate()
}

function installUpdate() {
  window.electron.installUpdate()
}

function dismissDialog() {
  showDialog.value = false
}
</script>

<template>
  <div class="update-dialog" v-if="showDialog">
    <div class="dialog-overlay" @click="dismissDialog"></div>
    <div class="dialog-content">
      <div class="dialog-header">
        <h3>🎉 发现新版本</h3>
      </div>

      <div class="dialog-body">
        <template v-if="updateStatus === 'available'">
          <p>新版本 <strong>{{ newVersion }}</strong> 已发布</p>
          <p class="desc">{{ updateMessage }}</p>
          <button class="btn-primary" @click="downloadUpdate">
            立即下载
          </button>
        </template>

        <template v-else-if="updateStatus === 'downloading'">
          <p>正在下载更新...</p>
          <div class="progress-bar">
            <div 
              class="progress-fill" 
              :style="{ width: downloadPercent + '%' }"
            ></div>
          </div>
          <p class="percent">{{ downloadPercent }}%</p>
        </template>

        <template v-else-if="updateStatus === 'downloaded'">
          <p>✅ 更新已下载完成</p>
          <p class="desc">{{ updateMessage }}</p>
          <button class="btn-primary" @click="installUpdate">
            立即重启安装
          </button>
        </template>

        <template v-else-if="updateStatus === 'error'">
          <p class="error">❌ {{ updateMessage }}</p>
          <button class="btn-secondary" @click="dismissDialog">
            关闭
          </button>
        </template>
      </div>
    </div>
  </div>
</template>

<style scoped>
.update-dialog {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.dialog-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
}

.dialog-content {
  position: relative;
  background: #1e1e2f;
  border-radius: 12px;
  padding: 24px;
  min-width: 320px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
}

.dialog-header h3 {
  margin: 0 0 16px;
  font-size: 20px;
  color: #fff;
}

.dialog-body {
  text-align: center;
}

.dialog-body p {
  margin: 0 0 16px;
  color: #a0a0a0;
}

.dialog-body .desc {
  font-size: 14px;
  color: #666;
}

.progress-bar {
  height: 8px;
  background: #333;
  border-radius: 4px;
  overflow: hidden;
  margin: 16px 0;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #667eea, #764ba2);
  transition: width 0.3s;
}

.percent {
  font-size: 24px;
  font-weight: bold;
  color: #667eea;
}

.error {
  color: #e74c3c;
}

.btn-primary {
  width: 100%;
  padding: 12px 24px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  border: none;
  border-radius: 8px;
  color: #fff;
  font-size: 16px;
  cursor: pointer;
  transition: opacity 0.3s;
}

.btn-primary:hover {
  opacity: 0.9;
}

.btn-secondary {
  width: 100%;
  padding: 12px 24px;
  background: #333;
  border: none;
  border-radius: 8px;
  color: #fff;
  font-size: 16px;
  cursor: pointer;
}
</style>

6.5 GitHub Releases 配置

# .github/workflows/release.yml
name: Build and Release

on:
  push:
    tags:
      - 'v*'

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

    steps:
      - uses: actions/checkout@v4

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

      - name: Install dependencies
        run: npm ci

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

      - name: Build and release
        uses: electron-builder/action@v1
        with:
          args: --publish always
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.os }}-build
          path: dist/

7. CI/CD 集成

7.1 GitHub Actions 工作流

# .github/workflows/build.yml
name: Electron Build

on:
  push:
    branches: [main]
    tags:
      - 'v*'
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '20'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  build-windows:
    needs: lint
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build:win

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: windows-build
          path: dist/

  build-mac:
    needs: lint
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build:mac

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: mac-build
          path: dist/

  build-linux:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build:linux

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: linux-build
          path: dist/

  release:
    needs: [build-windows, build-mac, build-linux]
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          path: dist/

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: dist/**/*.exe
          files: dist/**/*.dmg
          files: dist/**/*.AppImage
          files: dist/**/*.deb
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

7.2 GitLab CI/CD

# .gitlab-ci.yml
stages:
  - lint
  - build
  - release

variables:
  NODE_VERSION: "20"

lint:
  stage: lint
  image: node:${NODE_VERSION}
  script:
    - npm ci
    - npm run lint
  only:
    - main
    - merge_requests

build:windows:
  stage: build
  image: electronuserland/builder:wine
  script:
    - npm ci
    - npm run build:win
  artifacts:
    paths:
      - dist/
  only:
    - main
    - tags

build:mac:
  stage: build
  image: electronuserland/builder:mac
  script:
    - npm ci
    - npm run build:mac
  artifacts:
    paths:
      - dist/
  only:
    - main
    - tags

build:linux:
  stage: build
  image: electronuserland/builder
  script:
    - npm ci
    - npm run build:linux
  artifacts:
    paths:
      - dist/
  only:
    - main
    - tags

7.3 Jenkins 流水线

// Jenkinsfile
pipeline {
    agent any

    environment {
        NODE_VERSION = '20'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Install') {
            steps {
                bat 'npm ci'
            }
        }

        stage('Lint') {
            steps {
                bat 'npm run lint'
            }
        }

        stage('Build') {
            parallel {
                stage('Windows') {
                    steps {
                        bat 'npm run build:win'
                        archiveArtifacts artifacts: 'dist/**/*.exe', fingerprint: true
                    }
                }
                stage('macOS') {
                    when { not { equals expected: 'windows', actual: env.AGENT_OS } }
                    steps {
                        sh 'npm run build:mac'
                        archiveArtifacts artifacts: 'dist/**/*.dmg', fingerprint: true
                    }
                }
                stage('Linux') {
                    when { not { equals expected: 'windows', actual: env.AGENT_OS } }
                    steps {
                        sh 'npm run build:linux'
                        archiveArtifacts artifacts: 'dist/**/*.AppImage', fingerprint: true
                    }
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
    }
}

8. 实战:GameBox 完整打包配置

8.1 完整 package.json

{
  "name": "gamebox-app",
  "version": "1.0.0",
  "description": "GameBox - 跨平台游戏盒子",
  "main": "./out/main/index.js",
  "author": {
    "name": "erick",
    "email": "erick@example.com"
  },
  "license": "MIT",
  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "preview": "electron-vite preview",
    "postinstall": "electron-builder install-app-deps",

    "build:win": "npm run build && electron-builder --win",
    "build:mac": "npm run build && electron-builder --mac",
    "build:linux": "npm run build && electron-builder --linux",
    "build:all": "npm run build && electron-builder --win --mac --linux",

    "dist": "npm run build && electron-builder",
    "dist:dir": "npm run build && electron-builder --dir",

    "sign:mac": "electron-builder-sodium",
    "sign:win": "azure-signing-tool sign"
  },

  "dependencies": {
    "@vueuse/core": "^10.7.0",
    "electron-log": "^5.0.0",
    "electron-store": "^8.1.0",
    "electron-updater": "^6.1.0",
    "pinia": "^2.1.7",
    "vue": "^3.4.0",
    "vue-router": "^4.2.5"
  },

  "devDependencies": {
    "@electron-toolkit/preload": "^3.0.0",
    "@electron-toolkit/tsconfig": "^1.0.1",
    "@electron-toolkit/utils": "^3.0.0",
    "@types/node": "^20.10.0",
    "@vitejs/plugin-vue": "^5.0.0",
    "electron": "^28.0.0",
    "electron-builder": "^24.9.1",
    "electron-icon-builder": "^2.0.1",
    "electron-vite": "^2.0.0",
    "typescript": "^5.3.0",
    "vite": "^5.0.0",
    "vue-tsc": "^1.8.25"
  },

  "build": {
    "appId": "com.erick.gamebox",
    "productName": "GameBox",
    "copyright": "Copyright © 2026 erick",

    "asar": true,
    "asarUnpack": [
      "resources/**"
    ],

    "directories": {
      "output": "release/${version}",
      "buildResources": "build",
      "app": "out"
    },

    "files": [
      "out/**/*",
      "!out/**/*.map",
      "package.json"
    ],

    "extraResources": [
      {
        "from": "resources/",
        "to": "resources/",
        "filter": ["**/*"]
      }
    ],

    "publish": [
      {
        "provider": "github",
        "owner": "your-username",
        "repo": "gamebox-app"
      }
    ],

    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": ["x64"]
        },
        {
          "target": "portable",
          "arch": ["x64"]
        }
      ],
      "icon": "build/icon.ico",
      "artifactName": "${productName}-${version}-${arch}-setup.${ext}",
      "publisherName": "erick"
    },

    "nsis": {
      "oneClick": false,
      "perMachine": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "GameBox",
      "license": "LICENSE.txt",
      "include": "build/installer.nsh"
    },

    "portable": {
      "artifactName": "${productName}-${version}-portable.${ext}"
    },

    "mac": {
      "target": [
        {
          "target": "dmg",
          "arch": ["x64", "arm64"]
        },
        {
          "target": "zip",
          "arch": ["x64", "arm64"]
        }
      ],
      "icon": "build/icon.icns",
      "category": "public.app-category.games",
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "build/entitlements.plist",
      "entitlementsInherit": "build/entitlements.plist",
      "artifactName": "${productName}-${version}-${arch}.${ext}",
      "extendInfo": {
        "NSHumanReadableCopyright": "Copyright © 2026 erick"
      }
    },

    "dmg": {
      "title": "${productName} ${version}",
      "contents": [
        {
          "x": 130,
          "y": 220,
          "type": "file"
        },
        {
          "x": 410,
          "y": 220,
          "type": "link",
          "path": "/Applications"
        }
      ]
    },

    "linux": {
      "target": [
        {
          "target": "AppImage",
          "arch": ["x64"]
        },
        {
          "target": "deb",
          "arch": ["x64"]
        }
      ],
      "icon": "resources/icon.png",
      "category": "Game",
      "maintainer": "erick@example.com",
      "vendor": "GameBox",
      "artifactName": "${productName}-${version}-${arch}.${ext}",
      "executableName": "gamebox"
    }
  }
}

8.2 版本管理脚本

// scripts/version.js
const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')

// 获取当前版本
function getCurrentVersion() {
  const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
  return pkg.version
}

// 更新版本
function bumpVersion(type = 'patch') {
  const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
  const [major, minor, patch] = pkg.version.split('.').map(Number)

  let newVersion
  switch (type) {
    case 'major':
      newVersion = `${major + 1}.0.0`
      break
    case 'minor':
      newVersion = `${major}.${minor + 1}.0`
      break
    case 'patch':
    default:
      newVersion = `${major}.${minor}.${patch + 1}`
  }

  pkg.version = newVersion
  fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n')

  console.log(`版本更新: ${pkg.version} → ${newVersion}`)
  return newVersion
}

// 创建 Git tag
function createTag(version) {
  try {
    execSync(`git tag v${version}`, { stdio: 'inherit' })
    console.log(`✅ Git tag v${version} 已创建`)
  } catch (error) {
    console.error(`❌ 创建 tag 失败: ${error.message}`)
  }
}

// 主流程
const args = process.argv.slice(2)
const type = args[0] || 'patch'

const newVersion = bumpVersion(type)
createTag(newVersion)
# 使用脚本
node scripts/version.js patch   # 1.0.0 -> 1.0.1
node scripts/version.js minor   # 1.0.0 -> 1.1.0
node scripts/version.js major   # 1.0.0 -> 2.0.0

# 发布
git push origin main --tags

9. 排错指南

9.1 常见错误与解决方案

错误类型错误信息原因解决方案
依赖错误Failed to run install scriptnative 模块编译失败npm install --ignore-scripts 后手动编译
图标缺失icon.ico not found图标路径错误检查 buildResources 配置
ASAR 错误Cannot read property of undefined资源未正确打包添加 asarUnpack 配置
代码签名失败sign failed with exit code 1证书无效/过期检查证书有效期
权限错误EACCES: permission denied文件权限问题使用管理员权限运行
路径错误Cannot find module打包路径问题检查 files 配置
NSIS 错误Aborting creation of installer脚本语法错误检查 .nsh 文件语法

9.2 Windows 常见问题

# 问题:缺少 Visual C++ 运行时
# 解决:在 NSIS 配置中添加依赖
{
  "win": {
    "target": "nsis"
  },
  "nsis": {
    "include": "build/installer.nsh"
  }
}

# build/installer.nsh
!include "WinMessages.nsh"
!include "FileFunc.nsh"

!macro customInstall
  ; 安装 VC++ 运行库
  DetailPrint "Installing Visual C++ Runtime..."
  ExecWait '"$TEMP\vcruntime140_amd64.exe /install /quiet /norestart"'
!macroend
# 问题:Windows Defender 阻止
# 解决:代码签名 + 白名单申请

# 问题:用户权限不足
# 解决:设置 requestedExecutionLevel
{
  "win": {
    "requestedExecutionLevel": "requireAdministrator"
  }
}

9.3 macOS 常见问题

# 问题:Gatekeeper 阻止
# 解决:使用有效开发者签名

# 问题:应用崩溃 on macOS 13+
# 解决:添加 entitlements
{
  "mac": {
    "hardenedRuntime": true,
    "entitlements": "build/entitlements.plist"
  }
}

# 问题:DMG 签名失败
# 解决:先签名应用,再签名 DMG
codesign --sign "Developer ID" "GameBox.app"
hdiutil create -srcfolder "GameBox.app" "GameBox.dmg"
codesign --sign "Developer ID" "GameBox.dmg"

9.4 Linux 常见问题

# 问题:AppImage 无法运行
# 解决:确保可执行权限
chmod +x GameBox-*.AppImage

# 问题:依赖库缺失
# 解决:检查依赖
ldd GameBox
# 或使用 appimage-relate-tool
appimageextract GameBox.AppImage

9.5 更新相关问题

// 问题:更新检测失败
// 解决:检查网络和发布配置

// 1. 检查 publish 配置
{
  "build": {
    "publish": [
      {
        "provider": "github",
        "owner": "your-username",
        "repo": "your-repo"
      }
    ]
  }
}

// 2. 检查 GitHub Token 权限
// Settings -> Developer settings -> Personal access tokens
// 需要 repo:status 和 public_repo 权限

// 3. 添加调试日志
autoUpdater.logger = log
log.transports.file.level = 'debug'

9.6 构建诊断脚本

#!/bin/bash
# scripts/diagnose.sh - 构建诊断脚本

echo "======================================"
echo "  Electron 构建环境诊断"
echo "======================================"

echo ""
echo "📋 系统信息:"
echo "  操作系统: $(uname -s)"
echo "  架构: $(uname -m)"
echo "  Node 版本: $(node --version)"
echo "  npm 版本: $(npm --version)"

echo ""
echo "📦 全局安装的工具:"
npm list -g --depth=0 2>/dev/null || echo "  (无)"

echo ""
echo "🔍 electron-builder 版本:"
npx electron-builder --version

echo ""
echo "🖥️ Electron 版本:"
npm list electron

echo ""
echo "📂 项目文件检查:"
echo "  package.json: $([ -f package.json ] && echo '✅' || echo '❌')"
echo "  electron.vite.config.ts: $([ -f electron.vite.config.ts ] && echo '✅' || echo '❌')"
echo "  build/icon.ico: $([ -f build/icon.ico ] && echo '✅' || echo '❌')"
echo "  build/icon.icns: $([ -f build/icon.icns ] && echo '✅' || echo '❌')"

echo ""
echo "💾 磁盘空间:"
df -h . | tail -1

echo ""
echo "======================================"
echo "  诊断完成"
echo "======================================"

10. 最佳实践

10.1 打包最佳实践清单

✅ 必须:
- [ ] 使用 semver 规范版本号
- [ ] 配置 appId(反向域名格式)
- [ ] 准备各平台图标(256x256+)
- [ ] 设置合理的 files 配置
- [ ] 测试所有平台安装包
- [ ] 配置日志记录
- [ ] 处理未捕获异常

✅ 推荐:
- [ ] 启用 ASAR 打包
- [ ] 代码签名(生产环境)
- [ ] 自动更新机制
- [ ] CI/CD 自动化构建
- [ ] 构建产物校验(SHA256)
- [ ] 详细的构建日志

✅ 避免:
- [ ] 硬编码路径
- [ ] 打包不必要的文件
- [ ] 忽略代码签名
- [ ] 手动发布 Release

10.2 安全最佳实践

{
  "build": {
    "asar": true,
    "afterSign": "scripts/notarize.js",

    "win": {
      "sign": {
        "tool": "azure-signing-tool",
        "timestampServerUrl": "http://timestamp.digicert.com"
      }
    },

    "mac": {
      "hardenedRuntime": true,
      "gatekeeperAssess": true
    }
  }
}

10.3 性能优化

{
  "build": {
    "compression": "maximum",
    "electronDist": "node_modules/electron/dist",
    "electronVersion": "^28.0.0",

    "directories": {
      "output": "release/${version}",
      "buildResources": "build"
    },

    "files": [
      "out/**/*",
      "!out/**/*.map"
    ],

    "nsis": {
      "compression": "lzma"
    }
  }
}

10.4 产物命名规范

{
  "win": {
    "artifactName": "${productName}-${version}-${arch}-setup.${ext}"
  },

  "mac": {
    "artifactName": "${productName}-${version}-${arch}.${ext}"
  },

  "linux": {
    "artifactName": "${productName}-${version}-${arch}.${ext}"
  }
}

10.5 版本发布流程

┌─────────────────────────────────────────────────────────────┐
│                    版本发布流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 开发完成                                                │
│     ├── 代码审查                                             │
│     ├── 测试验证                                             │
│     └── 更新 CHANGELOG.md                                   │
│                                                             │
│  2. 版本升级                                                │
│     ├── 更新版本号                                           │
│     ├── 创建 Git tag                                        │
│     └── 推送到远程仓库                                       │
│                                                             │
│  3. CI/CD 构建                                              │
│     ├── GitHub Actions 自动构建                              │
│     ├── 多平台打包                                           │
│     └── 上传构建产物                                         │
│                                                             │
│  4. 发布 Release                                            │
│     ├── 创建 GitHub Release                                  │
│     ├── 上传安装包                                           │
│     ├── 添加发布说明                                          │
│     └── 自动更新推送                                         │
│                                                             │
│  5. 通知用户                                                │
│     ├── 更新博客                                             │
│     └── 社区通知                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

📚 总结

核心知识点回顾

模块核心技能
配置详解files/extraResources/publish 配置
多平台打包Windows/macOS/Linux 各平台配置
安装包定制NSIS/DMG 自定义脚本
代码签名Windows/macOS/Linux 签名方案
自动更新electron-updater 完整实现
CI/CDGitHub Actions/GitLab/Jenkins
排错指南常见错误与解决方案

延伸学习方向

  1. electron-forge – 官方推荐的替代打包工具
  2. appimage-builder – 高级 AppImage 定制
  3. Codesign 自动化 – 完整的签名流水线
  4. 差量更新 – 使用 electron-builder 的差量更新功能
  5. 多架构支持 – arm64/mips 等其他架构

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容