Skip to content

Electron 打包与分发

构建、自动更新与代码签名

目录


打包工具

工具对比

工具特点推荐场景
electron-builder功能全面、配置灵活生产项目首选
electron-forgeElectron 官方、插件化新项目快速启动
electron-packager轻量、仅打包简单需求

配置实践

electron-builder 配置

json
// package.json
{
  "name": "my-app",
  "version": "1.0.0",
  "main": "dist/main.js",
  "scripts": {
    "build": "electron-builder",
    "build:mac": "electron-builder --mac",
    "build:win": "electron-builder --win",
    "build:linux": "electron-builder --linux"
  },
  "build": {
    "appId": "com.mycompany.myapp",
    "productName": "My App",
    
    "directories": {
      "output": "release",
      "buildResources": "resources"
    },
    
    "files": [
      "dist/**/*",
      "node_modules/**/*",
      "package.json"
    ],
    
    "mac": {
      "category": "public.app-category.productivity",
      "icon": "resources/icon.icns",
      "target": [
        { "target": "dmg", "arch": ["x64", "arm64"] },
        { "target": "zip", "arch": ["x64", "arm64"] }
      ],
      "hardenedRuntime": true,
      "gatekeeperAssess": false,
      "entitlements": "resources/entitlements.mac.plist",
      "entitlementsInherit": "resources/entitlements.mac.plist"
    },
    
    "win": {
      "icon": "resources/icon.ico",
      "target": [
        { "target": "nsis", "arch": ["x64"] },
        { "target": "portable" }
      ]
    },
    
    "nsis": {
      "oneClick": false,
      "allowToChangeInstallationDirectory": true,
      "installerIcon": "resources/icon.ico",
      "uninstallerIcon": "resources/icon.ico",
      "createDesktopShortcut": true,
      "createStartMenuShortcut": true
    },
    
    "linux": {
      "icon": "resources/icons",
      "target": ["AppImage", "deb"],
      "category": "Utility"
    },
    
    "publish": {
      "provider": "github",
      "owner": "mycompany",
      "repo": "my-app"
    }
  }
}

electron-forge 配置

javascript
// forge.config.js
module.exports = {
  packagerConfig: {
    icon: './resources/icon',
    asar: true
  },
  
  makers: [
    {
      name: '@electron-forge/maker-squirrel',
      config: {
        name: 'my_app'
      }
    },
    {
      name: '@electron-forge/maker-dmg',
      config: {
        format: 'ULFO'
      }
    },
    {
      name: '@electron-forge/maker-deb',
      config: {}
    }
  ],
  
  plugins: [
    {
      name: '@electron-forge/plugin-webpack',
      config: {
        mainConfig: './webpack.main.config.js',
        renderer: {
          config: './webpack.renderer.config.js',
          entryPoints: [
            {
              html: './src/index.html',
              js: './src/renderer.js',
              name: 'main_window',
              preload: {
                js: './src/preload.js'
              }
            }
          ]
        }
      }
    }
  ]
};

自动更新

electron-updater 配置

typescript
// main.js
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';

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

// 检查更新
export function checkForUpdates() {
  autoUpdater.checkForUpdatesAndNotify();
}

// 事件监听
autoUpdater.on('checking-for-update', () => {
  log.info('Checking for update...');
});

autoUpdater.on('update-available', (info) => {
  log.info('Update available:', info.version);
  // 通知渲染进程
  mainWindow.webContents.send('update-available', info);
});

autoUpdater.on('update-not-available', () => {
  log.info('Update not available.');
});

autoUpdater.on('download-progress', (progress) => {
  log.info(`Download progress: ${progress.percent}%`);
  mainWindow.webContents.send('download-progress', progress);
});

autoUpdater.on('update-downloaded', (info) => {
  log.info('Update downloaded');
  // 提示用户重启
  dialog.showMessageBox({
    type: 'info',
    title: 'Update Ready',
    message: `Version ${info.version} has been downloaded. Restart to apply.`,
    buttons: ['Restart', 'Later']
  }).then(({ response }) => {
    if (response === 0) {
      autoUpdater.quitAndInstall();
    }
  });
});

autoUpdater.on('error', (err) => {
  log.error('Update error:', err);
});

更新服务配置

typescript
// GitHub Releases
autoUpdater.setFeedURL({
  provider: 'github',
  owner: 'mycompany',
  repo: 'my-app'
});

// 自建服务器
autoUpdater.setFeedURL({
  provider: 'generic',
  url: 'https://updates.myapp.com'
});

// 私有 S3
autoUpdater.setFeedURL({
  provider: 's3',
  bucket: 'my-app-updates',
  region: 'us-east-1'
});

更新流程

应用启动 → 检查更新 → 下载更新 → 提示用户 → 重启应用


                              /update/latest.yml (元数据)
                              /update/app-1.2.0.exe (安装包)
                              /update/app-1.2.0.exe.blockmap (增量)

代码签名

macOS 签名

bash
# 1. 获取证书
# Apple Developer Program → Certificates

# 2. 配置 electron-builder
# package.json
{
  "build": {
    "mac": {
      "identity": "Developer ID Application: Company Name (TEAM_ID)",
      "hardenedRuntime": true,
      "entitlements": "resources/entitlements.mac.plist",
      "entitlementsInherit": "resources/entitlements.mac.plist"
    },
    "afterSign": "scripts/notarize.js"
  }
}
xml
<!-- 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.disable-library-validation</key>
  <true/>
</dict>
</plist>
javascript
// scripts/notarize.js
const { notarize } = require('@electron/notarize');

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;
  
  if (electronPlatformName !== 'darwin') return;
  
  const appName = context.packager.appInfo.productFilename;
  
  await notarize({
    appBundleId: 'com.mycompany.myapp',
    appPath: `${appOutDir}/${appName}.app`,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_ID_PASSWORD,
    teamId: process.env.APPLE_TEAM_ID
  });
};

Windows 签名

json
// package.json
{
  "build": {
    "win": {
      "certificateFile": "path/to/certificate.pfx",
      "certificatePassword": "${env.WIN_CSC_KEY_PASSWORD}",
      "signingHashAlgorithms": ["sha256"]
    }
  }
}

CI/CD 配置

GitHub Actions

yaml
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

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

    steps:
      - uses: actions/checkout@v3
      
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      
      - run: npm ci
      - run: npm run build
      
      - name: Build Electron
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          # macOS 签名
          CSC_LINK: ${{ secrets.MAC_CERTS }}
          CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          # Windows 签名
          WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
        run: npm run electron:build
      
      - uses: actions/upload-artifact@v3
        with:
          name: release-${{ matrix.os }}
          path: release/*

高频面试题

Q1: 如何实现增量更新?

electron-updater 自动支持

  • 生成 .blockmap 文件(块级差异)
  • 只下载变化的块
  • 客户端合并块还原完整文件

Q2: macOS 公证 (Notarization) 是什么?

  1. 上传应用到 Apple 服务器
  2. Apple 扫描恶意代码
  3. 获取公证票据
  4. stapler 票据到应用

目的:Gatekeeper 验证,用户首次打开不会提示"无法验证开发者"

Q3: ASAR 打包的作用?

特性不打包ASAR
文件结构暴露源码归档隐藏
读取效率多次 IO单次加载
路径长度Windows 260 限制无限制
安全性中(仍可解包)

Q4: 如何减少安装包体积?

  1. 排除开发依赖devDependencies 不打包
  2. 平台原生模块:只包含目标平台
  3. 分析依赖:删除未使用的 npm 包
  4. 代码压缩:production 构建
  5. 图标优化:只保留必要尺寸
json
{
  "build": {
    "files": [
      "!**/node_modules/*/{CHANGELOG.md,README.md}",
      "!**/node_modules/*/{test,__tests__,tests}"
    ]
  }
}

前端面试知识库