Electron 应用升级方案与 electron-vite 配置指南
概述
Electron 应用的升级机制是桌面应用开发中的核心功能之一。本文将深入探讨 Electron 应用的各种升级方案、代码签名要求,以及如何使用 electron-vite 构建现代化的 Electron 应用。
Electron 升级方案对比
| 方案 | 平台支持 | 复杂度 | 适用场景 |
|---|---|---|---|
| Electron autoUpdater | macOS, Windows | 中等 | 需要完全控制更新流程 |
| electron-updater | macOS, Windows, Linux | 低 | 推荐方案,功能全面 |
| update-electron-app | macOS, Windows | 极低 | GitHub 开源项目 |
| electron-delta-updater | Windows (NSIS) | 中等 | 需要增量更新 |
方案一:Electron 内置 autoUpdater
The easiest and officially supported one is taking advantage of the built-in Squirrel framework and Electron’s autoUpdater module. — Electron 官方文档
基本用法
import { app, autoUpdater, dialog } from 'electron'
// 设置更新服务器 URLconst server = 'https://your-update-server.com'const url = `${server}/update/${process.platform}/${app.getVersion()}`
autoUpdater.setFeedURL({ url })
// 检查更新autoUpdater.checkForUpdates()
// 监听更新事件autoUpdater.on('update-available', () => { console.log('发现新版本')})
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => { dialog.showMessageBox({ type: 'info', title: '更新已就绪', message: `新版本 ${releaseName} 已下载完成,是否立即安装?`, buttons: ['立即安装', '稍后'] }).then((result) => { if (result.response === 0) { autoUpdater.quitAndInstall() } })})
autoUpdater.on('error', (error) => { console.error('更新出错:', error)})平台差异
flowchart TB
subgraph macOS
A[Squirrel.Mac] --> B[需要代码签名]
B --> C[自动后台更新]
end
subgraph Windows
D[Squirrel.Windows] --> E[需要安装器]
E --> F[首次启动有文件锁]
end
subgraph Linux
G[无内置支持] --> H[使用包管理器]
H --> I[apt/yum/pacman]
end
macOS 注意事项:
- 必须进行代码签名才能使用自动更新
- 使用 Squirrel.Mac 框架
Windows 注意事项:
- 需要使用 electron-winstaller 或 Electron Forge 生成安装包
- 首次启动时 Squirrel.Windows 会获取文件锁,此时无法检查更新
Linux:
- 无内置自动更新支持
- 建议使用发行版的包管理器进行更新
方案二:electron-updater (推荐)
electron-updater 是 electron-builder 提供的更新解决方案,功能比内置 autoUpdater 更强大。
核心优势
- 支持 Linux (AppImage)
- Windows 和 macOS 均支持代码签名验证
- 自动生成所需的元数据文件
- 支持下载进度和分阶段发布
- 内置多种发布源支持
安装
npm install electron-updater# 或pnpm add electron-updater基础配置
import { autoUpdater } from 'electron-updater'import log from 'electron-log'
// 配置日志autoUpdater.logger = logautoUpdater.autoDownload = falseautoUpdater.autoInstallOnAppQuit = true
export function initUpdater() { // 检查更新 autoUpdater.checkForUpdates()
autoUpdater.on('checking-for-update', () => { log.info('正在检查更新...') })
autoUpdater.on('update-available', (info) => { log.info(`发现新版本: ${info.version}`) // 可以在这里提示用户是否下载 autoUpdater.downloadUpdate() })
autoUpdater.on('update-not-available', () => { log.info('当前已是最新版本') })
autoUpdater.on('download-progress', (progress) => { log.info(`下载进度: ${progress.percent.toFixed(1)}%`) })
autoUpdater.on('update-downloaded', () => { log.info('更新已下载,准备安装') // 提示用户重启安装 })
autoUpdater.on('error', (error) => { log.error('更新错误:', error) })}发布源配置
在 package.json 或 electron-builder.yml 中配置:
GitHub Releases
publish: - provider: github owner: your-username repo: your-app releaseType: releaseAmazon S3
publish: - provider: s3 bucket: my-app-releases region: us-east-1 acl: public-read path: /releases/${os}/${arch}通用 HTTP 服务器
publish: - provider: generic url: https://releases.example.com channel: latest多发布源配置
# 第一个用于自动更新,其他用于备份publish: - provider: github owner: your-username repo: your-app - provider: s3 bucket: backup-releases region: eu-west-1高级配置示例
import { NsisUpdater, MacUpdater, AppImageUpdater } from 'electron-updater'import { app } from 'electron'import log from 'electron-log'
interface UpdaterOptions { provider: 'generic' | 'github' | 's3' url?: string owner?: string repo?: string token?: string}
export class AppUpdater { private autoUpdater: NsisUpdater | MacUpdater | AppImageUpdater
constructor(options: UpdaterOptions) { const updaterOptions = this.buildOptions(options)
// 根据平台创建更新器 if (process.platform === 'win32') { this.autoUpdater = new NsisUpdater(updaterOptions) } else if (process.platform === 'darwin') { this.autoUpdater = new MacUpdater(updaterOptions) } else { this.autoUpdater = new AppImageUpdater(updaterOptions) }
this.autoUpdater.logger = log this.autoUpdater.autoDownload = false this.autoUpdater.autoInstallOnAppQuit = true
// 私有仓库需要认证 if (options.token) { this.autoUpdater.addAuthHeader(`Bearer ${options.token}`) }
this.setupEvents() }
private buildOptions(options: UpdaterOptions) { switch (options.provider) { case 'generic': return { provider: 'generic' as const, url: options.url!, useMultipleRangeRequest: true } case 'github': return { provider: 'github' as const, owner: options.owner!, repo: options.repo!, private: !!options.token } case 's3': return { provider: 's3' as const, bucket: options.url!, region: 'us-east-1' } default: throw new Error(`未知的 provider: ${options.provider}`) } }
private setupEvents() { this.autoUpdater.on('checking-for-update', () => { log.info('检查更新中...') })
this.autoUpdater.on('update-available', (info) => { log.info(`发现新版本: ${info.version}`) })
this.autoUpdater.on('download-progress', (progress) => { log.info(`下载进度: ${progress.percent.toFixed(1)}%`) })
this.autoUpdater.on('update-downloaded', () => { log.info('更新已下载') })
this.autoUpdater.on('error', (error) => { log.error('更新错误:', error) }) }
async checkForUpdates() { return this.autoUpdater.checkForUpdates() }
async downloadUpdate() { return this.autoUpdater.downloadUpdate() }
quitAndInstall() { this.autoUpdater.quitAndInstall(false, true) }}方案三:update-electron-app
update-electron-app 是 Electron 官方提供的零配置更新模块,专为在 GitHub 发布的开源应用设计。
安装使用
npm install update-electron-appimport { updateElectronApp } from 'update-electron-app'
updateElectronApp()只需这一行代码,它会自动:
- 读取
package.json中的repository字段 - 使用 update.electronjs.org 服务检查更新
- 定期检查更新并提示用户安装
适用条件
- 应用必须开源并发布在 GitHub
- 有公开的 GitHub Releases
- 应用已进行代码签名 (macOS)
增量更新 (Delta Updates)
Electron 应用体积通常在 100-200MB,每次更新都下载完整包会消耗大量带宽。增量更新可以显著减少下载量。
更新方式对比
| 方式 | 下载量 | 实现复杂度 | 平台支持 |
|---|---|---|---|
| 全量更新 | 100-200MB | 低 | 全平台 |
| Blockmap 差分更新 | 10-50MB | 低 | Windows, macOS |
| 二进制差分 (Delta) | 0.1-1MB | 中 | Windows |
electron-updater 的 Blockmap 差分更新
electron-builder 内置的差分更新基于 blockmap 机制:
flowchart LR
A[下载新版 blockmap] --> B[对比新旧 blockmap]
B --> C[识别变化的块]
C --> D[仅下载变化部分]
D --> E[本地重组完整文件]
工作原理:
- 构建时生成
.blockmap文件,记录文件的块哈希 - 更新时下载新旧版本的 blockmap
- 对比识别变化的块 (如 “File has 35 changed blocks”)
- 仅下载变化部分 (如 “Full: 57,990 KB, To download: 764 KB (1%)”)
配置方式:
electron-builder 默认启用差分更新,确保以下文件上传到服务器:
# Windows NSISapp-setup.exeapp-setup.exe.blockmaplatest.yml
# macOSapp.zipapp.zip.blockmaplatest-mac.yml验证差分更新是否生效:
查看更新日志,应出现类似信息:
Download block maps (old: 'app-1.0.0.exe.blockmap', new: 'app-1.1.0.exe.blockmap')File has 35 changed blocksFull: 57,990.1 KB, To download: 764.79 KB (1%)真正的二进制差分:@electron-delta
The delta update mechanism based on the blockmap approach uses a very good amount of bandwidth. The binary diffing approach works better and downloads only the diff, generally less than 1MB to update the app. — electron-delta 文档
electron-delta 使用 HDiffPatch 库实现真正的二进制差分,可将更新包压缩到几 KB 到 1MB。
安装:
npm install @electron-delta/builder --save-devnpm install @electron-delta/updater --save配置 electron-builder:
const DeltaBuilder = require('@electron-delta/builder')
const options = { productIconPath: './build/icon.ico', productName: 'YourApp',
getPreviousReleases: async () => { // 返回之前版本的信息,用于生成 delta return [ { version: '1.0.0', url: 'https://your-server.com/releases/app-1.0.0.exe' } ] },
sign: async (filePath) => { // 可选:签名 delta 文件 }}
exports.default = async function(context) { const deltaBuilder = new DeltaBuilder(options) await deltaBuilder.build({ context, onProgress: (progress) => console.log(`Delta progress: ${progress}%`) })}// package.json 或 electron-builder.yml{ "build": { "afterAllArtifactBuild": ".electron-delta.js" }}在应用中使用:
import { DeltaUpdater } from '@electron-delta/updater'import { app } from 'electron'
const deltaUpdater = new DeltaUpdater({ logger: console, autoDownload: false, autoInstallOnAppQuit: true})
export async function checkForUpdates() { try { const result = await deltaUpdater.checkForUpdates() if (result?.updateInfo) { console.log(`发现新版本: ${result.updateInfo.version}`) // Delta 更新会自动优先使用 await deltaUpdater.downloadUpdate() } } catch (error) { console.error('更新检查失败:', error) }}
deltaUpdater.on('update-downloaded', () => { // 提示用户重启安装})限制:
- 目前仅支持 Windows (NSIS)
- macOS 支持开发中
- 需要保留历史版本用于生成 delta
更新服务器后端配置
根据项目需求,可以选择不同的更新服务器方案。
方案对比
| 方案 | 适用场景 | 维护成本 | 功能 |
|---|---|---|---|
| GitHub Releases | 开源项目 | 无 | 基础 |
| 静态文件托管 (S3/OSS) | 私有项目 | 低 | 基础 |
| Hazel | 私有 GitHub 项目 | 低 | 基础 + 缓存 |
| Nucleus | 企业级 | 中 | 多应用 + 渠道 |
| electron-release-server | 完全自主控制 | 高 | 全功能 + UI |
静态文件托管方案
最简单的方案是使用静态文件托管服务 (S3、阿里云 OSS、Cloudflare R2 等)。
目录结构:
releases/├── win32/│ ├── latest.yml│ ├── app-setup-1.0.0.exe│ ├── app-setup-1.0.0.exe.blockmap│ ├── app-setup-1.1.0.exe│ └── app-setup-1.1.0.exe.blockmap├── darwin/│ ├── latest-mac.yml│ ├── app-1.0.0.zip│ ├── app-1.0.0.zip.blockmap│ ├── app-1.1.0.zip│ └── app-1.1.0.zip.blockmap└── linux/ ├── latest-linux.yml └── app-1.1.0.AppImagelatest.yml 示例:
version: 1.1.0files: - url: app-setup-1.1.0.exe sha512: <sha512-hash> size: 85000000path: app-setup-1.1.0.exesha512: <sha512-hash>releaseDate: '2026-02-01T10:00:00.000Z'electron-builder 配置:
publish: - provider: generic url: https://your-bucket.s3.amazonaws.com/releases channel: latest应用端配置:
import { autoUpdater } from 'electron-updater'
// 设置更新源autoUpdater.setFeedURL({ provider: 'generic', url: 'https://your-bucket.s3.amazonaws.com/releases'})
autoUpdater.checkForUpdates()Squirrel 更新服务器 API 规范
如果使用 Electron 内置的 autoUpdater (非 electron-updater),服务器需要实现 Squirrel 协议。
URL 格式:
const server = 'https://your-server.com'const url = `${server}/update/${process.platform}/${app.getVersion()}`autoUpdater.setFeedURL({ url })macOS 响应格式 (Squirrel.Mac):
有更新时返回 200 OK + JSON:
{ "url": "https://your-server.com/releases/app-1.1.0.zip", "name": "v1.1.0", "notes": "Bug fixes and improvements", "pub_date": "2026-02-01T10:00:00Z"}无更新时返回 204 No Content。
Windows 响应格式 (Squirrel.Windows):
客户端会请求 /RELEASES 子路径,返回 RELEASES 文件内容:
<sha1> app-1.1.0-full.nupkg <size><sha1> app-1.1.0-delta.nupkg <size>Hazel - 轻量级更新服务器
Hazel 是 Vercel 官方维护的轻量级更新服务器,可免费部署在 Vercel 上。
特点:
- 自动从 GitHub Releases 拉取更新
- 利用 GitHub CDN 分发
- 支持私有仓库
- 自动缓存
部署到 Vercel:
- Fork hazel 仓库
- 在 Vercel 中导入项目
- 配置环境变量:
ACCOUNT=your-github-usernameREPOSITORY=your-repo-nameTOKEN=your-github-token # 私有仓库需要PRE=1 # 可选:仅提供预发布版本INTERVAL=15 # 可选:缓存刷新间隔 (分钟)应用端配置:
import { autoUpdater } from 'electron'
const server = 'https://your-hazel.vercel.app'const url = `${server}/update/${process.platform}/${app.getVersion()}`
autoUpdater.setFeedURL({ url })autoUpdater.checkForUpdates()electron-release-server - 全功能自托管方案
electron-release-server 提供完整的发布管理功能。
特点:
- Web 管理界面
- 多渠道支持 (stable, beta, alpha)
- 版本管理
- 下载统计
- 兼容 Squirrel 自动更新
Docker 部署:
version: '3'services: app: image: eoskin/electron-release-server ports: - "8080:80" environment: - APP_USERNAME=admin - APP_PASSWORD=your-secure-password - DB_HOST=db - DB_PORT=5432 - DB_USERNAME=postgres - DB_PASSWORD=postgres - DB_NAME=electron_release_server - TOKEN_SECRET=your-64-char-random-string - DATA_ENCRYPTION_KEY=your-32-byte-base64-key depends_on: - db volumes: - ./assets:/app/assets
db: image: postgres:14 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=electron_release_server volumes: - postgres_data:/var/lib/postgresql/data
volumes: postgres_data:生成密钥:
# 生成 DATA_ENCRYPTION_KEYnode -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# 生成 TOKEN_SECRETnode -e "console.log(require('crypto').randomBytes(48).toString('hex'))"启动服务:
docker-compose up -dNginx 反向代理配置:
server { listen 443 ssl; server_name releases.your-domain.com;
ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem;
location / { proxy_pass http://localhost:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache_bypass $http_upgrade; }}应用端配置:
import { autoUpdater } from 'electron'
const server = 'https://releases.your-domain.com'const url = `${server}/update/${process.platform}/${app.getVersion()}`
autoUpdater.setFeedURL({ url })autoUpdater.checkForUpdates()Nucleus - 企业级方案
Nucleus 是 Atlassian 开源的更新服务器,支持多应用和多渠道。
特点:
- 支持多个应用
- 多渠道 (stable, beta, alpha)
- 静态文件存储 (S3、本地)
- 百分比灰度发布
- 管理后台
适用场景:
- 企业内部多个 Electron 应用
- 需要精细化发布控制
- 已有 S3/CloudFront 基础设施
自定义更新服务器 (Node.js 示例)
如果需要完全自定义逻辑,可以自己实现更新服务器:
import express from 'express'import path from 'path'import fs from 'fs'
const app = express()const RELEASES_DIR = './releases'
// 获取最新版本信息function getLatestRelease(platform: string) { const ymlFile = platform === 'darwin' ? 'latest-mac.yml' : 'latest.yml' const ymlPath = path.join(RELEASES_DIR, platform, ymlFile)
if (fs.existsSync(ymlPath)) { const content = fs.readFileSync(ymlPath, 'utf-8') // 解析 YAML 返回版本信息 return parseYaml(content) } return null}
// Squirrel.Mac 更新检查端点app.get('/update/darwin/:version', (req, res) => { const currentVersion = req.params.version const latest = getLatestRelease('darwin')
if (!latest || latest.version === currentVersion) { return res.status(204).end() }
res.json({ url: `https://your-server.com/releases/darwin/${latest.path}`, name: `v${latest.version}`, notes: latest.releaseNotes || '', pub_date: latest.releaseDate })})
// Squirrel.Windows RELEASES 端点app.get('/update/win32/:version/RELEASES', (req, res) => { const releasesPath = path.join(RELEASES_DIR, 'win32', 'RELEASES')
if (fs.existsSync(releasesPath)) { res.type('text/plain').sendFile(releasesPath) } else { res.status(404).end() }})
// 静态文件服务app.use('/releases', express.static(RELEASES_DIR))
app.listen(3000, () => { console.log('Update server running on port 3000')})更新服务器最佳实践
代码签名详解
代码签名是桌面应用发布的核心环节。本节深入讲解 Windows 和 macOS 平台的签名方案、最佳实践以及 CI/CD 自动化配置。
为什么需要代码签名
flowchart LR
A[未签名应用] --> B{操作系统检查}
B -->|macOS| C[Gatekeeper 阻止]
B -->|Windows| D[SmartScreen 警告]
E[已签名应用] --> F{操作系统检查}
F -->|macOS| G[正常运行]
F -->|Windows| H[正常运行]
Code signing is a security technology to certify that an app was created by you. You should sign your application so it does not trigger any operating system security warnings. — Electron 官方文档
Windows 代码签名
证书类型对比
| 类型 | SmartScreen | 硬件要求 | 价格 | 适用场景 |
|---|---|---|---|---|
| Azure Trusted Signing | 即时信任 | 无 (云端) | $9.99/月 | 推荐方案 |
| 标准 OV 证书 | 需积累信任 | HSM/云端 | $200-500/年 | 小型项目 |
| EV 证书 | 即时信任 | USB Token | $300-600/年 | 驱动开发 |
重要变更 (2024)
EV certificates previously provided instant SmartScreen reputation, bypassing security warnings immediately. However, in March 2024, Microsoft changed how SmartScreen interacts with EV certificates - they no longer instantly remove warnings. — SSL Insights
2024 年 3 月起,EV 证书不再自动获得 SmartScreen 信任,OV 和 EV 证书现在都需要通过下载量积累信任。
硬件存储要求 (2023.06 起)
Starting June 1, 2023, private keys for code signing certificates need to be stored on a hardware storage module compliant with FIPS 140 Level 2. — electron-builder 文档
这意味着纯软件证书已不再可用,必须使用:
- 物理 USB Token (HSM)
- 云端签名服务 (如 Azure、DigiCert KeyLocker)
Azure Trusted Signing (推荐方案)
Azure Artifact Signing (原 Azure Trusted Signing) 是 Microsoft 官方云端签名服务。
优势:
- 价格最低 ($9.99/月)
- 即时获得 SmartScreen 信任
- 无需管理物理硬件
- 证书有效期仅 3 天,安全性更高
- CI/CD 友好
可用性限制 (2025):
- 美国、加拿大、欧盟、英国的组织
- 美国、加拿大的个人开发者
- 组织需要 3 年以上可验证的经营历史
设置步骤:
- 创建 Azure 账户并设置 Trusted Signing Account
- 创建 App Registration 并生成 Secret
- 分配 “Trusted Signing Certificate Profile Signer” 角色
- 配置 electron-builder
win: azureSignOptions: endpoint: https://eus.codesigning.azure.net codeSigningAccountName: your-account-name certificateProfileName: your-profile-name环境变量配置:
# Azure 认证export AZURE_TENANT_ID="your-tenant-id"export AZURE_CLIENT_ID="your-client-id"export AZURE_CLIENT_SECRET="your-client-secret"传统证书签名
如果无法使用 Azure Trusted Signing,可以使用传统证书:
win: certificateFile: path/to/certificate.pfx certificatePassword: ${env.WIN_CSC_KEY_PASSWORD} # 或使用证书主题名称 (适用于 HSM) certificateSubjectName: "Your Company Name"云端签名服务选项:
- DigiCert KeyLocker
- SSL.com eSigner
- GlobalSign
macOS 代码签名与公证
签名流程概览
flowchart TD
A[构建应用] --> B[代码签名]
B --> C[启用 Hardened Runtime]
C --> D[上传到 Apple 公证服务]
D --> E{公证结果}
E -->|通过| F[Staple 票据]
E -->|失败| G[查看日志修复问题]
F --> H[分发应用]
前提条件
- Apple Developer Program ($99/年)
- Developer ID 证书:
- Developer ID Application (应用签名)
- Developer ID Installer (PKG 签名)
- App-Specific Password (用于 notarytool)
Hardened Runtime
In the latest macOS versions, Apple requires that the hardened runtime is enabled to launch apps. It protects the app from malware injections, DLL attacks, and process memory space tampering. — Electron Forge
Electron 应用必须启用 Hardened Runtime 并配置正确的 entitlements:
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> <!-- Electron 必需 --> <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.cs.allow-dyld-environment-variables</key> <true/> <key>com.apple.security.automation.apple-events</key> <true/></dict></plist>公证 (Notarization)
From macOS 10.15 (Catalina) onwards, your application needs to be both code signed and notarized to run on a user’s machine without disabling additional operating system security checks. — Apple Developer Documentation
使用 notarytool 命令行工具:
# 1. 存储凭证 (一次性设置)xcrun notarytool store-credentials "notary-profile" \ --apple-id "your-apple-id@example.com" \ --team-id "ABCD123456" \ --password "app-specific-password"
# 2. 提交公证xcrun notarytool submit MyApp.dmg \ --keychain-profile "notary-profile" \ --wait
# 3. Staple 票据xcrun stapler staple MyApp.dmg
# 4. 验证 (可选)xcrun stapler validate MyApp.dmg重要提醒:
The Apple notary service will no longer accept uploads from Xcode 13 or earlier or from altool as of November 1, 2023. — Apple Developer Documentation
electron-builder macOS 完整配置
mac: category: public.app-category.developer-tools target: - dmg - zip hardenedRuntime: true gatekeeperAssess: false entitlements: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist notarize: teamId: ${env.APPLE_TEAM_ID}
# DMG 配置dmg: sign: false # DMG 本身不需要签名,内部 .app 已签名 contents: - x: 130 y: 220 - x: 410 y: 220 type: link path: /Applications环境变量配置:
# Apple 签名export APPLE_ID="your-apple-id@example.com"export APPLE_APP_SPECIFIC_PASSWORD="xxxx-xxxx-xxxx-xxxx"export APPLE_TEAM_ID="XXXXXXXXXX"
# 证书 (CI/CD 使用)export CSC_LINK="base64-encoded-p12-certificate"export CSC_KEY_PASSWORD="certificate-password"CI/CD 自动签名配置
GitHub Actions 完整示例
name: Release
on: push: tags: - 'v*'
jobs: build: strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
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
# macOS 签名设置 - name: Setup macOS signing if: matrix.os == 'macos-latest' env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security default-keychain -s build.keychain security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Windows Azure Trusted Signing 设置 - name: Setup Windows signing if: matrix.os == 'windows-latest' run: | dotnet tool install --global AzureSignTool
- name: Build and Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # macOS APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }} CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }} # Windows Azure Trusted Signing AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} run: npm run release所需 GitHub Secrets
| Secret | 平台 | 说明 |
|---|---|---|
MACOS_CERTIFICATE | macOS | Base64 编码的 .p12 证书 |
MACOS_CERTIFICATE_PWD | macOS | 证书密码 |
KEYCHAIN_PASSWORD | macOS | 临时 Keychain 密码 |
APPLE_ID | macOS | Apple ID 邮箱 |
APPLE_APP_SPECIFIC_PASSWORD | macOS | App 专用密码 |
APPLE_TEAM_ID | macOS | 团队 ID |
AZURE_TENANT_ID | Windows | Azure 租户 ID |
AZURE_CLIENT_ID | Windows | Azure 应用 ID |
AZURE_CLIENT_SECRET | Windows | Azure 应用密钥 |
生成 Base64 证书
# macOSbase64 -i Certificates.p12 -o encoded.txt
# Linuxbase64 -w 0 Certificates.p12 > encoded.txt签名最佳实践
常见问题排查
macOS 公证失败
# 查看详细日志xcrun notarytool log <submission-id> \ --keychain-profile "notary-profile"常见原因:
- 未启用 Hardened Runtime
- 缺少必需的 entitlements
- 使用了不允许的 entitlements
- 包含未签名的二进制文件
Windows SmartScreen 警告
- OV 证书需要积累下载量建立信任
- 使用 Azure Trusted Signing 可即时获得信任
- 确保签名时间戳有效
GitHub Actions 签名挂起
If your GitHub workflow just hangs forever at the Electron signing step, this appears to be a symptom of not properly configuring the macOS Keychain in the CI environment. — Code Jam
解决方案:确保正确创建和解锁 Keychain,设置 security set-key-partition-list。
electron-vite 配置指南
electron-vite 是基于 Vite 的下一代 Electron 构建工具,提供极速的开发体验。
核心特性
创建项目
npm create electron-vite@latest my-appcd my-appnpm installnpm run dev项目结构
my-app/├── electron.vite.config.ts├── package.json├── src/│ ├── main/│ │ └── index.ts│ ├── preload/│ │ └── index.ts│ └── renderer/│ ├── index.html│ └── src/│ ├── App.tsx│ └── main.tsx基础配置
import { defineConfig } from 'electron-vite'
export default defineConfig({ main: { // 主进程 Vite 配置 }, preload: { // 预加载脚本 Vite 配置 }, renderer: { // 渲染进程 Vite 配置 }})完整配置示例
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'import react from '@vitejs/plugin-react'import tailwindcss from '@tailwindcss/vite'import { resolve } from 'path'
export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/main/index.ts') } }, outDir: 'dist/main' } }, preload: { plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/preload/index.ts') } }, outDir: 'dist/preload' } }, renderer: { root: 'src/renderer', resolve: { alias: { '@renderer': resolve('src/renderer/src'), '@': resolve('src/renderer/src') } }, plugins: [react(), tailwindcss()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/renderer/index.html') } }, outDir: 'dist/renderer' } }})多窗口配置
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'import { resolve } from 'path'
export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { index: resolve(__dirname, 'src/main/index.ts') } } } }, preload: { plugins: [externalizeDepsPlugin()], build: { rollupOptions: { input: { browser: resolve(__dirname, 'src/preload/browser.ts'), webview: resolve(__dirname, 'src/preload/webview.ts') } } } }, renderer: { build: { rollupOptions: { input: { browser: resolve(__dirname, 'src/renderer/browser.html'), settings: resolve(__dirname, 'src/renderer/settings.html') } } } }})主进程加载窗口
import { app, BrowserWindow } from 'electron'import path from 'path'
function createWindow() { const mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, '../preload/browser.js'), sandbox: false } })
// 开发环境使用 HMR if (!app.isPackaged && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { mainWindow.loadFile(path.join(__dirname, '../renderer/browser.html')) }}
app.whenReady().then(createWindow)资源路径处理
Vite 默认使用绝对路径,在 Electron 的 file:// 协议下可能出问题。解决方案:
export default defineConfig({ renderer: { base: './', // 使用相对路径 build: { // ... } }})主进程中使用 loadFile 而非 loadURL:
// 生产环境mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))与 electron-builder 集成
{ "scripts": { "dev": "electron-vite dev", "build": "electron-vite build", "postbuild": "electron-builder" }, "build": { "directories": { "output": "release" }, "files": [ "dist/**/*" ], "mac": { "target": ["dmg", "zip"] }, "win": { "target": ["nsis"] }, "linux": { "target": ["AppImage", "deb"] }, "publish": { "provider": "github" } }}完整项目示例
目录结构
my-electron-app/├── electron.vite.config.ts├── electron-builder.yml├── package.json├── src/│ ├── main/│ │ ├── index.ts│ │ └── updater.ts│ ├── preload/│ │ └── index.ts│ └── renderer/│ ├── index.html│ └── src/│ ├── App.tsx│ ├── main.tsx│ └── components/└── resources/ └── icon.pngpackage.json
{ "name": "my-electron-app", "version": "1.0.0", "main": "dist/main/index.js", "scripts": { "dev": "electron-vite dev", "build": "electron-vite build", "postbuild": "electron-builder", "release": "electron-vite build && electron-builder --publish always" }, "dependencies": { "electron-updater": "^6.3.0" }, "devDependencies": { "electron": "^33.0.0", "electron-builder": "^25.0.0", "electron-vite": "^3.0.0", "@vitejs/plugin-react": "^4.0.0", "typescript": "^5.0.0" }}electron-builder.yml
appId: com.example.myappproductName: My Electron App
directories: output: release
files: - dist/**/* - package.json
mac: category: public.app-category.developer-tools target: - dmg - zip hardenedRuntime: true notarize: teamId: ${env.APPLE_TEAM_ID}
win: target: - nsis certificateSubjectName: "Your Company"
linux: target: - AppImage - deb
nsis: oneClick: false allowToChangeInstallationDirectory: true
publish: - provider: github owner: your-username repo: my-electron-app参考资料
自动更新
- Electron autoUpdater 官方文档
- Electron 更新应用教程
- electron-builder Auto Update
- update-electron-app
- @electron-delta/updater
代码签名
- Electron 代码签名指南
- electron-builder 代码签名
- Azure Artifact Signing 快速入门
- Azure Trusted Signing 实践指南
- Apple 公证工作流
- Apple notarytool 完整指南
- Electron Forge macOS 签名指南
- Electron Forge Windows 签名指南