Electron 桌面应用开发:从入门到精通

🚀 Electron 桌面应用开发

从入门到精通的完整指南

跨平台桌面应用 Web 技术 JavaScript

📚 目录

💡

致读者

本教程假设您已经具备 HTML、CSS 和 JavaScript 的基础知识。如果您是前端开发人员,那么恭喜您,Electron 将让您如鱼得水!

第一部分:Electron 入门基础

1.1 Electron 简介与核心概念

想象一下,您用 HTML 写了一个漂亮的网页,用 CSS 美化了它,再用 JavaScript 赋予了它各种交互功能。现在,您想把这个"网页"变成一个独立运行在 Windows、macOS 或 Linux 系统上的桌面程序,就像 QQ、VS Code 或者 Slack 那样。Electron 就是实现这个梦想的神奇工具!

Electron 是由 GitHub 开发的一个开源框架,它允许开发者使用Web 技术(HTML、CSS 和 JavaScript)构建原生的跨平台桌面应用程序。它通过将 Chromium(Google Chrome 浏览器核心)和 Node.js(JavaScript 运行时)集成到一个软件包中来实现这一点。

核心概念:主进程 vs 渲染进程

这是理解 Electron 最最核心的两个概念。可以把它们想象成一场演出的两位主角:

graph TD A[主进程 Main Process] --> B[渲染进程 1
BrowserWindow] A --> C[渲染进程 2
BrowserWindow] A --> D[渲染进程 N
BrowserWindow] B <--> E[IPC 通信] C <--> E D <--> E E <--> A F[Node.js APIs
文件系统/网络] --> A B --> G[Chromium
HTML/CSS/JS] C --> H[Chromium
HTML/CSS/JS] D --> I[Chromium
HTML/CSS/JS]
🎭 主进程 (Main Process)
  • • 管理应用程序生命周期
  • • 创建和销毁窗口
  • • 处理系统级交互
  • • 访问 Node.js API
  • • 一个应用只有一个主进程
🎨 渲染进程 (Renderer Process)
  • • 显示用户界面
  • • 运行在 Chromium 环境
  • • 执行前端 JavaScript
  • • 通过 IPC 与主进程通信
  • • 每个窗口一个渲染进程
⚠️

重要提示

渲染进程不能直接访问 Node.js API!如果渲染进程需要进行文件读写等操作,它必须通过进程间通信 (IPC) 的方式,请求主进程代为执行。

1.2 开发环境搭建

开始之前,我们需要准备好开发环境。

步骤 1:安装 Node.js & npm

Electron 基于 Node.js 运行,并使用 npm 进行包管理。如果您尚未安装,请前往 Node.js 官网 下载并安装 LTS 版本。

node -v
npm -v

步骤 2:创建项目

mkdir my-electron-app
cd my-electron-app
npm init -y
npm install --save-dev electron

步骤 3:创建应用文件

一个基本的 Electron 应用需要三个核心文件:

main.js

主进程脚本

index.html

渲染进程界面

preload.js

安全桥梁脚本

main.js (主进程):

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow () {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js')
    }
  });

  mainWindow.loadFile('index.html');
}

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

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

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

index.html (界面):

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>我的第一个 Electron 应用</title>
    <style>
        body { 
            font-family: sans-serif; 
            display: flex; 
            justify-content: center; 
            align-items: center; 
            height: 100vh; 
            margin: 0; 
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        h1 { font-size: 3em; text-shadow: 2px 2px 4px rgba(0,0,0,0.3); }
    </style>
</head>
<body>
    <h1>Hello, Electron World! 👋</h1>
</body>
</html>

preload.js (预加载脚本):

// 当前暂时留空,后续会详细讲解
// 这个脚本是主进程和渲染进程之间的安全桥梁

步骤 4:配置启动脚本

package.json 中添加启动脚本:

{
  "name": "my-electron-app",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^latest"
  }
}

步骤 5:运行应用

npm start
🎉

恭喜!

如果一切顺利,您应该看到一个漂亮的窗口弹出。您的第一个 Electron 应用成功运行了!

第二部分:Electron 核心开发

2.1 主进程开发详解

主进程是 Electron 应用的"大脑",它拥有访问操作系统底层 API 的能力。

🪟 BrowserWindow

  • • 创建和管理窗口
  • • 配置窗口属性
  • • 监听窗口事件
  • • 控制窗口显示

⚙️ App 模块

  • • 应用生命周期管理
  • • 系统事件处理
  • • 应用级别配置
  • • 跨平台兼容性

高级窗口配置示例

const mainWindow = new BrowserWindow({
  width: 1200,
  height: 800,
  minWidth: 600,
  minHeight: 400,
  titleBarStyle: 'hiddenInset', // macOS 样式
  frame: true,
  transparent: false,
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
    enableRemoteModule: false,
    preload: path.join(__dirname, 'preload.js')
  }
});

// 窗口事件监听
mainWindow.on('ready-to-show', () => {
  mainWindow.show();
});

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

2.2 进程间通信 (IPC)

IPC 是 Electron 架构的核心,实现主进程与渲染进程之间的安全通信。

sequenceDiagram participant R as 渲染进程 participant P as 预加载脚本 participant M as 主进程 R->>P: 调用暴露的API P->>M: ipcRenderer.invoke() M->>M: 处理请求 M-->>P: 返回结果 P-->>R: 返回给渲染进程
🔒

安全最佳实践

在 Electron 12+ 中,默认启用 contextIsolation: truenodeIntegration: false。必须通过预加载脚本安全地暴露 API。

安全的 IPC 通信示例

preload.js (安全桥梁):

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // 文件操作
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  saveFile: (content) => ipcRenderer.invoke('dialog:saveFile', content),
  
  // 系统信息
  getVersion: () => ipcRenderer.invoke('app:getVersion'),
  
  // 通知
  showNotification: (message) => ipcRenderer.invoke('notification:show', message)
});

main.js (主进程处理):

const { ipcMain, dialog, app, Notification } = require('electron');
const fs = require('fs');

// 文件对话框
ipcMain.handle('dialog:openFile', async () => {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'],
    filters: [
      { name: 'Text Files', extensions: ['txt'] },
      { name: 'All Files', extensions: ['*'] }
    ]
  });
  
  if (!canceled && filePaths.length > 0) {
    const content = fs.readFileSync(filePaths[0], 'utf-8');
    return { success: true, content, path: filePaths[0] };
  }
  return { success: false };
});

// 获取应用版本
ipcMain.handle('app:getVersion', () => {
  return app.getVersion();
});

// 显示通知
ipcMain.handle('notification:show', (event, message) => {
  new Notification({ title: '应用通知', body: message }).show();
  return { success: true };
});

renderer.js (渲染进程使用):

// 使用预加载脚本暴露的 API
document.addEventListener('DOMContentLoaded', async () => {
  // 获取应用版本
  const version = await window.electronAPI.getVersion();
  console.log('应用版本:', version);
  
  // 打开文件
  const openBtn = document.getElementById('openFile');
  openBtn.addEventListener('click', async () => {
    const result = await window.electronAPI.openFile();
    if (result.success) {
      document.getElementById('content').textContent = result.content;
    }
  });
  
  // 显示通知
  const notifyBtn = document.getElementById('notify');
  notifyBtn.addEventListener('click', async () => {
    await window.electronAPI.showNotification('Hello from Electron!');
  });
});

2.3 数据存储与文件操作

桌面应用经常需要处理用户数据和文件。Electron 提供了多种存储选项。

localStorage

简单键值对存储

文件系统

Node.js fs 模块

数据库

SQLite 集成

配置文件管理示例

// main.js 中的配置管理
const { app, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

const configPath = path.join(app.getPath('userData'), 'config.json');

// 加载配置
ipcMain.handle('config:load', async () => {
  try {
    if (fs.existsSync(configPath)) {
      const data = fs.readFileSync(configPath, 'utf-8');
      return { success: true, config: JSON.parse(data) };
    }
    return { success: true, config: getDefaultConfig() };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// 保存配置
ipcMain.handle('config:save', async (event, config) => {
  try {
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

function getDefaultConfig() {
  return {
    theme: 'light',
    notifications: true,
    autoSave: false
  };
}

第三部分:高级特性与实践

3.1 应用程序菜单与系统集成

创建应用菜单

const { Menu, app } = require('electron');

const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click: async () => {
          // 创建新文档逻辑
        }
      },
      { type: 'separator' },
      {
        label: '打开',
        accelerator: 'CmdOrCtrl+O',
        click: async () => {
          // 打开文件逻辑
        }
      },
      {
        label: '保存',
        accelerator: 'CmdOrCtrl+S',
        click: async () => {
          // 保存文件逻辑
        }
      }
    ]
  },
  {
    label: '编辑',
    submenu: [
      { role: 'undo' },
      { role: 'redo' },
      { type: 'separator' },
      { role: 'cut' },
      { role: 'copy' },
      { role: 'paste' }
    ]
  },
  {
    label: '视图',
    submenu: [
      { role: 'reload' },
      { role: 'forceReload' },
      { role: 'toggleDevTools' },
      { type: 'separator' },
      { role: 'resetZoom' },
      { role: 'zoomIn' },
      { role: 'zoomOut' },
      { type: 'separator' },
      { role: 'togglefullscreen' }
    ]
  }
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

3.2 自动更新

使用 electron-updater 实现自动更新功能:

npm install electron-updater
// main.js 中的自动更新
const { autoUpdater } = require('electron-updater');

// 配置更新
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;

// 检查更新
app.whenReady().then(() => {
  createWindow();
  
  // 启动后检查更新
  setTimeout(() => {
    autoUpdater.checkForUpdatesAndNotify();
  }, 2000);
});

// 更新事件
autoUpdater.on('update-available', (info) => {
  dialog.showMessageBox({
    type: 'info',
    title: '发现新版本',
    message: `发现新版本 ${info.version},是否现在下载?`,
    buttons: ['下载', '取消']
  }).then(result => {
    if (result.response === 0) {
      autoUpdater.downloadUpdate();
    }
  });
});

autoUpdater.on('update-downloaded', () => {
  dialog.showMessageBox({
    type: 'info',
    title: '更新准备完成',
    message: '新版本已下载,应用将重启以安装更新。',
    buttons: ['立即重启', '稍后']
  }).then(result => {
    if (result.response === 0) {
      autoUpdater.quitAndInstall();
    }
  });
});

3.3 性能优化

❌ 性能陷阱

  • • 频繁的 IPC 通信
  • • 内存泄漏
  • • 阻塞主线程
  • • 过多的DOM操作

✅ 优化策略

  • • 批量处理 IPC 消息
  • • 及时清理事件监听器
  • • 使用 Web Workers
  • • 虚拟化长列表

内存优化示例

// 正确的事件监听器管理
class WindowManager {
  constructor() {
    this.windows = new Set();
    this.eventListeners = new Map();
  }

  createWindow() {
    const window = new BrowserWindow({...});
    this.windows.add(window);

    // 绑定清理函数
    const cleanup = () => {
      this.windows.delete(window);
      this.cleanupEventListeners(window);
    };

    window.on('closed', cleanup);
    this.eventListeners.set(window, cleanup);

    return window;
  }

  cleanupEventListeners(window) {
    const cleanup = this.eventListeners.get(window);
    if (cleanup) {
      cleanup();
      this.eventListeners.delete(window);
    }
  }

  closeAllWindows() {
    this.windows.forEach(window => {
      if (!window.isDestroyed()) {
        window.close();
      }
    });
  }
}

3.4 安全性最佳实践

🔐

安全黄金法则

永远不要将不可信的外部内容加载到开启了 Node.js 集成的窗口中!

安全配置清单

  • nodeIntegration: false
  • contextIsolation: true
  • enableRemoteModule: false
  • 使用预加载脚本暴露API
  • 设置内容安全策略(CSP)

第四部分:实战项目 - 待办事项应用

🎯 项目目标

我们将构建一个功能完整的待办事项应用,整合前面学到的所有知识:

  • 现代化的用户界面
  • 安全的进程间通信
  • 本地数据持久化
  • 应用打包与分发

项目结构

todo-electron-app/
├── src/
│   ├── main.js         # 主进程
│   ├── preload.js      # 预加载脚本
│   └── renderer/
│       ├── index.html  # 主界面
│       ├── style.css   # 样式文件
│       └── app.js      # 渲染进程逻辑
├── package.json
└── README.md

核心代码实现

main.js (主进程):

const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

let mainWindow;
const dataPath = path.join(app.getPath('userData'), 'todos.json');

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 900,
    height: 700,
    minWidth: 600,
    minHeight: 400,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js')
    },
    titleBarStyle: 'hiddenInset',
    show: false
  });

  mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));

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

// IPC 处理程序
ipcMain.handle('todos:load', async () => {
  try {
    if (fs.existsSync(dataPath)) {
      const data = fs.readFileSync(dataPath, 'utf-8');
      return JSON.parse(data);
    }
    return [];
  } catch (error) {
    console.error('加载待办事项失败:', error);
    return [];
  }
});

ipcMain.handle('todos:save', async (event, todos) => {
  try {
    fs.writeFileSync(dataPath, JSON.stringify(todos, null, 2));
    return { success: true };
  } catch (error) {
    console.error('保存待办事项失败:', error);
    return { success: false, error: error.message };
  }
});

app.whenReady().then(createWindow);

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

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

preload.js (安全桥梁):

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('todoAPI', {
  loadTodos: () => ipcRenderer.invoke('todos:load'),
  saveTodos: (todos) => ipcRenderer.invoke('todos:save', todos)
});

index.html (现代化界面):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo App</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="app">
        <header class="header">
            <h1>📝 我的待办事项</h1>
            <div class="stats" id="stats"></div>
        </header>

        <main class="main">
            <form class="add-form" id="addForm">
                <input 
                    type="text" 
                    id="todoInput" 
                    placeholder="添加新的待办事项..." 
                    required
                >
                <button type="submit">添加</button>
            </form>

            <div class="filter-tabs">
                <button class="filter-tab active" data-filter="all">全部</button>
                <button class="filter-tab" data-filter="active">未完成</button>
                <button class="filter-tab" data-filter="completed">已完成</button>
            </div>

            <ul class="todo-list" id="todoList"></ul>
        </main>
    </div>

    <script src="app.js"></script>
</body>
</html>

app.js (渲染进程逻辑):

class TodoApp {
  constructor() {
    this.todos = [];
    this.currentFilter = 'all';
    this.init();
  }

  async init() {
    this.bindEvents();
    await this.loadTodos();
    this.render();
  }

  bindEvents() {
    document.getElementById('addForm').addEventListener('submit', (e) => {
      e.preventDefault();
      this.addTodo();
    });

    document.querySelectorAll('.filter-tab').forEach(tab => {
      tab.addEventListener('click', (e) => {
        this.setFilter(e.target.dataset.filter);
      });
    });
  }

  async addTodo() {
    const input = document.getElementById('todoInput');
    const text = input.value.trim();
    
    if (text) {
      const todo = {
        id: Date.now(),
        text: text,
        completed: false,
        createdAt: new Date().toISOString()
      };
      
      this.todos.push(todo);
      input.value = '';
      await this.saveTodos();
      this.render();
    }
  }

  async toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) {
      todo.completed = !todo.completed;
      await this.saveTodos();
      this.render();
    }
  }

  async deleteTodo(id) {
    this.todos = this.todos.filter(t => t.id !== id);
    await this.saveTodos();
    this.render();
  }

  setFilter(filter) {
    this.currentFilter = filter;
    document.querySelectorAll('.filter-tab').forEach(tab => {
      tab.classList.toggle('active', tab.dataset.filter === filter);
    });
    this.render();
  }

  getFilteredTodos() {
    switch (this.currentFilter) {
      case 'active':
        return this.todos.filter(t => !t.completed);
      case 'completed':
        return this.todos.filter(t => t.completed);
      default:
        return this.todos;
    }
  }

  render() {
    const todoList = document.getElementById('todoList');
    const filteredTodos = this.getFilteredTodos();
    
    todoList.innerHTML = filteredTodos.map(todo => `
      <li class="todo-item ${todo.completed ? 'completed' : ''}">
        <input 
          type="checkbox" 
          ${todo.completed ? 'checked' : ''} 
          onchange="app.toggleTodo(${todo.id})"
        >
        <span class="todo-text">${todo.text}</span>
        <button class="delete-btn" onclick="app.deleteTodo(${todo.id})">
          🗑️
        </button>
      </li>
    `).join('');

    this.updateStats();
  }

  updateStats() {
    const total = this.todos.length;
    const completed = this.todos.filter(t => t.completed).length;
    const remaining = total - completed;
    
    document.getElementById('stats').textContent = 
      `总计: ${total} | 已完成: ${completed} | 剩余: ${remaining}`;
  }

  async loadTodos() {
    try {
      this.todos = await window.todoAPI.loadTodos();
    } catch (error) {
      console.error('加载待办事项失败:', error);
      this.todos = [];
    }
  }

  async saveTodos() {
    try {
      await window.todoAPI.saveTodos(this.todos);
    } catch (error) {
      console.error('保存待办事项失败:', error);
    }
  }
}

const app = new TodoApp();

应用打包

使用 electron-builder 进行应用打包:

# 安装打包工具
npm install --save-dev electron-builder

# 添加打包脚本到 package.json
npm run dist

package.json 配置示例:

{
  "name": "todo-electron-app",
  "version": "1.0.0",
  "main": "src/main.js",
  "scripts": {
    "start": "electron .",
    "dist": "electron-builder",
    "dist:win": "electron-builder --win",
    "dist:mac": "electron-builder --mac",
    "dist:linux": "electron-builder --linux"
  },
  "build": {
    "appId": "com.example.todoapp",
    "productName": "Todo应用",
    "directories": {
      "output": "dist"
    },
    "files": [
      "src/**/*",
      "package.json"
    ],
    "mac": {
      "icon": "assets/icon.icns",
      "category": "public.app-category.productivity"
    },
    "win": {
      "icon": "assets/icon.ico",
      "target": ["nsis", "portable"]
    },
    "linux": {
      "icon": "assets/icon.png",
      "target": ["AppImage", "deb"]
    }
  },
  "devDependencies": {
    "electron": "^latest",
    "electron-builder": "^latest"
  }
}

🎉 恭喜您!

您已经掌握了 Electron 桌面应用开发的核心技能!

✨ 已掌握技能

进程架构、IPC通信、数据存储

🛠️ 实践项目

完整的待办事项应用

🚀 下一步

探索更多高级特性

互动区域

登录后可以点赞此内容

参与互动

登录后可以点赞和评论此内容,与作者互动交流