Skip to content

第7章:应用打包与分发

学习使用 electron-builder 进行应用打包、代码签名和自动更新,实现专业的应用分发

7.1 打包工具选择

Electron 应用有多种打包工具可选,每种都有其特点和适用场景。

主要打包工具对比

工具特点适用场景
electron-builder功能全面,配置灵活,支持自动更新推荐用于生产环境
electron-packager简单轻量,配置简单适合简单应用或学习
electron-forge官方推荐,集成度高适合新项目

本教程主要使用 electron-builder,因为它功能最全面且在生产环境中使用最广泛。

安装 electron-builder

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

# 或者使用 yarn
yarn add electron-builder --dev

7.2 基础打包配置

package.json 配置

json
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "description": "我的 Electron 应用",
  "main": "main.js",
  "homepage": "https://myapp.com",
  "author": {
    "name": "你的名字",
    "email": "your.email@example.com",
    "url": "https://yourwebsite.com"
  },
  "scripts": {
    "start": "electron .",
    "dev": "electron . --enable-logging",
    "build": "electron-builder",
    "build:win": "electron-builder --win",
    "build:mac": "electron-builder --mac",
    "build:linux": "electron-builder --linux",
    "dist": "npm run build",
    "pack": "electron-builder --dir",
    "postinstall": "electron-builder install-app-deps"
  },
  "build": {
    "appId": "com.yourcompany.myapp",
    "productName": "My Electron App",
    "directories": {
      "output": "dist",
      "buildResources": "build"
    },
    "files": [
      "main.js",
      "preload.js",
      "renderer.js",
      "index.html",
      "package.json",
      "node_modules/**/*",
      "assets/**/*"
    ],
    "extraResources": [
      {
        "from": "resources/",
        "to": "resources/",
        "filter": ["**/*"]
      }
    ],
    "win": {
      "target": [
        {
          "target": "nsis",
          "arch": ["x64", "ia32"]
        },
        {
          "target": "portable",
          "arch": ["x64"]
        }
      ],
      "icon": "build/icon.ico",
      "requestedExecutionLevel": "asInvoker"
    },
    "mac": {
      "target": [
        {
          "target": "dmg",
          "arch": ["x64", "arm64"]
        },
        {
          "target": "zip",
          "arch": ["x64", "arm64"]
        }
      ],
      "icon": "build/icon.icns",
      "category": "public.app-category.productivity",
      "hardenedRuntime": true,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.plist"
    },
    "linux": {
      "target": [
        {
          "target": "AppImage",
          "arch": ["x64"]
        },
        {
          "target": "deb",
          "arch": ["x64"]
        },
        {
          "target": "rpm",
          "arch": ["x64"]
        }
      ],
      "icon": "build/icon.png",
      "category": "Office"
    },
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true,
      "shortcutName": "My Electron App"
    },
    "dmg": {
      "title": "My Electron App",
      "icon": "build/icon.icns",
      "background": "build/dmg-background.png",
      "contents": [
        {
          "x": 410,
          "y": 150,
          "type": "link",
          "path": "/Applications"
        },
        {
          "x": 130,
          "y": 150,
          "type": "file"
        }
      ]
    }
  },
  "devDependencies": {
    "electron": "^latest",
    "electron-builder": "^latest"
  }
}

目录结构

my-electron-app/
├── build/                    # 构建资源
│   ├── icon.ico             # Windows 图标
│   ├── icon.icns            # macOS 图标
│   ├── icon.png             # Linux 图标
│   ├── dmg-background.png   # macOS DMG 背景
│   └── entitlements.mac.plist # macOS 权限文件
├── resources/               # 额外资源文件
├── src/                     # 源代码
│   ├── main.js
│   ├── preload.js
│   ├── renderer.js
│   └── index.html
├── assets/                  # 应用资源
├── dist/                    # 构建输出目录
├── package.json
└── README.md

7.3 图标和资源准备

图标要求

不同平台对图标有不同的要求:

Windows (.ico)

  • 包含多种尺寸:16x16, 24x24, 32x32, 48x48, 64x64, 128x128, 256x256
  • 格式:ICO 文件

macOS (.icns)

  • 包含多种尺寸:16x16, 32x32, 64x64, 128x128, 256x256, 512x512, 1024x1024
  • 格式:ICNS 文件

Linux (.png)

  • 推荐尺寸:512x512 或 1024x1024
  • 格式:PNG 文件

图标生成脚本

javascript
// scripts/generate-icons.js
const sharp = require('sharp')
const fs = require('fs')
const path = require('path')

class IconGenerator {
  constructor(sourceImage) {
    this.sourceImage = sourceImage
    this.buildDir = path.join(__dirname, '..', 'build')
  }

  async generateAllIcons() {
    // 确保构建目录存在
    if (!fs.existsSync(this.buildDir)) {
      fs.mkdirSync(this.buildDir, { recursive: true })
    }

    await this.generateWindowsIcon()
    await this.generateLinuxIcon()
    console.log('图标生成完成!')
  }

  async generateWindowsIcon() {
    const sizes = [16, 24, 32, 48, 64, 128, 256]
    const iconPath = path.join(this.buildDir, 'icon.ico')
    
    // 生成不同尺寸的 PNG 文件
    const pngFiles = []
    for (const size of sizes) {
      const pngPath = path.join(this.buildDir, `icon-${size}.png`)
      await sharp(this.sourceImage)
        .resize(size, size)
        .png()
        .toFile(pngPath)
      pngFiles.push(pngPath)
    }

    // 使用 png2icons 或类似工具将 PNG 转换为 ICO
    console.log('Windows 图标文件已生成:', iconPath)
  }

  async generateLinuxIcon() {
    const iconPath = path.join(this.buildDir, 'icon.png')
    
    await sharp(this.sourceImage)
      .resize(512, 512)
      .png()
      .toFile(iconPath)
    
    console.log('Linux 图标文件已生成:', iconPath)
  }

  async generateMacIcon() {
    // macOS 图标生成需要使用 iconutil 工具
    // 这里提供基本的 PNG 生成,实际的 ICNS 转换需要在 macOS 上进行
    const sizes = [16, 32, 64, 128, 256, 512, 1024]
    const iconsetDir = path.join(this.buildDir, 'icon.iconset')
    
    if (!fs.existsSync(iconsetDir)) {
      fs.mkdirSync(iconsetDir)
    }

    for (const size of sizes) {
      const fileName = `icon_${size}x${size}.png`
      const filePath = path.join(iconsetDir, fileName)
      
      await sharp(this.sourceImage)
        .resize(size, size)
        .png()
        .toFile(filePath)
      
      // 生成 @2x 版本
      if (size <= 512) {
        const fileName2x = `icon_${size}x${size}@2x.png`
        const filePath2x = path.join(iconsetDir, fileName2x)
        
        await sharp(this.sourceImage)
          .resize(size * 2, size * 2)
          .png()
          .toFile(filePath2x)
      }
    }

    console.log('macOS 图标集已生成,请在 macOS 上运行以下命令生成 ICNS:')
    console.log(`iconutil -c icns ${iconsetDir}`)
  }
}

// 使用示例
if (require.main === module) {
  const sourceImage = process.argv[2] || 'source-icon.png'
  const generator = new IconGenerator(sourceImage)
  generator.generateAllIcons().catch(console.error)
}

module.exports = IconGenerator

7.4 高级打包配置

多环境配置

javascript
// electron-builder.config.js
const config = {
  appId: 'com.yourcompany.myapp',
  productName: 'My Electron App',
  
  directories: {
    output: 'dist',
    buildResources: 'build'
  },

  files: [
    'dist-electron/**/*',
    'dist/**/*',
    'node_modules/**/*',
    'package.json'
  ],

  extraMetadata: {
    main: 'dist-electron/main.js'
  },

  // 开发环境配置
  ...(process.env.NODE_ENV === 'development' && {
    compression: 'store', // 不压缩,加快构建速度
    removePackageScripts: false
  }),

  // 生产环境配置
  ...(process.env.NODE_ENV === 'production' && {
    compression: 'maximum', // 最大压缩
    removePackageScripts: true
  }),

  win: {
    target: [
      {
        target: 'nsis',
        arch: ['x64']
      }
    ],
    icon: 'build/icon.ico',
    publisherName: 'Your Company Name',
    verifyUpdateCodeSignature: false
  },

  nsis: {
    oneClick: false,
    allowToChangeInstallationDirectory: true,
    allowElevation: true,
    createDesktopShortcut: true,
    createStartMenuShortcut: true,
    shortcutName: 'My Electron App',
    include: 'build/installer.nsh', // 自定义安装脚本
    script: 'build/installer.nsh'
  },

  mac: {
    target: [
      {
        target: 'dmg',
        arch: ['x64', 'arm64']
      }
    ],
    icon: 'build/icon.icns',
    category: 'public.app-category.productivity',
    hardenedRuntime: true,
    gatekeeperAssess: false,
    entitlements: 'build/entitlements.mac.plist',
    entitlementsInherit: 'build/entitlements.mac.plist'
  },

  dmg: {
    title: '${productName}',
    icon: 'build/icon.icns',
    background: 'build/dmg-background.png',
    contents: [
      {
        x: 410,
        y: 150,
        type: 'link',
        path: '/Applications'
      },
      {
        x: 130,
        y: 150,
        type: 'file'
      }
    ],
    window: {
      width: 540,
      height: 380
    }
  },

  linux: {
    target: [
      {
        target: 'AppImage',
        arch: ['x64']
      },
      {
        target: 'deb',
        arch: ['x64']
      }
    ],
    icon: 'build/icon.png',
    category: 'Office',
    desktop: {
      Name: 'My Electron App',
      Comment: 'A great Electron application',
      Keywords: 'electron;app;productivity'
    }
  },

  // 发布配置
  publish: [
    {
      provider: 'github',
      owner: 'your-username',
      repo: 'your-repo'
    }
  ]
}

module.exports = config

自定义构建脚本

javascript
// scripts/build.js
const builder = require('electron-builder')
const path = require('path')

class BuildManager {
  constructor() {
    this.config = require('../electron-builder.config.js')
  }

  async buildForPlatform(platform, arch = 'x64') {
    const targets = new Map()
    
    switch (platform) {
      case 'win':
        targets.set(builder.Platform.WINDOWS, new Map([
          [builder.Arch.x64, ['nsis', 'portable']]
        ]))
        break
      case 'mac':
        targets.set(builder.Platform.MAC, new Map([
          [builder.Arch.x64, ['dmg']],
          [builder.Arch.arm64, ['dmg']]
        ]))
        break
      case 'linux':
        targets.set(builder.Platform.LINUX, new Map([
          [builder.Arch.x64, ['AppImage', 'deb']]
        ]))
        break
      default:
        throw new Error(`Unsupported platform: ${platform}`)
    }

    try {
      console.log(`开始构建 ${platform} 平台...`)
      
      const result = await builder.build({
        targets,
        config: this.config,
        publish: 'never' // 不自动发布
      })

      console.log(`${platform} 平台构建完成:`, result)
      return result
    } catch (error) {
      console.error(`${platform} 平台构建失败:`, error)
      throw error
    }
  }

  async buildAll() {
    const platforms = ['win', 'mac', 'linux']
    const results = {}

    for (const platform of platforms) {
      try {
        results[platform] = await this.buildForPlatform(platform)
      } catch (error) {
        results[platform] = { error: error.message }
      }
    }

    return results
  }

  async buildCurrent() {
    const platform = process.platform
    const platformMap = {
      'win32': 'win',
      'darwin': 'mac',
      'linux': 'linux'
    }

    const targetPlatform = platformMap[platform]
    if (!targetPlatform) {
      throw new Error(`Unsupported platform: ${platform}`)
    }

    return await this.buildForPlatform(targetPlatform)
  }
}

// 命令行使用
if (require.main === module) {
  const buildManager = new BuildManager()
  const args = process.argv.slice(2)
  
  if (args.length === 0) {
    buildManager.buildCurrent().catch(console.error)
  } else {
    const platform = args[0]
    buildManager.buildForPlatform(platform).catch(console.error)
  }
}

module.exports = BuildManager

7.5 代码签名

代码签名是确保应用安全性和用户信任的重要步骤。

Windows 代码签名

javascript
// Windows 代码签名配置
const winConfig = {
  win: {
    certificateFile: 'path/to/certificate.p12',
    certificatePassword: process.env.CSC_KEY_PASSWORD,
    // 或者使用证书存储
    certificateSubjectName: 'Your Company Name',
    certificateSha1: 'certificate-thumbprint',
    
    // 时间戳服务器
    timeStampServer: 'http://timestamp.digicert.com',
    
    // 签名算法
    signingHashAlgorithms: ['sha256'],
    
    // 额外的签名参数
    signDlls: true
  }
}

macOS 代码签名

javascript
// macOS 代码签名配置
const macConfig = {
  mac: {
    // 开发者 ID
    identity: 'Developer ID Application: Your Name (TEAM_ID)',
    
    // 权限文件
    entitlements: 'build/entitlements.mac.plist',
    entitlementsInherit: 'build/entitlements.mac.plist',
    
    // 强化运行时
    hardenedRuntime: true,
    
    // 公证
    notarize: {
      teamId: 'YOUR_TEAM_ID'
    }
  }
}

权限文件示例

xml
<!-- build/entitlements.mac.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.allow-dyld-environment-variables</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/>
</dict>
</plist>

## 7.6 自动更新

自动更新是现代桌面应用的重要功能,让用户能够及时获得最新版本。

### 自动更新配置

```javascript
// 主进程中的自动更新管理
const { autoUpdater } = require('electron-updater')
const { app, dialog, BrowserWindow } = require('electron')

class AutoUpdateManager {
  constructor() {
    this.setupAutoUpdater()
    this.setupIPC()
  }

  setupAutoUpdater() {
    // 配置更新服务器
    autoUpdater.setFeedURL({
      provider: 'github',
      owner: 'your-username',
      repo: 'your-repo',
      private: false
    })

    // 或者使用自定义服务器
    // autoUpdater.setFeedURL({
    //   provider: 'generic',
    //   url: 'https://your-update-server.com/updates'
    // })

    // 设置更新检查间隔(毫秒)
    autoUpdater.autoDownload = false // 不自动下载
    autoUpdater.autoInstallOnAppQuit = true

    this.setupUpdateEvents()
  }

  setupUpdateEvents() {
    // 检查更新时
    autoUpdater.on('checking-for-update', () => {
      console.log('正在检查更新...')
      this.sendToRenderer('update-checking')
    })

    // 发现可用更新
    autoUpdater.on('update-available', (info) => {
      console.log('发现可用更新:', info)
      this.sendToRenderer('update-available', info)
      this.showUpdateDialog(info)
    })

    // 没有可用更新
    autoUpdater.on('update-not-available', (info) => {
      console.log('当前已是最新版本')
      this.sendToRenderer('update-not-available', info)
    })

    // 下载进度
    autoUpdater.on('download-progress', (progressObj) => {
      console.log(`下载进度: ${progressObj.percent}%`)
      this.sendToRenderer('update-download-progress', progressObj)
    })

    // 下载完成
    autoUpdater.on('update-downloaded', (info) => {
      console.log('更新下载完成')
      this.sendToRenderer('update-downloaded', info)
      this.showInstallDialog()
    })

    // 更新错误
    autoUpdater.on('error', (error) => {
      console.error('更新错误:', error)
      this.sendToRenderer('update-error', error.message)
    })
  }

  setupIPC() {
    const { ipcMain } = require('electron')

    ipcMain.handle('check-for-updates', () => {
      return this.checkForUpdates()
    })

    ipcMain.handle('download-update', () => {
      return this.downloadUpdate()
    })

    ipcMain.handle('install-update', () => {
      return this.installUpdate()
    })

    ipcMain.handle('get-app-version', () => {
      return app.getVersion()
    })
  }

  async checkForUpdates() {
    try {
      const result = await autoUpdater.checkForUpdates()
      return { success: true, result }
    } catch (error) {
      return { success: false, error: error.message }
    }
  }

  async downloadUpdate() {
    try {
      await autoUpdater.downloadUpdate()
      return { success: true }
    } catch (error) {
      return { success: false, error: error.message }
    }
  }

  installUpdate() {
    autoUpdater.quitAndInstall()
  }

  sendToRenderer(channel, data) {
    const mainWindow = BrowserWindow.getAllWindows()[0]
    if (mainWindow) {
      mainWindow.webContents.send(channel, data)
    }
  }

  async showUpdateDialog(updateInfo) {
    const result = await dialog.showMessageBox({
      type: 'info',
      title: '发现新版本',
      message: `发现新版本 ${updateInfo.version}`,
      detail: updateInfo.releaseNotes || '点击下载按钮开始下载更新',
      buttons: ['下载更新', '稍后提醒', '跳过此版本'],
      defaultId: 0,
      cancelId: 1
    })

    switch (result.response) {
      case 0: // 下载更新
        this.downloadUpdate()
        break
      case 1: // 稍后提醒
        // 设置稍后提醒
        setTimeout(() => {
          this.checkForUpdates()
        }, 60 * 60 * 1000) // 1小时后再次检查
        break
      case 2: // 跳过此版本
        // 记录跳过的版本
        this.skipVersion(updateInfo.version)
        break
    }
  }

  async showInstallDialog() {
    const result = await dialog.showMessageBox({
      type: 'info',
      title: '更新已下载',
      message: '更新已下载完成',
      detail: '点击"立即安装"重启应用并安装更新,或选择"稍后安装"在下次启动时安装',
      buttons: ['立即安装', '稍后安装'],
      defaultId: 0,
      cancelId: 1
    })

    if (result.response === 0) {
      this.installUpdate()
    }
  }

  skipVersion(version) {
    const Store = require('electron-store')
    const store = new Store()
    store.set('skippedVersion', version)
  }

  isVersionSkipped(version) {
    const Store = require('electron-store')
    const store = new Store()
    return store.get('skippedVersion') === version
  }
}

// 使用自动更新管理器
const autoUpdateManager = new AutoUpdateManager()

// 应用准备就绪后检查更新
app.whenReady().then(() => {
  // 延迟检查更新,避免影响应用启动
  setTimeout(() => {
    autoUpdateManager.checkForUpdates()
  }, 5000)
})

渲染进程中的更新 UI

javascript
// renderer/update-manager.js
class UpdateUI {
  constructor() {
    this.setupUpdateListeners()
    this.createUpdateUI()
  }

  setupUpdateListeners() {
    // 监听更新事件
    window.electronAPI.onUpdateChecking(() => {
      this.showUpdateStatus('正在检查更新...')
    })

    window.electronAPI.onUpdateAvailable((info) => {
      this.showUpdateAvailable(info)
    })

    window.electronAPI.onUpdateNotAvailable(() => {
      this.showUpdateStatus('当前已是最新版本')
    })

    window.electronAPI.onUpdateDownloadProgress((progress) => {
      this.updateDownloadProgress(progress)
    })

    window.electronAPI.onUpdateDownloaded(() => {
      this.showUpdateReady()
    })

    window.electronAPI.onUpdateError((error) => {
      this.showUpdateError(error)
    })
  }

  createUpdateUI() {
    const updateContainer = document.createElement('div')
    updateContainer.id = 'update-container'
    updateContainer.className = 'update-container hidden'

    updateContainer.innerHTML = `
      <div class="update-content">
        <div class="update-icon">🔄</div>
        <div class="update-title">应用更新</div>
        <div class="update-message" id="update-message"></div>
        <div class="update-progress" id="update-progress">
          <div class="progress-bar">
            <div class="progress-fill" id="progress-fill"></div>
          </div>
          <div class="progress-text" id="progress-text"></div>
        </div>
        <div class="update-actions" id="update-actions">
          <button id="check-update-btn" class="btn btn-primary">检查更新</button>
          <button id="download-update-btn" class="btn btn-success hidden">下载更新</button>
          <button id="install-update-btn" class="btn btn-warning hidden">安装更新</button>
          <button id="close-update-btn" class="btn btn-secondary">关闭</button>
        </div>
      </div>
    `

    document.body.appendChild(updateContainer)
    this.setupUpdateActions()
  }

  setupUpdateActions() {
    document.getElementById('check-update-btn').addEventListener('click', () => {
      window.electronAPI.checkForUpdates()
    })

    document.getElementById('download-update-btn').addEventListener('click', () => {
      window.electronAPI.downloadUpdate()
    })

    document.getElementById('install-update-btn').addEventListener('click', () => {
      window.electronAPI.installUpdate()
    })

    document.getElementById('close-update-btn').addEventListener('click', () => {
      this.hideUpdateUI()
    })
  }

  showUpdateUI() {
    document.getElementById('update-container').classList.remove('hidden')
  }

  hideUpdateUI() {
    document.getElementById('update-container').classList.add('hidden')
  }

  showUpdateStatus(message) {
    this.showUpdateUI()
    document.getElementById('update-message').textContent = message
    document.getElementById('update-progress').classList.add('hidden')
    document.getElementById('download-update-btn').classList.add('hidden')
    document.getElementById('install-update-btn').classList.add('hidden')
  }

  showUpdateAvailable(info) {
    this.showUpdateUI()
    document.getElementById('update-message').textContent =
      `发现新版本 ${info.version}`
    document.getElementById('download-update-btn').classList.remove('hidden')
  }

  updateDownloadProgress(progress) {
    document.getElementById('update-progress').classList.remove('hidden')
    document.getElementById('progress-fill').style.width = `${progress.percent}%`
    document.getElementById('progress-text').textContent =
      `${Math.round(progress.percent)}% (${this.formatBytes(progress.transferred)}/${this.formatBytes(progress.total)})`
  }

  showUpdateReady() {
    document.getElementById('update-message').textContent = '更新已下载完成'
    document.getElementById('update-progress').classList.add('hidden')
    document.getElementById('download-update-btn').classList.add('hidden')
    document.getElementById('install-update-btn').classList.remove('hidden')
  }

  showUpdateError(error) {
    this.showUpdateUI()
    document.getElementById('update-message').textContent = `更新失败: ${error}`
    document.getElementById('update-progress').classList.add('hidden')
  }

  formatBytes(bytes) {
    if (bytes === 0) return '0 Bytes'
    const k = 1024
    const sizes = ['Bytes', 'KB', 'MB', 'GB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
  }
}

// 初始化更新 UI
document.addEventListener('DOMContentLoaded', () => {
  new UpdateUI()
})

7.7 发布和分发

GitHub Releases 发布

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

class PublishManager {
  constructor() {
    this.packageJson = require('../package.json')
    this.version = this.packageJson.version
  }

  async publishToGitHub() {
    try {
      // 1. 构建应用
      console.log('开始构建应用...')
      execSync('npm run build', { stdio: 'inherit' })

      // 2. 创建 Git 标签
      console.log(`创建版本标签 v${this.version}...`)
      execSync(`git tag v${this.version}`)
      execSync(`git push origin v${this.version}`)

      // 3. 发布到 GitHub
      console.log('发布到 GitHub Releases...')
      execSync('npm run publish', { stdio: 'inherit' })

      console.log('发布完成!')
    } catch (error) {
      console.error('发布失败:', error.message)
      throw error
    }
  }

  async publishToCustomServer() {
    // 自定义服务器发布逻辑
    const distPath = path.join(__dirname, '..', 'dist')
    const files = fs.readdirSync(distPath)

    for (const file of files) {
      if (file.endsWith('.exe') || file.endsWith('.dmg') || file.endsWith('.AppImage')) {
        console.log(`上传文件: ${file}`)
        // 实现文件上传逻辑
        await this.uploadFile(path.join(distPath, file))
      }
    }
  }

  async uploadFile(filePath) {
    // 实现文件上传到自定义服务器的逻辑
    console.log(`上传文件: ${filePath}`)
  }

  generateReleaseNotes() {
    // 生成发布说明
    const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md')
    if (fs.existsSync(changelogPath)) {
      const changelog = fs.readFileSync(changelogPath, 'utf8')
      // 解析 changelog 获取当前版本的更新内容
      return this.parseChangelog(changelog, this.version)
    }
    return `版本 ${this.version} 发布`
  }

  parseChangelog(changelog, version) {
    const lines = changelog.split('\n')
    const versionLine = lines.findIndex(line => line.includes(version))

    if (versionLine === -1) return `版本 ${version} 发布`

    const nextVersionLine = lines.findIndex((line, index) =>
      index > versionLine && line.startsWith('##')
    )

    const endLine = nextVersionLine === -1 ? lines.length : nextVersionLine
    return lines.slice(versionLine + 1, endLine).join('\n').trim()
  }
}

// 使用发布管理器
if (require.main === module) {
  const publishManager = new PublishManager()
  publishManager.publishToGitHub().catch(console.error)
}

自动化 CI/CD 配置

yaml
# .github/workflows/build.yml
name: Build and Release

on:
  push:
    tags:
      - 'v*'

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

    strategy:
      matrix:
        os: [windows-latest, macos-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 application
      run: npm run build
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        CSC_LINK: ${{ secrets.CSC_LINK }}
        CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
        APPLE_ID: ${{ secrets.APPLE_ID }}
        APPLE_ID_PASS: ${{ secrets.APPLE_ID_PASS }}

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

    - name: Release
      uses: softprops/action-gh-release@v1
      if: startsWith(github.ref, 'refs/tags/')
      with:
        files: dist/*
        draft: false
        prerelease: false
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

本章小结

通过本章学习,你应该已经:

  • ✅ 了解了不同打包工具的特点和选择
  • ✅ 掌握了 electron-builder 的基础配置
  • ✅ 学会了准备应用图标和资源文件
  • ✅ 了解了高级打包配置和多环境支持
  • ✅ 掌握了代码签名的基本概念和配置
  • ✅ 学会了实现自动更新功能
  • ✅ 了解了应用发布和分发的流程
  • ✅ 掌握了 CI/CD 自动化构建和发布

在下一章中,我们将学习性能优化和安全最佳实践!

Released under the MIT License.