如何使用Electron + Angular 构建跨平台桌面应用

随着现代前端技术的发展,开发跨平台桌面应用已经不再依赖传统的 C++ 或 Java 桌面框架。Electron 提供了一个基于 Chromium 和 Node.js 的运行时环境,使得使用 Web 技术构建桌面应用成为可能。而 Angular 作为成熟的前端框架,则提供了强大的组件化和模块化能力。

一、为什么选择 Electron + Angular

  • Electron 提供原生的桌面能力(系统托盘、原生菜单、快捷键、文件系统、自动更新等)
  • Angular 提供强大的前端工程化能力(TypeScript、依赖注入、RxJS、模块化、AOT 编译、强大的 CLI)
  • 两者结合既能保持 Web 开发的高效,又能获得接近原生的桌面体验
  • 企业级项目中代码可维护性、类型安全、测试能力远超纯 Electron + HTML/JS 方案

二、项目初始化

1. 创建 Angular 项目

# 推荐使用 Angular 18+
ng new electron-angular-todo --routing=true --style=scss --strict
cd electron-angular-todo

2. 添加 Electron 支持

# 安装 electron
npm install --save-dev electron @types/electron

# 安装常用工具包
npm install --save-dev wait-on concurrently electron-builder

3. 配置 package.json 主入口

{
  "name": "electron-angular-todo",
  "main": "main.js",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "electron": "ng build --base-href ./ && electron .",
    "electron:dev": "concurrently \"ng build --watch\" \"wait-on http://localhost:4200 && electron .\"",
    "build:win": "ng build --configuration production && electron-builder --win",
    "build:mac": "ng build --configuration production && electron-builder --mac",
    "build:linux": "ng build --configuration production && electron-builder --linux"
  }
}

4. 创建 Electron 主进程文件 main.js

// main.js
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, globalShortcut } = require('electron');
const path = require('path');
const isDev = require('electron-is-dev');

let mainWindow;
let tray = null;

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    show: false, // 先不显示,内容加载完再 show 避免白屏
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js')
    }
  });

  const startUrl = isDev 
    ? 'http://localhost:4200' 
    : `file://${path.join(__dirname, '../dist/electron-angular-todo/browser/index.html')}`;

  mainWindow.loadURL(startUrl);

  mainWindow.once('ready-to-show', () => {
    mainWindow.show();
    if (isDev) mainWindow.webContents.openDevTools();
  });

  mainWindow.on('closed', () => {
    mainWindow = null;
  });
}

app.whenReady().then(() => {
  createWindow();
  createTray();

  // 注册全局快捷键 Cmd/Ctrl + Shift + T 显示/隐藏窗口
  globalShortcut.register('CommandOrControl+Shift+T', () => {
    if (mainWindow.isVisible()) {
      mainWindow.hide();
    } else {
      mainWindow.show();
    }
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

// 创建系统托盘
function createTray() {
  const iconPath = path.join(__dirname, 'assets/tray-icon.png');
  const icon = nativeImage.createFromPath(iconPath);
  tray = new Tray(icon.resize({ width: 16, height: 16 }));

  const contextMenu = Menu.buildFromTemplate([
    { label: '显示主窗口', click: () => mainWindow.show() },
    { label: '退出应用', click: () => app.quit() }
  ]);
  tray.setToolTip('Angular Todo Desktop');
  tray.setContextMenu(contextMenu);
  tray.on('click', () => mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show());
}

5. 创建 Preload 脚本(安全上下文桥)

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // 主进程 → 渲染进程(单向)
  onUpdateAvailable: (callback) => ipcRenderer.on('update-available', callback),
  onUpdateDownloaded: (callback) => ipcRenderer.on('update-downloaded', callback),

  // 渲染进程 → 主进程(双向)
  minimize: () => ipcRenderer.send('window-minimize'),
  maximize: () => ipcRenderer.send('window-maximize'),
  close: () => ipcRenderer.send('window-close'),

  // 托盘消息
  showTrayMessage: (title, body) => ipcRenderer.send('show-tray-message', title, body)
});

三、在 Angular 中使用 Electron 能力

1. 定义类型声明

// src/types/electron.d.ts
interface ElectronAPI {
  onUpdateAvailable: (callback: () => void) => void;
  onUpdateDownloaded: (callback: () => void) => void;
  minimize: () => void;
  maximize: () => void;
  close: () => void;
  showTrayMessage: (title: string, body: string) => void;
}

declare global {
  interface Window {
    electronAPI: ElectronAPI;
  }
}

2. 创建窗口控制服务

// src/app/services/window-control.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class WindowControlService {
  minimize() {
    if (this.isElectron()) {
      window.electronAPI.minimize();
    }
  }

  maximize() {
    if (this.isElectron()) {
      window.electronAPI.maximize();
    }
  }

  close() {
    if (this.isElectron()) {
      window.electronAPI.close();
    } else {
      window.close();
    }
  }

  isElectron(): boolean {
    return !!(window && window.electronAPI);
  }
}

3. 自动更新服务(使用 electron-updater)

安装依赖:

npm install electron-updater

主进程添加更新逻辑:

// main.js 追加
const { autoUpdater } = require('electron-updater');

autoUpdater.setFeedURL({
  provider: 'github',
  owner: 'your-username',
  repo: 'electron-angular-todo',
  private: false
});

app.whenReady().then(() => {
  // ... 之前的代码
  autoUpdater.checkForUpdatesAndNotify();

  autoUpdater.on('update-available', () => {
    mainWindow.webContents.send('update-available');
  });

  autoUpdater.on('update-downloaded', () => {
    mainWindow.webContents.send('update-downloaded');
  });
});

ipcMain.on('restart-app', () => {
  autoUpdater.quitAndInstall();
});

Angular 更新提示组件:

// update-notification.component.ts
@Component({
  selector: 'app-update-notification',
  template: `
    <div class="update-banner" *ngIf="updateReady">
      新版本已下载,<button (click)="restart()">立即重启更新</button>
    </div>
  `
})
export class UpdateNotificationComponent implements OnInit, OnDestroy {
  updateReady = false;

  ngOnInit() {
    if (window.electronAPI) {
      window.electronAPI.onUpdateDownloaded(() => this.updateReady = true);
    }
  }

  restart() {
    ipcRenderer.send('restart-app');
  }

  ngOnDestroy() {
    // 清理监听
  }
}

四、实战案例:企业级待办事项应用

功能清单:

  • 任务增删改查(本地 IndexedDB 存储,使用 @ngneat/elf)
  • 系统托盘常驻 + 未完成任务数字角标
  • 全局快捷键创建任务
  • 自动更新
  • 原生通知提醒
  • 深色模式适配(Angular Material)
  • 打包发布(Windows/macOS/Linux)

核心代码片段:

// 托盘未读角标更新
this.store.query(getUnfinishedCount).subscribe(count => {
  if (window.electronAPI) {
    const title = count > 0 ? `(${count}) Todo` : 'Todo';
    window.electronAPI.showTrayMessage(title, count ? `${count} 个待办事项` : '全部完成');
  }
});

五、打包配置(electron-builder)

// package.json 中的 build 配置
"build": {
  "appId": "com.example.todo",
  "productName": "Angular Todo",
  "directories": {
    "output": "release/"
  },
  "files": [
    "dist/electron-angular-todo/**/*",
    "main.js",
    "preload.js"
  ],
  "mac": {
    "target": ["dmg", "zip"],
    "category": "public.app-category.productivity"
  },
  "win": {
    "target": ["nsis", "portable"],
    "icon": "build/icon.ico"
  },
  "linux": {
    "target": ["AppImage", "deb"]
  },
  "nsis": {
    "oneClick": false,
    "allowToChangeInstallationDirectory": true
  }
}

执行打包:

npm run build:win
# 或
npm run build:mac

六、最佳实践总结

  1. 始终开启 contextIsolation + preload 脚本,确保安全
  2. 使用 Angular 的 AOT + production 构建,体积可控制在 15MB 以内(压缩后安装包 ~50MB)
  3. 自动更新推荐使用 GitHub Releases + electron-updater(最简单)
  4. 大文件操作、数据库推荐使用主进程 + IPC,避免阻塞渲染线程
  5. 开发时使用 electron:dev 脚本实现热重载
  6. 托盘图标建议提供多尺寸(16/24/32/48/256)+ @2x 高清版本

通过以上完整方案,你可以快速构建一个技术栈现代、体验接近原生、维护性极高的跨平台桌面应用。Electron + Angular 是目前最成熟、最适合中大型桌面项目的组合之一。

THE END