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

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

欢迎来到 Electron 桌面应用开发的世界!本教程将带您从零开始,使用您熟悉的 Web 技术(HTML、CSS、JavaScript)构建功能强大的跨平台桌面应用程序。

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

第一部分:Electron 入门基础

1.1 Electron 简介与核心概念

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

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

核心概念:主进程 (Main Process) 与渲染进程 (Renderer Process)

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

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

Node.js 集成能力

Electron 的强大之处在于它将 Node.js 的能力带到了桌面应用程序中。这意味着您可以在主进程中使用所有 Node.js 模块(例如 fs 模块进行文件操作,http 模块进行网络请求等),这极大地扩展了 Web 技术的边界,让您的应用能够做更多原生应用才能做的事情。

1.2 开发环境搭建

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

步骤 1:安装 Node.js & npm/yarn

Electron 基于 Node.js 运行,并使用 npm (Node Package Manager) 或 yarn 进行包管理。如果您尚未安装,请前往 Node.js 官网 下载并安装 LTS (长期支持) 版本。安装完成后,您可以通过命令行检查版本:


node -v
npm -v
# 或者如果您使用 yarn
yarn -v
            

您应该会看到类似 `v18.x.x` 和 `9.x.x` 的版本号。

步骤 2:初始化 Electron 项目

创建一个新的文件夹作为您的项目目录,然后进入该目录,使用 npm 或 yarn 初始化一个新的 Node.js 项目:


mkdir my-electron-app
cd my-electron-app
npm init -y
# 或者
yarn init -y
            

这会在您的项目根目录创建一个 `package.json` 文件,它是 Node.js 项目的配置文件。

步骤 3:安装 Electron

接下来,安装 Electron 作为您项目的开发依赖:


npm install --save-dev electron
# 或者
yarn add --dev electron
            

安装完成后,`package.json` 文件中会多出 `electron` 这一项。

步骤 4:创建第一个 Electron 应用

一个最基本的 Electron 应用通常需要三个文件:

  1. `main.js` (主进程脚本): 负责创建窗口和处理应用生命周期。
  2. `index.html` (渲染进程界面): 您应用程序的界面。
  3. `package.json`: 项目配置文件,包含启动命令。

`main.js`:


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

function createWindow () {
  // 创建浏览器窗口
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // 强烈建议禁用 Node.js 集成,并通过预加载脚本安全地暴露API
      nodeIntegration: false,
      // 开启上下文隔离,这是更安全的做法,防止恶意脚本访问Electron内部API
      contextIsolation: true,
      preload: path.join(__dirname, 'preload.js') // 预加载脚本
    }
  });

  // 加载 index.html 文件
  mainWindow.loadFile('index.html');

  // 打开开发者工具 (可选,方便调试)
  // mainWindow.webContents.openDevTools();
}

// 当 Electron 应用准备就绪时,调用 createWindow
app.whenReady().then(() => {
  createWindow();

  // macOS 特定:当所有窗口都关闭时,如果用户没有显式退出,应用会保持活跃状态
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

// 当所有窗口都关闭时退出应用
app.on('window-all-closed', () => {
  // 在 macOS 上,除非用户显式用 Cmd + Q 退出,否则应用通常保持活跃
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
            

`index.html`:





    
    我的第一个 Electron 应用
    
    
    


    

Hello, Electron World! 👋

`preload.js` (安全桥梁): 这是一个在主进程和渲染进程之间,但在渲染进程的 Web 内容加载之前运行的脚本。它是连接 Node.js API 和渲染进程的安全桥梁。


// preload.js (暂时留空,后续会详细讲解)
// const { contextBridge, ipcRenderer } = require('electron');
// contextBridge.exposeInMainWorld('electronAPI', {
//   // 这里可以暴露主进程提供的安全API
// });
            

`renderer.js` (渲染进程脚本): 暂时留空,或者放入一些简单的JavaScript来验证HTML加载成功。


// renderer.js
console.log('Electron 渲染进程已加载!');
            

修改 `package.json`:

在 `scripts` 部分添加一个 `start` 命令,指向您的主进程文件:


{
  "name": "my-electron-app",
  "version": "1.0.0",
  "description": "My first Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "electron": "^28.0.0"
  }
}
            

确保将 `"main": "main.js"` 设置为主进程的入口文件。

步骤 5:运行第一个 Electron 应用

现在,您可以在命令行中运行您的应用了:


npm start
# 或者
yarn start
            

如果一切顺利,您应该会看到一个标题为"我的第一个 Electron 应用"的窗口弹出,里面显示着"Hello, Electron World! 👋"。恭喜您,您的第一个 Electron 应用成功运行了!

graph TD A[用户启动 Electron 应用] --> B{Node.js 环境 (主进程)} B --创建--> C[BrowserWindow 实例] C --加载HTML--> D[Chromium 渲染引擎 (渲染进程)] D --显示界面--> E[用户界面 (index.html)] B --系统交互--> F[文件系统/网络/原生菜单等] D --IPC通信 (通过Preload安全桥接)--> B B --返回结果--> D

上图清晰地展示了 Electron 应用中主进程与渲染进程的关系以及它们如何协同工作。预加载脚本作为中间层,确保了安全通信。

第二部分:Electron 核心开发

2.1 主进程开发

主进程是 Electron 应用的"大脑",它拥有访问操作系统底层 API 的能力。以下是主进程中常用的模块和功能:

2.1.1 `BrowserWindow`:创建与管理窗口

BrowserWindow 是 Electron 中用于创建新窗口的类。您可以通过它来定义窗口的大小、位置、是否可调整大小、是否带边框等属性。


// main.js (片段)
const { BrowserWindow } = require('electron');

function createWindow () {
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    minWidth: 400, // 最小宽度
    minHeight: 300, // 最小高度
    frame: true, // 是否显示系统边框 (true为显示,false为无边框窗口)
    transparent: false, // 是否透明 (需要 frame: false 配合)
    webPreferences: {
      nodeIntegration: false, // 禁用 Node.js API
      contextIsolation: true, // 开启上下文隔离
      preload: path.join(__dirname, 'preload.js')
    }
  });

  // 加载本地 HTML 文件
  mainWindow.loadFile('index.html');

  // 也可以加载远程 URL,用于 Web 应用的桌面化
  // mainWindow.loadURL('https://www.electronjs.org');

  // 监听窗口关闭事件
  mainWindow.on('closed', () => {
    // 销毁窗口引用,防止内存泄漏。
    // 在 Electron 20+ 中,当窗口关闭时,其引用通常会自动垃圾回收,
    // 但如果您的应用有多个窗口且需要精确管理,仍可手动置空。
  });
}
            

2.1.2 应用程序生命周期管理 (`app` 模块)

app 模块控制着 Electron 应用程序的事件生命周期。您已经看到过它的使用,比如 `app.whenReady()` 和 `app.on('window-all-closed')`。

2.1.3 菜单 (Menu) 与系统托盘 (Tray)

Electron 允许您创建自定义的应用程序菜单和系统托盘图标。


// main.js (片段)
const { app, BrowserWindow, Menu, Tray, dialog } = require('electron');
const path = require('path');

let tray = null; // 声明一个全局变量来保持 Tray 实例的引用

// 创建系统托盘图标
function createTray() {
  // __dirname 指向当前文件目录,确保 'icon.png' 存在于此
  tray = new Tray(path.join(__dirname, 'assets', 'icon.png')); // 建议将图标放在 assets 文件夹
  const contextMenu = Menu.buildFromTemplate([
    { label: '打开应用', click: () => {
      if (BrowserWindow.getAllWindows().length === 0) {
        createWindow(); // 如果没有窗口,则创建一个新窗口
      } else {
        // 如果有窗口,可以尝试聚焦或显示主窗口
        BrowserWindow.getAllWindows()[0].show();
      }
    }},
    { type: 'separator' }, // 分隔线
    { label: '退出', click: () => app.quit() }
  ]);
  tray.setToolTip('我的 Electron 应用'); // 鼠标悬停时的提示
  tray.setContextMenu(contextMenu); // 设置右键菜单
}

app.whenReady().then(() => {
  createWindow();
  // createTray(); // 在这里调用以创建系统托盘

  // 创建应用菜单 (顶部菜单栏)
  const template = [
    {
      label: '文件',
      submenu: [
        { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => { /* 新建逻辑 */ } },
        { type: 'separator' }, // 分隔线
        { label: '退出', role: 'quit' } // 'quit' 是一个预定义的角色,可以直接退出应用
      ]
    },
    {
      label: '编辑',
      role: 'editMenu' // 使用预定义角色自动填充标准编辑菜单
    },
    {
      label: '视图',
      role: 'viewMenu' // 使用预定义角色自动填充标准视图菜单
    },
    {
      label: '帮助',
      submenu: [
        { label: '关于', click: () => dialog.showMessageBox({
          title: '关于我的应用',
          message: `版本:${app.getVersion()}\n作者:My Awesome App`
        })}
      ]
    }
  ];

  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu); // 设置应用程序菜单
});
            

注意: `Tray` 图标文件通常是 `.png` 或 `.ico` 格式,需要放在项目目录下,并确保路径正确,例如创建一个 `assets` 文件夹存放。

2.1.4 原生对话框 (Dialog)

dialog 模块提供 API 用于显示原生操作系统的文件打开/保存对话框、消息框等。


// main.js (片段)
const { dialog } = require('electron');
const fs = require('fs'); // Node.js 文件系统模块

// 示例:显示一个消息框
async function showCustomMessageBox() {
  const result = await dialog.showMessageBox({
    type: 'info',
    title: '我的应用',
    message: '这是一个来自主进程的消息!',
    detail: '您可以在这里提供更多详情信息。',
    buttons: ['确定', '取消']
  });
  console.log(`用户点击了按钮索引: ${result.response}`); // 0 for '确定', 1 for '取消'
  return result.response;
}

// 示例:打开文件对话框并读取文件内容
async function openAndReadFile() {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    properties: ['openFile'], // 允许选择文件
    filters: [
      { name: '文本文件', extensions: ['txt', 'md'] },
      { name: '所有文件', extensions: ['*'] }
    ]
  });

  if (!canceled && filePaths.length > 0) {
    const filePath = filePaths[0];
    try {
      const content = fs.readFileSync(filePath, 'utf-8');
      console.log('文件内容:', content);
      return { success: true, content: content };
    } catch (err) {
      console.error('读取文件失败:', err);
      dialog.showErrorBox('文件读取错误', `无法读取文件: ${err.message}`);
      return { success: false, error: err.message };
    }
  }
  return { success: false, cancelled: true };
}

// 这些函数通常会通过 IPC 暴露给渲染进程调用
// 例如:ipcMain.handle('show-msg-box', showCustomMessageBox);
// 例如:ipcMain.handle('open-and-read-file', openAndReadFile);
            

2.1.5 文件系统操作 (fs 模块)

由于主进程是 Node.js 环境,您可以直接使用 Node.js 的核心模块,比如 fs (File System) 来进行文件和目录的操作。


// main.js (片段)
const fs = require('fs');
const path = require('path');
const { app } = require('electron'); // 获取 app 模块来获取路径

// 获取应用程序的用户数据目录
const userDataPath = app.getPath('userData');
const demoFilePath = path.join(userDataPath, 'demo.txt');

// 写入文件 (异步)
function writeDemoFile(content) {
  fs.writeFile(demoFilePath, content, 'utf-8', (err) => {
    if (err) {
      console.error('写入文件失败:', err);
    } else {
      console.log(`${demoFilePath} 写入成功!`);
    }
  });
}

// 读取文件 (同步)
function readDemoFile() {
  try {
    if (fs.existsSync(demoFilePath)) {
      const content = fs.readFileSync(demoFilePath, 'utf-8');
      console.log(`${demoFilePath} 内容:`, content);
      return content;
    } else {
      console.log(`${demoFilePath} 不存在。`);
      return null;
    }
  } catch (err) {
    console.error('读取文件失败:', err);
    return null;
  }
}

// 使用示例:
// app.whenReady().then(() => {
//   writeDemoFile('Hello from Electron!');
//   setTimeout(() => { // 稍后读取,确保写入完成
//     readDemoFile();
//   }, 1000);
// });
            

2.2 渲染进程开发

渲染进程是您的应用程序界面所在的地方,它本质上就是一个 Chrome 浏览器页面。您可以使用您熟悉的所有 Web 技术来构建界面。

2.2.1 Web 技术栈(HTML/CSS/JavaScript)应用

一切如常。您在浏览器中开发的经验可以直接复制到 Electron 渲染进程中。例如,创建一个计数器应用:

`index.html`:





    
    计数器应用
    
    


    

简单的计数器

0

`renderer.js`: (放在与 `index.html` 同级目录)


// renderer.js
let count = 0;
const counterElement = document.getElementById('counter');
const increaseButton = document.getElementById('increase');
const decreaseButton = document.getElementById('decrease');

increaseButton.addEventListener('click', () => {
    count++;
    counterElement.textContent = count;
});

decreaseButton.addEventListener('click', () => {
    count--;
    counterElement.textContent = count;
});
            

2.2.2 与主进程的通信 (IPC)

这是 Electron 开发中非常重要的一环。由于渲染进程不能直接访问 Node.js API,它需要通过 IPC (Inter-Process Communication) 机制与主进程进行通信。Electron 提供了 `ipcMain` (主进程) 和 `ipcRenderer` (渲染进程) 模块来实现这一点。

安全警告: 在 Electron 12 之后,默认启用了 `contextIsolation: true` 和 `nodeIntegration: false`,这是为了安全。这意味着渲染进程无法直接访问 `require` 或 Electron API。为了安全地与主进程通信,我们强烈推荐使用预加载脚本 (Preload Script)

主进程发送消息给渲染进程

主进程可以通过特定窗口的 `webContents.send()` 方法向其渲染进程发送消息。

`main.js` (片段):


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

let mainWindow;

function createWindow () {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'), // 引入预加载脚本
      nodeIntegration: false, // 禁用 Node.js 集成,更安全
      contextIsolation: true // 开启上下文隔离,更安全
    }
  });

  mainWindow.loadFile('index.html');
  // mainWindow.webContents.openDevTools();

  // 假设在某个事件后,主进程想给渲染进程发消息
  setTimeout(() => {
    if (mainWindow && !mainWindow.isDestroyed()) {
      mainWindow.webContents.send('main-process-message', '你好,我是主进程!');
    }
  }, 3000); // 3秒后发送消息
}

app.whenReady().then(createWindow);

// ... 其他生命周期事件 ...
            
渲染进程发送消息给主进程 (单向和双向)

渲染进程通过 `ipcRenderer` 模块发送消息。注意: 由于 `contextIsolation: true`,我们不能直接在渲染进程中使用 `require('electron')`。我们需要通过预加载脚本来"暴露"所需的 API。

`preload.js`:


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

contextBridge.exposeInMainWorld('electronAPI', {
  // 渲染进程向主进程发送消息(单向)
  sendMessageToMain: (message) => ipcRenderer.send('renderer-message', message),
  // 渲染进程向主进程发送消息并等待回复(双向)
  invokeMainProcess: (channel, args) => ipcRenderer.invoke(channel, args),
  // 监听主进程发来的消息
  onMainProcessMessage: (callback) => ipcRenderer.on('main-process-message', (_event, value) => callback(value))
});
            

`main.js` (接收渲染进程消息):


// main.js (片段)
const { app, BrowserWindow, ipcMain } = require('electron');

// ... createWindow 函数 ...

app.whenReady().then(createWindow);

// 监听渲染进程发送的单向消息
ipcMain.on('renderer-message', (event, message) => {
  console.log('收到渲染进程消息:', message); // "你好,我是渲染进程!"
  // 这是一个单向通信的例子,通常不直接回复
});

// 监听渲染进程发送的双向消息 (invoke/handle)
ipcMain.handle('get-app-version', async (event) => {
  const version = app.getVersion();
  console.log('渲染进程请求应用版本:', version);
  return version; // 返回数据给渲染进程
});

// ... 其他生命周期事件 ...
            

`renderer.js` (发送和接收消息):


// renderer.js
// 通过 window.electronAPI 访问预加载脚本暴露的 API
document.addEventListener('DOMContentLoaded', () => {
    const messageButton = document.createElement('button');
    messageButton.textContent = '向主进程发送消息';
    document.body.appendChild(messageButton);

    messageButton.addEventListener('click', () => {
        // 单向发送
        window.electronAPI.sendMessageToMain('你好,我是渲染进程!');
    });

    const versionButton = document.createElement('button');
    versionButton.textContent = '获取应用版本';
    document.body.appendChild(versionButton);

    versionButton.addEventListener('click', async () => {
        // 双向通信:请求并等待回复
        const version = await window.electronAPI.invokeMainProcess('get-app-version');
        alert(`应用版本:${version}`);
    });

    // 监听主进程发来的消息
    window.electronAPI.onMainProcessMessage((message) => {
        const p = document.createElement('p');
        p.textContent = `主进程说:${message}`;
        document.body.appendChild(p);
    });
});
            

2.3 应用程序打包与分发

开发完成后,您需要将应用打包成可执行文件,以便在不同操作系统上分发给用户。最流行的工具是 `electron-builder`。

2.3.1 使用 `electron-builder`

首先,安装 `electron-builder` 作为开发依赖:


npm install --save-dev electron-builder
# 或者
yarn add --dev electron-builder
            

然后在 `package.json` 中添加配置和打包脚本:


{
  "name": "my-electron-app",
  "version": "1.0.0",
  "description": "My first Electron application",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "dist": "electron-builder",
    "postinstall": "electron-builder install-app-deps"
  },
  "author": "Your Name",
  "license": "ISC",
  "devDependencies": {
    "electron": "^28.0.0",
    "electron-builder": "^24.0.0"
  },
  "build": {
    "appId": "com.yourcompany.yourappname",
    "productName": "我的Electron应用",
    "copyright": "Copyright © 2024 ${author}",
    "directories": {
      "output": "dist"
    },
    "files": [
      "**/*"
    ],
    "win": {
      "target": "nsis",
      "icon": "assets/icon.ico"
    },
    "mac": {
      "target": "dmg",
      "icon": "assets/icon.icns"
    },
    "linux": {
      "target": "AppImage",
      "icon": "assets/icon.png"
    }
  }
}
            

最后,运行打包命令:


npm run dist
# 或者
yarn dist
            

这将在项目的 `dist` 目录下生成针对不同操作系统的安装包或可执行文件。

2.3.2 代码签名 (Code Signing) 简介

代码签名是为您的应用程序"盖章"的过程,以证明它来自可信的发布者,并且自发布以来没有被篡改。这对于桌面应用程序的安全性至关重要,特别是 macOS 和 Windows。没有签名的应用在这些系统上可能会被标记为"不受信任"或"来自未知开发者",从而阻止用户安装。

代码签名配置通常在 `electron-builder` 的 `build` 配置中完成,涉及到证书路径和密码等敏感信息。对于初学者来说,这一步可以暂时跳过,但在发布正式产品时是必不可少的。

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

3.1 进程间通信 (IPC) 深度解析

我们之前已经初步了解了 IPC,现在深入一下其安全性和最佳实践。

3.1.1 同步与异步 IPC

3.1.2 使用上下文隔离 (Context Isolation) 增强安全性

默认情况下,在 Electron 12+ 中,`contextIsolation` 属性在 `webPreferences` 中是 `true`。这意味着:

graph TD A[渲染进程] --> B{Web Context} B --无法直接访问--> C{Node.js/Electron API Context} C --通过预加载脚本暴露--> D[window.electronAPI 对象] D --安全地暴露API--> B

上图展示了上下文隔离如何将渲染进程的 Web 上下文与 Node.js 上下文隔离开来,从而防止恶意脚本直接访问 Node.js API。通过预加载脚本安全地暴露有限的 API 是最佳实践。

为什么这很重要? 如果 `nodeIntegration` 开启且 `contextIsolation` 关闭,渲染进程中的任何外部 JavaScript 代码(例如来自 CDN 的脚本)都可能获得完整的 Node.js 访问权限,这可能导致严重的安全漏洞。

3.1.3 预加载脚本 (Preload Script) 的作用与最佳实践

预加载脚本是解决 `nodeIntegration: false` 和 `contextIsolation: true` 情况下渲染进程无法直接访问 Node.js API 的关键。

它的作用:

最佳实践:

回忆: 我们在 2.2.2 节中已经展示了 `preload.js` 的基本用法。现在您应该明白为什么那样做是安全的。

3.2 本地数据存储

桌面应用通常需要存储用户数据、配置等。Electron 提供了多种存储选项:

3.2.1 使用 `localStorage` 或 `IndexedDB`

如果您只需要在渲染进程中存储少量非敏感数据,可以直接使用浏览器原生的 `localStorage` 或 `IndexedDB`。

`localStorage` (简单键值对存储):


// 渲染进程 (renderer.js)
// 存储数据
localStorage.setItem('username', 'Alice');
// 读取数据
const username = localStorage.getItem('username');
console.log('用户名:', username);
            

`IndexedDB` (更复杂、异步的结构化数据存储): 适用于需要存储大量结构化数据或离线访问数据的场景。使用方式与 Web 开发中无异。

3.2.2 使用 Node.js `fs` 模块存储文件

对于需要直接读写文件系统的数据(如配置文件、用户文档等),应在主进程中使用 Node.js 的 `fs` 模块,并通过 IPC 与渲染进程交互。这通常是更灵活和强大的方式。


// main.js (片段) - 写入和读取 JSON 配置
const fs = require('fs');
const path = require('path');
const { app, ipcMain } = require('electron');

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

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

ipcMain.handle('load-config', async (event) => {
  try {
    if (fs.existsSync(configPath)) {
      const data = fs.readFileSync(configPath, 'utf-8');
      return { success: true, config: JSON.parse(data) };
    }
    return { success: true, config: {} }; // 如果文件不存在,返回空对象
  } catch (error) {
    console.error('加载配置失败:', error);
    return { success: false, error: error.message };
  }
});
            

`app.getPath('userData')`: 这是一个非常重要的 Electron API,它返回应用程序的用户数据目录路径。这是存储用户特定文件(如配置文件、数据库)的最佳位置,因为它在不同操作系统上都保持一致且通常具有写入权限。

`preload.js` (片段) - 暴露 API:


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

contextBridge.exposeInMainWorld('configAPI', {
  save: (config) => ipcRenderer.invoke('save-config', config),
  load: () => ipcRenderer.invoke('load-config')
});
            

`renderer.js` (片段) - 使用 API:


// renderer.js (片段)
document.addEventListener('DOMContentLoaded', async () => {
    // 假设有一个按钮用于加载配置
    const loadButton = document.createElement('button');
    loadButton.textContent = '加载配置';
    document.body.appendChild(loadButton);

    loadButton.addEventListener('click', async () => {
        const { success, config, error } = await window.configAPI.load();
        if (success) {
            console.log('加载到的配置:', config);
            alert('加载到的配置: ' + JSON.stringify(config));
        } else {
            console.error('加载配置失败:', error);
            alert('加载配置失败: ' + error);
        }
    });

    // 假设有一个按钮用于保存配置
    const saveButton = document.createElement('button');
    saveButton.textContent = '保存配置';
    document.body.appendChild(saveButton);

    saveButton.addEventListener('click', async () => {
        const newConfig = { theme: 'dark', notifications: true };
        const saveResult = await window.configAPI.save(newConfig);
        if (saveResult.success) {
            console.log('配置保存成功!');
            alert('配置保存成功!');
        } else {
            console.error('配置保存失败:', saveResult.error);
            alert('配置保存失败: ' + saveResult.error);
        }
    });
});
            

3.2.3 SQLite 或其他本地数据库集成

对于更复杂的数据管理,您可以集成本地数据库,如 SQLite。最常见的方式是使用 Node.js 的 SQLite 驱动(如 `sqlite3` 或 `better-sqlite3`),并在主进程中进行数据库操作。


// main.js (片段) - SQLite 示例 (需要安装 'better-sqlite3')
// npm install better-sqlite3
const Database = require('better-sqlite3');
const dbPath = path.join(app.getPath('userData'), 'my_database.db');
let db;

ipcMain.handle('init-db', async () => {
  try {
    db = new Database(dbPath);
    // 创建表
    db.exec(`
      CREATE TABLE IF NOT EXISTS todos (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        text TEXT NOT NULL,
        completed INTEGER DEFAULT 0
      )
    `);
    return { success: true };
  } catch (error) {
    console.error('数据库初始化失败:', error);
    return { success: false, error: error.message };
  }
});

ipcMain.handle('add-todo-db', async (event, todoText) => {
  try {
    const stmt = db.prepare('INSERT INTO todos (text) VALUES (?)');
    const info = stmt.run(todoText);
    return { success: true, id: info.lastInsertRowid };
  } catch (error) {
    console.error('添加待办失败:', error);
    return { success: false, error: error.message };
  }
});

ipcMain.handle('get-todos-db', async () => {
  try {
    const todos = db.prepare('SELECT * FROM todos').all();
    return { success: true, todos: todos };
  } catch (error) {
    console.error('获取待办失败:', error);
    return { success: false, error: error.message };
  }
});

// 在应用退出前,确保关闭数据库连接 (FIXED)
app.on('before-quit', () => {
  if (db && db.open) { // 先检查数据库是否已连接
    db.close(); // 然后关闭它
    console.log('数据库连接已关闭。');
  }
});
            

注意: 使用 `better-sqlite3` 这样的原生模块时,打包时可能需要额外配置 `electron-builder` 以确保二进制文件被正确包含。

3.3 应用程序更新

为您的桌面应用提供自动更新是提升用户体验的关键。Electron 内置了 `autoUpdater` 模块,通常与 `electron-updater` 库配合使用。

3.3.1 基本概念:手动更新与自动更新

3.3.2 使用 `electron-updater` 实现自动更新

`electron-updater` 是一个简化自动更新流程的库,它基于 Electron 的 `autoUpdater` 模块。

首先,安装 `electron-updater`:


npm install electron-updater
# 或者
yarn add electron-updater
            

然后,在主进程 (`main.js`) 中配置和使用:


// main.js (片段) - 自动更新
const { app, BrowserWindow, dialog } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path'); // 确保引入 path
const log = require('electron-log'); // 推荐使用日志库

// 配置日志,方便调试 (可选,但强烈推荐)
log.transports.file.level = 'info';
autoUpdater.logger = log;

// 您可以禁用自动下载,手动触发
// autoUpdater.autoDownload = false;

function setupAutoUpdater() {
    // 检查更新
    autoUpdater.checkForUpdatesAndNotify();

    // 更新错误时触发
    autoUpdater.on('error', (err) => {
        log.error('更新出错:', err);
        dialog.showErrorBox('更新失败', `检查更新时发生错误: ${err.message}`);
    });

    // 检测到新版本时触发
    autoUpdater.on('update-available', (info) => {
        log.info('发现新版本:', info.version);
        dialog.showMessageBox({
            type: 'info',
            title: '发现新版本',
            message: `发现新版本 ${info.version},是否现在下载?`,
            buttons: ['立即下载', '稍后']
        }).then(result => {
            if (result.response === 0) { // 用户点击了"立即下载"
                autoUpdater.downloadUpdate();
            }
        });
    });

    // 没有可用更新时触发
    autoUpdater.on('update-not-available', () => {
        log.info('当前已是最新版本。');
    });

    // 更新下载进度时触发
    autoUpdater.on('download-progress', (progressObj) => {
        let log_message = `下载速度: ${(progressObj.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s`;
        log_message += ` - 已下载 ${progressObj.percent.toFixed(2)}%`;
        log_message += ` (${(progressObj.transferred / (1024 * 1024)).toFixed(2)} MB / ${(progressObj.total / (1024 * 1024)).toFixed(2)} MB)`;
        log.info(log_message);
        // 可以通过 IPC 发送进度到渲染进程显示
        // mainWindow.webContents.send('download-progress', progressObj.percent);
    });

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

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

注意: `electron-updater` 通常与 GitHub Releases 或您自己的更新服务器配合使用。您需要将打包好的应用发布到这些平台上,`electron-updater` 才能检测到新版本。

3.4 性能优化与调试

Electron 应用本质上是浏览器和 Node.js 的结合,因此性能优化和调试方法与 Web 和 Node.js 应用有很多共通之处。

3.4.1 Chrome 开发者工具在 Electron 中的应用

每个 Electron 窗口 (渲染进程) 都可以像 Chrome 浏览器标签页一样打开开发者工具。在 `BrowserWindow` 创建后,调用 `mainWindow.webContents.openDevTools();` 即可打开。您可以使用它来:

对于主进程,您可以使用 VS Code 等 IDE 的调试功能(通过 `F5` 启动调试配置),或者通过 Chrome 开发者工具连接到 Node.js 进程进行调试(在命令行启动应用时添加 `--inspect` 或 `--inspect-brk` 参数)。

3.4.2 内存与 CPU 优化技巧

3.4.3 常见性能问题及解决方案

3.5 安全性最佳实践

构建 Electron 应用时,安全性是重中之重,尤其当您的应用需要处理用户数据或访问网络时。

Electron 安全性黄金法则: 永远不要将不可信的外部内容加载到开启了 Node.js 集成的窗口中!

3.5.1 禁用 `nodeIntegration` 与开启 `contextIsolation`

这是最重要的安全设置。

请始终在 `webPreferences` 中使用它们,并配合预加载脚本 (Preload Script) 来安全地暴露所需功能。


// main.js (片段)
new BrowserWindow({
  webPreferences: {
    nodeIntegration: false, // 禁用 Node.js API 在渲染进程的直接访问
    contextIsolation: true, // 开启上下文隔离
    preload: path.join(__dirname, 'preload.js') // 必须使用预加载脚本来桥接
  }
});
            

3.5.2 内容安全策略 (CSP)

与 Web 应用类似,您可以在 HTML 文件中使用 Content Security Policy (CSP) 来限制渲染进程可以加载的资源(脚本、样式、图片等),从而防止跨站脚本攻击 (XSS)。




            

上述 CSP 策略意味着:

重要: `unsafe-inline` 和 `unsafe-eval` 应该被视为最后手段。如果可能,应将所有脚本和样式外部化,并使用哈希值或 Nonce 值来允许它们。

3.5.3 处理外部链接

当用户点击渲染进程中的一个链接时,如果链接是外部网站,应该在用户默认浏览器中打开,而不是在 Electron 窗口中。这可以防止将敏感的 Node.js/Electron API 暴露给外部网站。

`main.js` (片段):


// main.js (片段)
const { shell } = require('electron');

app.on('web-contents-created', (event, contents) => {
  contents.on('will-navigate', (event, navigationUrl) => {
    const parsedUrl = new URL(navigationUrl);
    // 仅允许加载应用内部的协议 (如 file:)
    if (parsedUrl.protocol !== 'file:') {
      event.preventDefault();
      shell.openExternal(navigationUrl); // 在默认浏览器中打开外部链接
    }
  });
});
            

3.6 常用第三方库与框架集成

Electron 应用作为前端技术的延伸,可以无缝集成各种流行的前端框架和 UI 库。

3.6.1 集成前端框架 (React/Vue/Angular)

将 React、Vue 或 Angular 项目打包成 Electron 应用非常常见。基本流程是:

  1. 使用您选择的框架的脚手架(如 Create React App, Vue CLI, Angular CLI)创建一个标准 Web 项目。
  2. 将该 Web 项目打包为静态文件(例如,`npm run build` 或 `yarn build` 生成 `build` 或 `dist` 目录)。
  3. 在 Electron 的 `main.js` 中,将 `mainWindow.loadFile('index.html')` 修改为加载您的打包目录中的 `index.html`。例如:
    
    // main.js (React/Vue/Angular 集成)
    // const path = require('path');
    // mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html')); // 假设您的构建输出在项目根目录的 'dist' 文件夹
                        
  4. 通常,您还需要配置 Electron 的打包工具 (`electron-builder`),以确保将前端框架的构建输出也包含在最终的安装包中。

3.6.2 集成 UI 库 (Ant Design/Element UI)

这些 UI 库通常是基于 React、Vue 或纯 Web Component 的。集成方式与集成前端框架类似,只要它们能在标准浏览器环境中运行,就能在 Electron 渲染进程中运行。

只需在您的前端项目中安装并引入这些 UI 库,然后在您的组件中使用它们即可。

第四部分:实战项目:构建一个简单的待办事项 (Todo List) 应用

理论知识学习了这么多,让我们通过一个简单的实战项目来巩固!我们将构建一个具备以下功能的待办事项应用:

4.1 项目结构搭建

首先,在您的项目根目录创建以下文件和文件夹:


my-todo-app/
├── main.js         # Electron 主进程脚本
├── index.html      # 渲染进程界面
├── renderer.js     # 渲染进程逻辑脚本
├── preload.js      # 预加载脚本 (用于安全地桥接主/渲染进程)
├── package.json    # 项目配置文件
└── assets/         # (新增) 存放图标等静态资源
    └── icon.png
    └── icon.ico
    └── icon.icns
            

确保您的 `package.json` 配置了 `main.js` 和 `start` 脚本,并且安装了 `electron`。

4.2 主进程与渲染进程交互实现

4.2.1 `main.js` (主进程)

主进程将负责:


// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
const fs = require('fs');

let mainWindow;
// 将数据文件放在 userData 目录下,确保跨平台持久化和写入权限
const dataFilePath = path.join(app.getPath('userData'), 'todos.json');

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

  mainWindow.loadFile('index.html');
  // mainWindow.webContents.openDevTools(); // 调试用
}

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

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

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

// IPC 处理:保存待办事项数据
ipcMain.handle('save-todos', async (event, todos) => {
  try {
    fs.writeFileSync(dataFilePath, JSON.stringify(todos, null, 2), 'utf-8');
    return { success: true };
  } catch (error) {
    console.error('保存待办事项失败:', error);
    dialog.showErrorBox('错误', '保存待办事项失败: ' + error.message);
    return { success: false, error: error.message };
  }
});

// IPC 处理:加载待办事项数据
ipcMain.handle('load-todos', async (event) => {
  try {
    if (fs.existsSync(dataFilePath)) {
      const data = fs.readFileSync(dataFilePath, 'utf-8');
      return { success: true, todos: JSON.parse(data) };
    }
    return { success: true, todos: [] }; // 如果文件不存在,返回空数组
  } catch (error) {
    console.error('加载待办事项失败:', error);
    dialog.showErrorBox('错误', '加载待办事项失败: ' + error.message);
    return { success: false, error: error.message };
  }
});
            

4.2.2 `preload.js` (预加载脚本)

预加载脚本将负责安全地将 IPC 功能暴露给渲染进程。


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

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

4.2.3 `index.html` (渲染进程界面)

简单的 HTML 结构,包含输入框、按钮和列表。





    
    我的待办事项
    
    


    

待办事项列表

4.2.4 `renderer.js` (渲染进程逻辑)

渲染进程将处理 UI 交互和通过 `window.todoAPI` 与主进程通信。


// renderer.js
const todoInput = document.getElementById('new-todo-input');
const addTodoButton = document.getElementById('add-todo-button');
const todoList = document.getElementById('todo-list');

let todos = []; // 存储待办事项的数组

// 函数:渲染待办事项列表
function renderTodos() {
    todoList.innerHTML = ''; // 清空现有列表
    todos.forEach((todo, index) => {
        const li = document.createElement('li');
        li.className = todo.completed ? 'completed' : '';
        li.innerHTML = `
            ${escapeHtml(todo.text)}
            
`; todoList.appendChild(li); }); } // 简单的 HTML 转义函数,防止 XSS function escapeHtml(text) { const div = document.createElement('div'); div.innerText = text; return div.innerHTML; } // 函数:保存待办事项到文件 async function saveTodosToFile() { const result = await window.todoAPI.saveTodos(todos); if (!result.success) { console.error('保存失败:', result.error); // 可以在这里向用户显示一个错误通知 } } // 事件监听:添加待办 async function addTodo() { const text = todoInput.value.trim(); if (text) { todos.push({ text: text, completed: false }); todoInput.value = ''; // 清空输入框 renderTodos(); await saveTodosToFile(); // 保存到文件 } } addTodoButton.addEventListener('click', addTodo); todoInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { addTodo(); } }); // 事件监听:切换完成状态和删除 todoList.addEventListener('click', async (event) => { const button = event.target.closest('button'); if (!button) return; const index = parseInt(button.dataset.index, 10); if (isNaN(index) || index < 0 || index >= todos.length) { return; } if (button.classList.contains('toggle')) { todos[index].completed = !todos[index].completed; } else if (button.classList.contains('delete')) { todos.splice(index, 1); // 删除对应项 } renderTodos(); await saveTodosToFile(); }); // 页面加载时加载待办事项 document.addEventListener('DOMContentLoaded', async () => { const result = await window.todoAPI.loadTodos(); if (result.success && result.todos) { todos = result.todos; renderTodos(); } else { console.error('加载失败:', result.error); // 可以在这里向用户显示一个错误通知 } });

4.3 打包与测试

按照第二部分的"应用程序打包与分发"中的说明,使用 `electron-builder` 配置 `package.json` 并运行 `npm run dist` 进行打包。

打包完成后,在 `dist` 目录下找到您的安装包或可执行文件,安装并运行它,测试您的待办事项应用是否正常工作,包括添加、标记完成、删除以及最重要的——关闭应用后重新打开,数据是否依然存在。

恭喜您!您已经完成了您的第一个 Electron 桌面应用程序的开发,并学习了从入门到高级的核心概念和实践。

Electron 的世界广阔而充满可能,希望本教程能为您打开探索的大门。祝您在桌面应用开发的道路上越走越远!

如果您在学习过程中有任何疑问,或者想了解更深入的特定主题,请随时提出。

互动区域

登录后可以点赞此内容

参与互动

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