{T}

第一个 Electron 应用

本文将带你从零开始创建一个完整的 Electron 应用,深入了解 Electron 应用的工作原理和最佳实践。

前置要求

在开始之前,确保你已经安装了以下工具:

  • Node.js: 建议使用 LTS 版本(18.x 或更高)
  • npmpnpm: 包管理器
  • 代码编辑器: 推荐 VS Code

💡 如果你还未配置开发环境,请先参考 简介与安装 完成环境配置

项目初始化

1. 创建项目结构

bash
# 创建项目目录
mkdir my-electron-app
cd my-electron-app

# 初始化 package.json
npm init -y

2. 安装 Electron

bash
npm install electron --save-dev

3. 配置 package.json

修改 package.json 文件,设置入口文件和启动脚本:

json
{
  "name": "my-electron-app",
  "version": "1.0.0",
  "description": "我的第一个 Electron 应用",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "dev": "electron . --enable-logging"
  },
  "keywords": ["electron", "desktop", "app"],
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "electron": "^28.0.0"
  }
}

关键配置说明:

  • main: 指定 Electron 应用的主进程入口文件
  • scripts.start: 启动应用的快捷命令
  • electron 作为 devDependencies: 打包后的应用会包含 Electron 二进制文件,运行时不需要单独依赖

核心文件编写

1. 主进程文件 (main.js)

主进程是 Electron 应用的核心,负责创建窗口、管理应用生命周期、处理系统交互。

javascript
// main.js
const { app, BrowserWindow } = require('electron')
const path = require('path')

/**
 * 创建浏览器窗口
 * BrowserWindow 是 Electron 提供的窗口管理类
 */
function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 800,              // 窗口宽度
    height: 600,             // 窗口高度
    webPreferences: {
      // 预加载脚本路径
      // path.join 使用跨平台路径拼接,避免 Windows/macOS 路径分隔符差异
      preload: path.join(__dirname, 'preload.js'),
      
      // 启用上下文隔离(安全最佳实践,Electron 12+ 默认启用)
      contextIsolation: true,
      
      // 禁用 Node.js 集成(安全最佳实践,推荐禁用)
      nodeIntegration: false,
      
      // 启用沙箱模式(增强安全性)
      sandbox: true
    }
  })

  // 加载应用的 HTML 文件
  mainWindow.loadFile('index.html')

  // 开发环境自动打开开发者工具
  if (process.env.NODE_ENV === 'development') {
    mainWindow.webContents.openDevTools()
  }
  
  // 窗口关闭时的事件处理
  mainWindow.on('closed', () => {
    // 解除窗口引用,帮助垃圾回收
    mainWindow = null
  })
}

// Electron 初始化完成后创建窗口
// app.whenReady() 返回一个 Promise,确保 app 完全就绪
app.whenReady().then(() => {
  createWindow()

  // macOS 特殊处理
  // 在 macOS 上,当点击 dock 图标且没有其他窗口打开时,
  // 通常会在应用程序中重新创建一个窗口
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

// 关闭所有窗口时退出应用(macOS 除外)
// macOS 应用通常在用户明确退出前保持活动状态
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// 在应用退出前清理资源
app.on('before-quit', () => {
  console.log('应用即将退出')
})

2. 预加载脚本 (preload.js)

预加载脚本运行在渲染进程中,但拥有访问 Node.js API 的能力,是连接主进程和渲染进程的安全桥梁。

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

/**
 * 使用 contextBridge 安全地将 API 暴露给渲染进程
 * contextIsolation: true 时必须使用此方法
 * 
 * 优势:
 * 1. 防止原型污染攻击
 * 2. 确保只有经过验证的 API 可以在渲染进程中使用
 * 3. 提供类型安全的接口
 */
contextBridge.exposeInMainWorld('electronAPI', {
  // 获取版本信息
  versions: {
    node: () => process.versions.node,
    chrome: () => process.versions.chrome,
    electron: () => process.versions.electron
  },
  
  // IPC 通信示例:发送消息到主进程
  send: (channel, data) => {
    // 白名单验证,防止任意 IPC 调用
    const validChannels = ['toMain']
    if (validChannels.includes(channel)) {
      ipcRenderer.send(channel, data)
    }
  },
  
  // IPC 通信示例:接收主进程消息
  receive: (channel, func) => {
    const validChannels = ['fromMain']
    if (validChannels.includes(channel)) {
      // 移除旧的监听器,避免内存泄漏
      ipcRenderer.removeAllListeners(channel)
      ipcRenderer.on(channel, (event, ...args) => func(...args))
    }
  },
  
  // IPC 通信示例:双向通信
  invoke: async (channel, data) => {
    const validChannels = ['dialog:open', 'file:read']
    if (validChannels.includes(channel)) {
      return await ipcRenderer.invoke(channel, data)
    }
    throw new Error(`Invalid IPC channel: ${channel}`)
  }
})

3. HTML 页面 (index.html)

渲染进程的 HTML 页面,用于展示应用界面。

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  
  <!-- Content Security Policy: 防止 XSS 攻击 -->
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
  
  <title>我的第一个 Electron 应用</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 20px;
    }
    
    h1 {
      font-size: 3rem;
      margin-bottom: 20px;
      text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
    }
    
    .info {
      background: rgba(255, 255, 255, 0.1);
      padding: 20px 30px;
      border-radius: 10px;
      backdrop-filter: blur(10px);
      margin-top: 20px;
    }
    
    .info p {
      margin: 10px 0;
      font-size: 1.1rem;
    }
    
    .version {
      color: #ffd700;
      font-weight: bold;
    }
  </style>
</head>
<body>
  <h1>🎉 Hello Electron!</h1>
  <p>欢迎使用 Electron 桌面应用开发</p>
  
  <div class="info">
    <p>Node.js 版本: <span class="version" id="node-version"></span></p>
    <p>Chromium 版本: <span class="version" id="chrome-version"></span></p>
    <p>Electron 版本: <span class="version" id="electron-version"></span></p>
  </div>

  <!-- 引入渲染进程脚本 -->
  <script src="./renderer.js"></script>
</body>
</html>

4. 渲染进程脚本 (renderer.js)

在渲染进程中运行的 JavaScript 代码,处理页面交互和显示逻辑。

javascript
// renderer.js

/**
 * 使用 preload.js 暴露的 electronAPI
 * window.electronAPI 是通过 contextBridge 注入的安全接口
 */
function displayVersions() {
  try {
    // 获取并显示版本信息
    const nodeVersion = window.electronAPI.versions.node()
    const chromeVersion = window.electronAPI.versions.chrome()
    const electronVersion = window.electronAPI.versions.electron()
    
    document.getElementById('node-version').innerText = nodeVersion
    document.getElementById('chrome-version').innerText = chromeVersion
    document.getElementById('electron-version').innerText = electronVersion
    
    console.log('版本信息加载成功')
  } catch (error) {
    console.error('获取版本信息失败:', error)
  }
}

// DOM 加载完成后执行
window.addEventListener('DOMContentLoaded', () => {
  displayVersions()
  
  console.log('渲染进程已就绪')
})

运行应用

启动开发环境

bash
npm start

或使用开发模式(带日志输出):

bash
npm run dev

启动后,你应该能看到一个窗口显示版本信息。

项目结构说明

完成后的项目结构如下:

code
my-electron-app/
├── package.json        # 项目配置和依赖管理
├── main.js             # 主进程入口文件
├── preload.js          # 预加载脚本(安全桥梁)
├── index.html          # 渲染进程 HTML 页面
├── renderer.js         # 渲染进程 JavaScript
├── .gitignore          # Git 忽略文件配置
└── node_modules/       # 项目依赖(自动生成)

文件职责

文件进程类型主要职责
main.js主进程创建窗口、管理应用生命周期、系统交互
preload.js渲染进程安全暴露 Node.js API 给渲染进程
index.html渲染进程应用界面结构
renderer.js渲染进程页面交互逻辑、DOM 操作

调试方法

1. 使用 Chrome DevTools

Electron 内置了 Chrome DevTools,方便调试渲染进程:

方法一:代码自动打开

javascript
// main.js
mainWindow.webContents.openDevTools()

方法二:快捷键

  • macOS: Cmd + Option + I
  • Windows/Linux: F12Ctrl + Shift + I

方法三:菜单栏

在应用菜单中选择 ViewToggle Developer Tools

2. 主进程调试

方法一:VS Code 调试配置

在项目根目录创建 .vscode/launch.json

json
{
  "version": "0.2.0",
  "compounds": [
    {
      "name": "Electron: All",
      "configurations": ["Electron: Main", "Electron: Renderer"],
      "stopAll": true
    }
  ],
  "configurations": [
    {
      "name": "Electron: Main",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args": ["."],
      "outputCapture": "std",
      "console": "integratedTerminal"
    },
    {
      "name": "Electron: Renderer",
      "port": 9222,
      "request": "attach",
      "type": "chrome",
      "webRoot": "${workspaceFolder}",
      "timeout": 30000
    }
  ]
}

使用方法:

  1. F5 或点击 VS Code 调试面板的"运行"按钮
  2. 选择 Electron: All 同时调试主进程和渲染进程
  3. 在代码中设置断点即可

方法二:命令行调试

bash
# 启用 Node.js 调试模式
electron --inspect=5858 .
electron --inspect-brk=5858 .  # 在第一行代码处暂停

然后在 Chrome 浏览器打开 chrome://inspect 连接调试。

3. 日志调试

javascript
// main.js - 主进程日志
console.log('主进程日志:', data)

// renderer.js - 渲染进程日志
console.log('渲染进程日志:', data)

// 查看 IPC 通信
ipcMain.on('channel', (event, data) => {
  console.log('收到 IPC 消息:', data)
})

查看日志位置:

  • 主进程日志: 终端输出
  • 渲染进程日志: Chrome DevTools Console

常见错误与解决方案

1. 模块未找到错误

错误信息:

code
Error: Cannot find module 'electron'

解决方案:

bash
# 确认 electron 已安装
npm ls electron

# 重新安装
npm install electron --save-dev

2. 窗口无法显示

可能原因及解决方案:

javascript
// 检查 1: 确认 HTML 文件路径正确
mainWindow.loadFile('index.html')  // 确保文件存在

// 检查 2: 捕获加载错误
mainWindow.loadFile('index.html').catch(err => {
  console.error('页面加载失败:', err)
})

// 检查 3: 监听加载事件
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
  console.error('加载失败:', errorCode, errorDescription)
})

3. contextBridge 报错

错误信息:

code
contextBridge is not defined

解决方案:

确保在 webPreferences 中正确配置:

javascript
const mainWindow = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,  // 必须启用
    nodeIntegration: false   // 必须禁用
  }
})

4. IPC 通信失败

错误信息:

code
Error: An object could not be cloned

原因: IPC 消息只能传递可序列化的数据(JSON 支持的类型)

解决方案:

javascript
// ❌ 错误:传递函数或 DOM 元素
ipcRenderer.send('channel', { callback: () => {} })

// ✅ 正确:只传递可序列化数据
ipcRenderer.send('channel', { 
  id: 1,
  name: 'test',
  data: { key: 'value' }
})

5. 空白页面问题

可能原因:

  1. CSP 策略限制:
html
<!-- 检查 CSP 设置 -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  1. 文件路径错误:
javascript
// 使用 path.join 确保路径正确
mainWindow.loadFile(path.join(__dirname, 'index.html'))
  1. JavaScript 错误: 打开 DevTools 查看控制台是否有错误信息。

6. macOS 窗口关闭后应用不退出

这是 macOS 的正常行为,需添加处理:

javascript
app.on('window-all-closed', () => {
  // macOS 应用通常在用户明确退出前保持活动状态
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

最佳实践

1. 安全最佳实践

javascript
// ✅ 推荐
const mainWindow = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,      // 启用上下文隔离
    nodeIntegration: false,      // 禁用 Node.js 集成
    sandbox: true,               // 启用沙箱
    preload: path.join(__dirname, 'preload.js')
  }
})

// ❌ 不安全(仅用于开发测试)
const mainWindow = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,       // 危险!
    contextIsolation: false      // 危险!
  }
})

2. 窗口管理

javascript
// 存储窗口引用,避免被垃圾回收
let mainWindow = null

function createWindow() {
  mainWindow = new BrowserWindow({...})
  
  mainWindow.on('closed', () => {
    mainWindow = null  // 解除引用
  })
}

// 多窗口管理
const windows = new Set()

function createWindow() {
  const win = new BrowserWindow({...})
  windows.add(win)
  win.on('closed', () => windows.delete(win))
}

3. 环境变量管理

javascript
// 使用 dotenv 管理环境变量
require('dotenv').config()

const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'

if (isDev) {
  mainWindow.webContents.openDevTools()
}

4. 错误处理

javascript
// 全局错误捕获
process.on('uncaughtException', (error) => {
  console.error('未捕获的异常:', error)
  // 可以在这里添加错误上报
})

process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的 Promise 拒绝:', reason)
})

// 窗口加载错误
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
  console.error('页面加载失败:', errorCode, errorDescription)
})

5. 性能优化

javascript
// 延迟加载模块
let heavyModule = null

async function loadHeavyModule() {
  if (!heavyModule) {
    heavyModule = await import('./heavy-module.js')
  }
  return heavyModule
}

// 节流和防抖
const { debounce } = require('lodash')

const debouncedSave = debounce((data) => {
  // 保存数据
}, 300)

// 及时清理资源
mainWindow.on('closed', () => {
  mainWindow.webContents.session.clearCache()
  mainWindow = null
})

6. 代码组织

推荐的项目结构:

code
my-electron-app/
├── src/
│   ├── main/              # 主进程代码
│   │   ├── index.js       # 入口文件
│   │   ├── window.js      # 窗口管理
│   │   └── ipc.js         # IPC 通信
│   ├── renderer/          # 渲染进程代码
│   │   ├── index.html
│   │   ├── renderer.js
│   │   └── styles/
│   └── preload/           # 预加载脚本
│       └── index.js
├── resources/             # 应用资源
│   ├── icon.png
│   └── tray-icon.png
├── tests/                 # 测试文件
├── package.json
└── README.md

下一步学习

  • 项目结构说明 - 了解 Electron 项目的标准结构和文件组织方式
  • 核心概念 - 深入学习主进程、渲染进程、IPC 通信等核心概念
  • API 参考 - 探索更多 Electron API 功能
  • 打包发布 - 学习如何打包和分发你的应用

相关资源