electron系列5:深入理解Electron打包

核心:让应用从代码变成可交付的安装包,覆盖Windows、macOS、Linux三大平台

一、为什么打包如此重要?

很多Electron开发者认为打包就是简单的"npm run build",但实际上:

javascript 复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    打包常见问题集锦                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ❌ 开发环境运行正常,打包后白屏                                 │
│  ❌ Windows打包成功,macOS上路径报错                             │
│  ❌ 安装包200MB,用户抱怨太大                                   │
│  ❌ 没有代码签名,被杀毒软件报毒                                │
│  ❌ 更新功能不工作,用户用着旧版本                              │
│  ❌ CI/CD构建失败,每次手动打包                                 │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

来跟着系统性地解决这些问题。

二、打包工具对比

2.1 主流工具对比表

维度 electron-builder electron-forge @electron/packager
易用性 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
配置灵活度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
Windows支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
macOS支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
Linux支持 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
自动更新 ✅ 内置 ✅ 内置 ❌ 需自行实现
代码签名 ✅ 完善 ✅ 支持 ⚠️ 基础
应用商店发布 ✅ 支持 ✅ 支持
社区活跃度 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐
学习曲线 中等

2.2 选型建议

本系列选择:electron-builder(我们通用的vue3插件,react插件里面用的就是这个)

  • 功能最全面

  • 自动更新方案成熟

  • CI/CD集成友好

三、electron-builder 完整配置

3.1 基础配置

electron-builder.json

javascript 复制代码
{
  "appId": "com.yourcompany.yourapp",
  "productName": "MyElectronApp",
  "copyright": "Copyright © 2024 YourCompany",
  "directories": {
    "output": "release",
    "buildResources": "build"
  },
  "files": [
    "packages/main/dist/**/*",
    "packages/preload/dist/**/*",
    "packages/renderer/dist/**/*",
    "node_modules/**/*"
  ],
  "extraResources": [
    {
      "from": "resources/",
      "to": "resources/",
      "filter": ["**/*"]
    }
  ],
  "asar": true,
  "asarUnpack": [
    "**/node_modules/sharp/**/*",
    "**/node_modules/ffmpeg-static/**/*"
  ],
  "compression": "maximum"
}

3.2 Windows 配置

javascript 复制代码
{
  "win": {
    "target": [
      {
        "target": "nsis",
        "arch": ["x64", "ia32"]
      },
      {
        "target": "portable",
        "arch": ["x64"]
      }
    ],
    "icon": "build/icon.ico",
    "publisherName": "YourCompany",
    "certificateFile": "cert.pfx",
    "certificatePassword": "",
    "verifyUpdateCodeSignature": true,
    "signAndEditExecutable": true,
    "signDlls": true
  },
  "nsis": {
    "oneClick": false,
    "perMachine": true,
    "allowToChangeInstallationDirectory": true,
    "createDesktopShortcut": true,
    "createStartMenuShortcut": true,
    "shortcutName": "MyElectronApp",
    "installerIcon": "build/icon.ico",
    "uninstallerIcon": "build/icon.ico",
    "license": "LICENSE.txt",
    "language": "2052",
    "installerHeader": "build/installerHeader.bmp",
    "installerSidebar": "build/installerSidebar.bmp",
    "uninstallerDisplayName": "MyElectronApp",
    "include": "build/installer.nsh"
  },
  "portable": {
    "requestExecutionLevel": "user",
    "unpackDirName": "MyElectronApp"
  }
}

3.3 macOS 配置

javascript 复制代码
{
  "mac": {
    "target": [
      {
        "target": "dmg",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "zip",
        "arch": ["x64", "arm64"]
      }
    ],
    "icon": "build/icon.icns",
    "category": "public.app-category.productivity",
    "hardenedRuntime": true,
    "gatekeeperAssess": true,
    "entitlements": "build/entitlements.mac.plist",
    "entitlementsInherit": "build/entitlements.mac.plist",
    "darkModeSupport": true,
    "minimumSystemVersion": "10.15"
  },
  "dmg": {
    "title": "MyElectronApp ${version}",
    "icon": "build/icon.icns",
    "iconSize": 100,
    "background": "build/dmg-background.png",
    "window": {
      "width": 540,
      "height": 380
    },
    "contents": [
      {
        "x": 130,
        "y": 150,
        "type": "file",
        "path": "/Applications"
      },
      {
        "x": 410,
        "y": 150,
        "type": "file",
        "path": "/Volumes/MyElectronApp/MyElectronApp.app"
      }
    ]
  },
  "mas": {
    "entitlements": "build/entitlements.mas.plist",
    "entitlementsInherit": "build/entitlements.mas.plist",
    "hardenedRuntime": false,
    "type": "distribution"
  }
}

build/entitlements.mac.plist

XML 复制代码
<?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.device.audio-input</key>
    <true/>
    <key>com.apple.security.device.camera</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-only</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.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <false/>
</dict>
</plist>

3.4 Linux 配置

javascript 复制代码
{
  "linux": {
    "target": [
      {
        "target": "AppImage",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "deb",
        "arch": ["x64", "arm64"]
      },
      {
        "target": "rpm",
        "arch": ["x64"]
      },
      {
        "target": "snap",
        "arch": ["x64"]
      }
    ],
    "icon": "build/icon.png",
    "category": "Utility",
    "maintainer": "YourCompany <support@example.com>",
    "description": "MyElectronApp - A powerful desktop application",
    "desktop": {
      "Name": "MyElectronApp",
      "Comment": "A powerful desktop application",
      "Categories": ["Utility"]
    }
  },
  "AppImage": {
    "systemIntegration": "doNotAsk",
    "synopsis": "MyElectronApp",
    "description": "A powerful desktop application",
    "license": "MIT"
  },
  "deb": {
    "priority": "optional",
    "depends": ["gconf2", "gconf-service", "libnotify4"]
  },
  "snap": {
    "confinement": "strict",
    "grade": "stable",
    "plugs": ["network", "network-bind", "home", "removable-media"]
  }
}

四、代码签名完整流程

4.1 Windows代码签名

步骤1:申请证书

从CA机构购买代码签名证书

推荐:DigiCert、Sectigo、GlobalSign

价格:普通证书 200-300/年,EV证书 400-600/年

步骤2:安装证书

导入证书到Windows

certlm.msc

导入到"个人" -> "证书"

导出为PFX格式

包含私钥和证书链

步骤3:配置签名

javascript 复制代码
// electron-builder.json
{
  "win": {
    "signingHashAlgorithms": ["sha256"],
    "signDlls": true,
    "certificateFile": "./certs/my-cert.pfx",
    "certificatePassword": "${CERT_PASSWORD}",
    "rfc3161TimeStampServer": "http://timestamp.digicert.com",
    "timeStampServer": "http://timestamp.digicert.com"
  }
}

步骤4:CI/CD环境变量

GitHub Secrets

CERT_PASSWORD=your_password

CERT_BASE64=base64_encoded_cert

4.2 macOS代码签名

步骤1:准备证书

**# 需要三种证书

1. Developer ID Application (用于分发)

2. Developer ID Installer (用于安装包)

3. Mac App Distribution (用于Mac App Store)**

# 查看证书
security find-identity -v -p basic

步骤2:配置签名

javascript 复制代码
// electron-builder.json
{
  "mac": {
    "identity": "Developer ID Application: YourCompany (TEAMID)",
    "notarize": {
      "teamId": "TEAMID",
      "appleId": "apple@example.com",
      "appleIdPassword": "@keychain:AC_PASSWORD"
    }
  }
}

步骤3:公证配置

javascript 复制代码
# 使用环境变量
export APPLE_ID="apple@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export APPLE_TEAM_ID="TEAMID"

# 或使用keychain
xcrun notarytool store-credentials "AC_PASSWORD" \
  --apple-id "apple@example.com" \
  --team-id "TEAMID" \
  --password "xxxx-xxxx-xxxx-xxxx"

4.3 验证签名

javascript 复制代码
# Windows验证
signtool verify /pa /v MyApp.exe

# macOS验证
codesign -dv --verbose=4 MyApp.app
spctl -a -v MyApp.app

# 验证公证
xcrun stapler validate MyApp.app

五、自动更新方案

5.1 更新流程图

5.2 完整更新实现

packages/main/src/updater/index.ts

javascript 复制代码
import { autoUpdater } from 'electron-updater'
import { BrowserWindow, dialog } from 'electron'
import log from 'electron-log'

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

// 更新状态
let updateCheckInterval: NodeJS.Timeout | null = null
let downloadProgress = 0

export function setupAutoUpdater(mainWindow: BrowserWindow) {
  // 开发环境跳过更新检查
  if (process.env.NODE_ENV === 'development') {
    log.info('Skip auto updater in development')
    return
  }
  
  // 配置更新源
  autoUpdater.setFeedURL({
    provider: 'github',
    owner: 'your-username',
    repo: 'your-repo',
    private: false
  })
  
  // 检查更新
  autoUpdater.checkForUpdates()
  
  // 设置定时检查(每4小时)
  updateCheckInterval = setInterval(() => {
    autoUpdater.checkForUpdates()
  }, 4 * 60 * 60 * 1000)
  
  // ============== 事件监听 ==============
  
  // 发现新版本
  autoUpdater.on('checking-for-update', () => {
    log.info('Checking for update...')
    mainWindow.webContents.send('update:checking')
  })
  
  // 没有新版本
  autoUpdater.on('update-not-available', (info) => {
    log.info('Update not available', info)
    mainWindow.webContents.send('update:not-available')
  })
  
  // 发现新版本
  autoUpdater.on('update-available', (info) => {
    log.info('Update available', info)
    
    // 显示更新对话框
    const response = dialog.showMessageBoxSync(mainWindow, {
      type: 'info',
      title: '发现新版本',
      message: `发现新版本 ${info.version},是否立即更新?`,
      detail: `当前版本: ${autoUpdater.currentVersion}\n最新版本: ${info.version}`,
      buttons: ['立即更新', '稍后提醒'],
      defaultId: 0,
      cancelId: 1
    })
    
    if (response === 0) {
      // 开始下载
      autoUpdater.downloadUpdate()
      mainWindow.webContents.send('update:downloading', { progress: 0 })
    }
  })
  
  // 下载进度
  autoUpdater.on('download-progress', (progress) => {
    downloadProgress = progress.percent
    log.info(`Download progress: ${progress.percent}%`)
    
    mainWindow.webContents.send('update:progress', {
      percent: progress.percent,
      bytesPerSecond: progress.bytesPerSecond,
      total: progress.total,
      transferred: progress.transferred
    })
  })
  
  // 下载完成
  autoUpdater.on('update-downloaded', (info) => {
    log.info('Update downloaded', info)
    
    mainWindow.webContents.send('update:downloaded')
    
    // 询问用户是否立即重启
    const response = dialog.showMessageBoxSync(mainWindow, {
      type: 'info',
      title: '更新就绪',
      message: '更新已下载完成,是否立即重启应用?',
      buttons: ['立即重启', '稍后重启'],
      defaultId: 0
    })
    
    if (response === 0) {
      // 退出并安装更新
      setImmediate(() => {
        autoUpdater.quitAndInstall()
      })
    }
  })
  
  // 更新错误
  autoUpdater.on('error', (err) => {
    log.error('Update error', err)
    
    dialog.showErrorBox('更新失败', `更新过程中发生错误:${err.message}`)
    mainWindow.webContents.send('update:error', err.message)
  })
}

// 手动检查更新
export function checkForUpdates() {
  autoUpdater.checkForUpdates()
}

// 开始下载更新
export function downloadUpdate() {
  autoUpdater.downloadUpdate()
}

// 退出并安装
export function quitAndInstall() {
  autoUpdater.quitAndInstall()
}

// 获取下载进度
export function getDownloadProgress() {
  return downloadProgress
}

// 清理定时器
export function cleanupUpdater() {
  if (updateCheckInterval) {
    clearInterval(updateCheckInterval)
    updateCheckInterval = null
  }
}

5.3 更新UI组件

packages/renderer/src/components/UpdateNotification.vue

javascript 复制代码
<template>
  <Transition name="slide">
    <div v-if="visible" class="update-notification" :class="status">
      <div class="notification-icon">
        <span v-if="status === 'checking'">🔍</span>
        <span v-else-if="status === 'available'">📦</span>
        <span v-else-if="status === 'downloading'">⬇️</span>
        <span v-else-if="status === 'downloaded'">✅</span>
        <span v-else-if="status === 'error'">❌</span>
      </div>
      
      <div class="notification-content">
        <div class="notification-title">{{ title }}</div>
        <div class="notification-message">{{ message }}</div>
        
        <div v-if="status === 'downloading'" class="progress-bar">
          <div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
          <span class="progress-text">{{ progressPercent }}%</span>
        </div>
      </div>
      
      <div class="notification-actions">
        <button v-if="status === 'available'" @click="downloadNow" class="btn-primary">
          立即更新
        </button>
        <button v-if="status === 'downloaded'" @click="restartNow" class="btn-primary">
          立即重启
        </button>
        <button v-if="status !== 'downloading'" @click="dismiss" class="btn-secondary">
          稍后
        </button>
      </div>
    </div>
  </Transition>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

type UpdateStatus = 'checking' | 'available' | 'downloading' | 'downloaded' | 'error' | 'hidden'

const visible = ref(false)
const status = ref<UpdateStatus>('hidden')
const title = ref('')
const message = ref('')
const progressPercent = ref(0)
const versionInfo = ref<any>(null)

let timeoutId: NodeJS.Timeout | null = null

const showNotification = (newStatus: UpdateStatus, customTitle?: string, customMessage?: string) => {
  status.value = newStatus
  visible.value = true
  
  switch (newStatus) {
    case 'checking':
      title.value = customTitle || '检查更新'
      message.value = customMessage || '正在检查新版本...'
      break
    case 'available':
      title.value = customTitle || '发现新版本'
      message.value = customMessage || `发现新版本 ${versionInfo.value?.version},是否立即更新?`
      break
    case 'downloading':
      title.value = customTitle || '正在下载'
      message.value = customMessage || '正在下载更新包...'
      break
    case 'downloaded':
      title.value = customTitle || '更新就绪'
      message.value = customMessage || '更新已下载完成,重启后生效'
      break
    case 'error':
      title.value = customTitle || '更新失败'
      message.value = customMessage || '更新过程中发生错误'
      break
  }
  
  // 5秒后自动隐藏(除了下载中)
  if (newStatus !== 'downloading') {
    if (timeoutId) clearTimeout(timeoutId)
    timeoutId = setTimeout(() => {
      if (status.value !== 'downloading') {
        visible.value = false
      }
    }, 5000)
  }
}

const downloadNow = () => {
  window.electronAPI?.downloadUpdate()
  showNotification('downloading')
}

const restartNow = () => {
  window.electronAPI?.quitAndInstall()
}

const dismiss = () => {
  visible.value = false
}

// 监听更新事件
onMounted(() => {
  window.electronAPI?.onUpdateStatus?.((event: any, data: any) => {
    switch (data.type) {
      case 'checking':
        showNotification('checking')
        break
      case 'not-available':
        showNotification('hidden')
        break
      case 'available':
        versionInfo.value = data.info
        showNotification('available')
        break
      case 'progress':
        progressPercent.value = data.progress.percent
        showNotification('downloading')
        break
      case 'downloaded':
        showNotification('downloaded')
        break
      case 'error':
        showNotification('error', '更新失败', data.error)
        break
    }
  })
})

onUnmounted(() => {
  if (timeoutId) clearTimeout(timeoutId)
})
</script>

<style scoped>
.update-notification {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 360px;
  background: var(--bg-primary);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  display: flex;
  padding: 16px;
  gap: 12px;
  z-index: 10000;
  border-left: 4px solid;
}

.update-notification.checking { border-left-color: #ffa500; }
.update-notification.available { border-left-color: #2196f3; }
.update-notification.downloading { border-left-color: #4caf50; }
.update-notification.downloaded { border-left-color: #4caf50; }
.update-notification.error { border-left-color: #f44336; }

.notification-icon {
  font-size: 24px;
  width: 40px;
  text-align: center;
}

.notification-content {
  flex: 1;
}

.notification-title {
  font-weight: 600;
  margin-bottom: 4px;
}

.notification-message {
  font-size: 13px;
  color: var(--text-secondary);
  margin-bottom: 8px;
}

.progress-bar {
  height: 6px;
  background: var(--border-color);
  border-radius: 3px;
  overflow: hidden;
  position: relative;
  margin-top: 8px;
}

.progress-fill {
  height: 100%;
  background: #4caf50;
  border-radius: 3px;
  transition: width 0.3s;
}

.progress-text {
  position: absolute;
  right: 0;
  top: -18px;
  font-size: 11px;
  color: var(--text-secondary);
}

.notification-actions {
  display: flex;
  gap: 8px;
  align-items: flex-start;
}

.btn-primary {
  padding: 4px 12px;
  background: #2196f3;
  border: none;
  border-radius: 4px;
  color: white;
  cursor: pointer;
  font-size: 12px;
}

.btn-secondary {
  padding: 4px 12px;
  background: transparent;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.slide-enter-active,
.slide-leave-active {
  transition: all 0.3s ease;
}

.slide-enter-from,
.slide-leave-to {
  transform: translateX(100%);
  opacity: 0;
}
</style>

六、CI/CD 集成

6.1 GitHub Actions 完整配置

.github/workflows/build.yml

javascript 复制代码
name: Build and Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        include:
          - os: windows-latest
            platform: win
          - os: macos-latest
            platform: mac
          - os: ubuntu-latest
            platform: linux

    runs-on: ${{ matrix.os }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

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

      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8

      - name: Install dependencies
        run: pnpm install

      - name: Build all packages
        run: pnpm run build:all

      # Windows 签名
      - name: Import Windows certificate
        if: matrix.os == 'windows-latest'
        run: |
          $pfxPath = Join-Path $env:temp "cert.pfx"
          [System.IO.File]::WriteAllBytes($pfxPath, [System.Convert]::FromBase64String($env:WINDOWS_CERT_BASE64))
          Import-PfxCertificate -FilePath $pfxPath -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:CERT_PASSWORD -Force -AsPlainText)
        env:
          WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }}
          CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}

      # macOS 签名
      - name: Import macOS certificate
        if: matrix.os == 'macos-latest'
        run: |
          echo $MACOS_CERT_BASE64 | base64 --decode > certificate.p12
          security create-keychain -p ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} build.keychain
          security default-keychain -s build.keychain
          security unlock-keychain -p ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} build.keychain
          security import certificate.p12 -k build.keychain -P ${{ secrets.MACOS_CERT_PASSWORD }} -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple: -s -k ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} build.keychain
        env:
          MACOS_CERT_BASE64: ${{ secrets.MACOS_CERT_BASE64 }}
          MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
          MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}

      # 打包
      - name: Build Electron app
        run: |
          pnpm run build:electron
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          CERT_PASSWORD: ${{ secrets.CERT_PASSWORD }}
          NODE_ENV: production

      # 上传 artifacts
      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.platform }}-artifacts
          path: |
            release/**/*.exe
            release/**/*.dmg
            release/**/*.zip
            release/**/*.AppImage
            release/**/*.deb
            release/**/latest.yml
            release/**/latest-mac.yml
            release/**/latest-linux.yml

  release:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/')

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

      - name: Create Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            artifacts/**/*.exe
            artifacts/**/*.dmg
            artifacts/**/*.zip
            artifacts/**/*.AppImage
            artifacts/**/*.deb
            artifacts/**/latest.yml
            artifacts/**/latest-mac.yml
            artifacts/**/latest-linux.yml
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

6.2 版本管理策略

scripts/version.js

javascript 复制代码
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

// 获取版本号
const getVersion = () => {
  const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
  return packageJson.version
}

// 更新版本号
const updateVersion = (type) => {
  const commands = {
    patch: 'npm version patch --no-git-tag-version',
    minor: 'npm version minor --no-git-tag-version',
    major: 'npm version major --no-git-tag-version'
  }
  
  execSync(commands[type], { stdio: 'inherit' })
  
  const newVersion = getVersion()
  console.log(`Version updated to ${newVersion}`)
  
  // 更新所有子包
  const packages = ['main', 'preload', 'renderer']
  packages.forEach(pkg => {
    const pkgPath = path.join('packages', pkg, 'package.json')
    const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
    pkgJson.version = newVersion
    fs.writeFileSync(pkgPath, JSON.stringify(pkgJson, null, 2))
  })
  
  return newVersion
}

// 创建Git标签
const createTag = (version) => {
  execSync(`git add .`, { stdio: 'inherit' })
  execSync(`git commit -m "chore: release v${version}"`, { stdio: 'inherit' })
  execSync(`git tag -a v${version} -m "Release v${version}"`, { stdio: 'inherit' })
  execSync(`git push && git push --tags`, { stdio: 'inherit' })
}

// 主函数
const main = () => {
  const type = process.argv[2]
  
  if (!['patch', 'minor', 'major'].includes(type)) {
    console.error('Usage: node scripts/version.js [patch|minor|major]')
    process.exit(1)
  }
  
  const newVersion = updateVersion(type)
  createTag(newVersion)
}

main()

七、体积优化技巧

7.1 优化前后对比

复制代码
优化项:                                                         
 • node_modules 去重: -25MB                                     
 • 图片压缩: -15MB                                               
 • 代码分割: -10MB                                               
 • asar 打包: -8MB                                              
 • 移除 source map: -12MB    

7.2 优化配置

javascript 复制代码
// electron-builder.json - 体积优化配置
{
  "compression": "maximum",
  "asar": true,
  "asarUnpack": [],
  "files": [
    "packages/**/dist/**/*",
    "!**/*.map",
    "!**/test/**",
    "!**/tests/**",
    "!**/__tests__/**",
    "!**/*.d.ts",
    "!**/*.log",
    "!**/docs/**",
    "!**/examples/**"
  ],
  "extraMetadata": {
    "main": "packages/main/dist/index.js"
  }
}
javascript 复制代码
// vite.config.js - 构建优化
export default {
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true,
        pure_funcs: ['console.log']
      }
    },
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia'],
          'ui': ['element-plus']
        }
      }
    }
  }
}

八、常见问题排查

问题1:打包后白屏

javascript 复制代码
// 原因:路径问题
// 错误写法
win.loadFile('./index.html')

// 正确写法
import path from 'path'
win.loadFile(path.join(__dirname, '../renderer/dist/index.html'))

// 生产环境检查
const isDev = !app.isPackaged
const indexPath = isDev
  ? 'http://localhost:5173'
  : path.join(__dirname, '../renderer/dist/index.html')

问题2:node_modules找不到

javascript 复制代码
// 确保 asarUnpack 包含需要原生模块的包
{
  "asarUnpack": [
    "**/node_modules/sharp/**/*",
    "**/node_modules/ffi-napi/**/*",
    "**/node_modules/ref-napi/**/*"
  ]
}

问题3:代码签名失败

javascript 复制代码
# Windows: 检查证书
certlm.msc
# 确认证书有效期和私钥

# macOS: 检查钥匙串
security find-identity -v
# 确认证书已安装

# 验证签名
codesign -dv --verbose=4 YourApp.app
相关推荐
患得患失9492 小时前
【前端WebSocket】心跳功能,心跳重置策略、双向确认(Ping-Pong) 以及 指数退避算法(Exponential Backoff)
前端·websocket·算法
英俊潇洒美少年2 小时前
React 实现 AI 流式打字机对话:SSE 分包粘包处理 + 并发优化
前端·javascript·react.js
chQHk57BN2 小时前
前端测试入门:Jest、Cypress等测试框架使用教程
前端
遇见你...2 小时前
前端技术知识点
前端
AC赳赳老秦2 小时前
OpenClaw image-processing技能实操:批量抠图、图片尺寸调整,适配办公需求
开发语言·前端·人工智能·python·深度学习·机器学习·openclaw
叫我一声阿雷吧2 小时前
JS 入门通关手册(44):宏任务 / 微任务 / Event Loop(前端最难核心,面试必考
javascript·宏任务·event loop· 前端面试· 微任务· 事件循环·js单线程
We་ct2 小时前
LeetCode 172. 阶乘后的零:从暴力到最优,拆解解题核心
开发语言·前端·javascript·算法·leetcode·typescript
军军君012 小时前
数字孪生监控大屏实战模板:可视化数字统计展示
前端·javascript·vue.js·typescript·echarts·数字孪生·前端大屏
此刻觐神3 小时前
IMX6ULL开发板学习-03(Linux文件相关命令)
前端·chrome