Electron桌面端应用开发教程:多端开发核心技术

Viewed 0

多端开发之桌面端应用之Electron

前言:从 Web 到桌面,多端统一的终点

在现代软件开发体系中,多端架构已成为主流选择。从 Web、Mobile、MiniApp 到 Desktop,开发者需要在不同平台间平衡开发效率与用户体验。桌面端作为这个体系中的关键一环,承载着高性能计算、专业工具、企业办公等核心场景。

桌面端在多端体系中的价值

桌面端在多端应用体系中(包括Web端、Mobile端、小程序端等)占据独特地位,运行于Windows、macOS、Linux等操作系统。它拥有更强的系统权限、更好的性能、更丰富的原生能力,非常适合专业的工具场景。

桌面端的独特优势

  • 系统级权限:可以访问文件系统、管理进程、发送系统通知。
  • 性能优势:不受浏览器沙箱限制,能够充分利用硬件资源。
  • 专业场景:适用于开发IDE、设计工具、音视频处理、数据分析等复杂应用。
  • 企业级应用:满足内网部署、安全隔离、深度定制化等需求。

为什么 Electron 能打破前端与桌面的边界

传统桌面开发需要掌握 C++、C#、Objective-C 等原生语言,学习曲线陡峭。而 Electron 的出现,让前端开发者能够使用熟悉的 JavaScript/TypeScript、HTML、CSS 技术栈构建桌面应用,极大地降低了开发门槛。

其核心代码示例如下:

const { app, BrowserWindow } = require('electron');
// 创建应用窗口
const win = new BrowserWindow({
  width: 1200,
  height: 800,
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true
  }
});
// 加载前端应用页面
win.loadFile('index.html');

Electron 的突破性创新

  1. 技术栈统一:直接使用前端技术。
  2. 生态复用:可以接入庞大的 npm 生态。
  3. 快速迭代:完整的前端工程化工具链支持。
  4. 跨平台一致:一套代码可构建 Windows、macOS、Linux 应用。

前端工程师的桌面新边界:从浏览器到操作系统

Electron 将前端开发者的能力边界从浏览器扩展到了操作系统层面。对比浏览器中的 Web 应用,Electron 应用拥有完整文件系统访问、进程管理、完整系统通知 API、不受 CORS 限制的网络请求、多窗口与托盘控制等能力,并且性能更接近原生应用,支持完全离线运行。

趋势视角:WebRuntime 的崛起

我们正见证着 WebRuntime 的演进:从第一代的浏览器,到第二代的 Node.js(服务端 JavaScript),再到第三代的 Electron、Tauri(桌面 WebRuntime),以及第四代的 WebContainer、Deno(云端 WebRuntime)。Electron 正是第三代的核心代表,它融合了 Web 标准、Node.js 运行时和操作系统原生 API。

Electron 如何成为「多端统一」的关键拼图

在多端开发体系中,Electron 扮演着桌面端容器的角色。开发者可以通过适配器模式,实现核心业务逻辑的跨平台复用(Web、Electron、React Native),同时利用 Electron 的特有能力增强桌面端体验。UI层(如 React/Vue 组件)和业务逻辑层可以高度共享,通过统一的构建工具进行打包。

Electron 的起源与发展

起源:从 Atom Shell 到 Electron

Electron 起源于 2013 年,由 GitHub 开发,最初名为 Atom Shell,用于构建其 Atom 文本编辑器。GitHub 需要一款跨平台的桌面编辑器,并希望使用 Web 技术进行快速迭代,同时能访问系统级 API。其核心创新在于将 Chromium 浏览器内核与 Node.js 运行时融合。

2015 年,项目正式开源并更名为 Electron,迅速获得社区关注。GitHub 意识到它不只是一个编辑器框架,而是一个通用的桌面应用开发平台。

  • 2013年:Atom Shell 诞生,作为 GitHub 内部项目。
  • 2015年:更名为 Electron 并正式开源。
  • 2016年:Electron 1.0 发布,API 进入稳定阶段。
  • 2018年:Electron 3.0 引入重要的上下文隔离安全特性。
  • 2020年至今:持续迭代,默认启用上下文隔离,并不断优化性能与安全,支持最新 Web 标准。

核心技术栈:Chromium + Node.js + V8

Electron 应用的本质是一个集成了 Node.js 的 Chromium 浏览器。其架构主要包含三部分:

  1. Chromium:用于渲染界面,包含 Blink 渲染引擎、V8 JavaScript 引擎和 Skia 图形库,采用多进程架构保障稳定性。
  2. Node.js:提供底层系统能力,基于 libuv 事件循环,提供文件系统、网络等 API。
  3. Native APIs:提供跨平台抽象,封装了窗口管理、系统托盘、全局快捷键、原生对话框等操作系统原生功能。

例如,Electron 28 包含了 Chromium 120、Node.js 18 和 V8 12。

工程视角:版本演进与构建优化

Electron 需要协调 Chromium、Node.js 和自身三个独立项目的版本,这带来了挑战。在 package.json 中,electron 依赖项就包含了特定的 Chromium 和 Node.js 版本。你可以通过 process.versions 查看具体信息。

构建优化策略包括

  1. 原生模块重编译:为 Electron 的 Node 版本重新编译原生模块(使用 electron-rebuild)。
  2. 多版本兼容:在代码中检测运行环境(process.versions.electron),区分 Electron 和纯浏览器环境。

趋势视角:Electron 与竞争方案的格局

桌面开发方案众多,各有千秋。Qt (C++), WPF (C#) 是传统原生方案。NW.js 是老牌的 Web 技术方案。Electron 是目前最成熟、生态最丰富的 Web 技术方案。React Native Desktop、Flutter Desktop 是移动优先的跨端方案。Tauri (Rust + Web) 是新兴的轻量级方案,体积小性能好。WebContainer 则是纯粹的 Web 沙盒环境。

就市场份额而言,Electron 最为成熟,占有率最高;Tauri 作为挑战者增长迅速;Flutter Desktop 仍在发展中;NW.js 则逐渐被取代。

生态现状与代表应用

Electron 拥有庞大的生态和众多成功案例:

  • 开发工具:Visual Studio Code、Atom、Postman、GitHub Desktop。
  • 协作办公:Slack、Discord、Microsoft Teams、Notion。
  • 设计创意:Figma Desktop、Obsidian、Typora。
  • 其他工具:Twitch、WhatsApp Desktop、1Password 早期版本。

其生态规模庞大,在 npm 上有超过 15,000 个相关包,在 GitHub 上拥有超过 110,000 星,被数万款生产环境应用所使用。

核心概念解析

Electron 的主进程和渲染进程有什么区别?如何进行通信?

主进程(Main Process)

  • 每个 Electron 应用有且仅有一个主进程。
  • 运行在 Node.js 环境中,可使用所有 Node.js API。
  • 负责应用生命周期(启动、退出)、创建管理窗口(BrowserWindow)、处理系统级操作(菜单、托盘、通知)。

渲染进程(Renderer Process)

  • 每个 BrowserWindow 实例对应一个独立的渲染进程。
  • 运行在 Chromium 环境中,类似于浏览器标签页,负责展示 Web 界面和用户交互。
  • 默认无法直接访问 Node.js API(出于安全考虑)。

进程间通信(IPC)方式

  1. IPC 模块:使用 ipcMain(主进程)和 ipcRenderer(渲染进程)模块发送和监听消息。
  2. Preload 脚本:在渲染进程加载前注入,作为安全桥梁连接两个进程。
  3. Remote 模块:已废弃,不推荐使用。

通信时,主进程通过 ipcMain.onipcMain.handle 监听,渲染进程通过 ipcRenderer.sendipcRenderer.invoke 发送,并通过 contextBridge 在预加载脚本中安全暴露 API。

在 Electron 中,如何优化应用性能?

启动性能优化

  1. 延迟加载:按需加载非必要模块。
  2. V8 快照:使用 mksnapshot 预编译常用代码。
  3. 窗口预加载:提前创建隐藏窗口,需要时快速显示。

运行时性能优化

  1. 进程隔离:避免主进程阻塞,将耗时任务放入独立进程或 Worker。
  2. 渲染优化:启用硬件加速,使用 CSS transform/opacity 做动画,对长列表进行虚拟滚动。
  3. 内存管理:及时销毁不用的 BrowserWindow,监控并避免内存泄漏。

打包体积优化

  1. Tree Shaking 与代码分割。
  2. 使用 electron-builderasarUnpack 合理排除大文件。
  3. 仅打包目标平台所需的原生模块依赖。

Electron 项目初始化

项目初始化主要有两种推荐方案:官方的 Electron Forge 和社区的 Electron Vite。

基于 Electron Forge(官方推荐)

Electron Forge 是官方维护的完整工具链,覆盖创建、开发、打包、发布全流程,适合生产环境。

创建项目

# 使用 npm(也可用 yarn/pnpm)
npm init electron-app@latest my-electron-app
cd my-electron-app

国内用户可配置 npm 镜像源加速。

使用模板:Forge 提供了多种模板,如 webpackwebpack-typescriptvitevite-typescript。其中 vite-typescript 模板开发体验最佳。

# 使用 Vite + TypeScript 模板
npm init electron-app@latest my-app -- --template=vite-typescript

创建后的项目包含主进程、预加载脚本、渲染进程代码及配置文件。

集成前端框架(以 Vue 3 为例)

  1. 安装 Vue 及相关 Vite 插件。
  2. 修改渲染器的 Vite 配置(vite.renderer.config.js),添加 vue() 插件和路径别名。
  3. 修改 index.html,提供挂载节点。
  4. 创建 App.vue 组件,并通过预加载脚本暴露的 API(如 window.electronAPI)调用 Electron 能力。
  5. 修改渲染器入口文件(renderer.js),使用 createApp 挂载 Vue 应用。
  6. 在预加载脚本(preload.js)中使用 contextBridge.exposeInMainWorld 安全暴露 API。
  7. 在主进程(main.js)中添加对应的 IPC 处理器(如 ipcMain.handle('ping', ...))。

启动应用

  • npm start:启动开发模式(支持热更新)。
  • npm run package:打包应用。
  • npm run make:制作安装包。
  • npm run publish:发布应用。

基于 Electron Vite

Electron Vite 是社区提供的轻量级方案,专为 Vite 优化,开发体验更快捷。

创建项目

pnpm create @quick-start/electron my-app

创建时可选择原生 JavaScript 或 Vue、React、Svelte 等框架模板。项目结构清晰,主进程、预加载脚本、渲染进程代码分离。

其核心配置文件 electron.vite.config.ts 分别定义了主进程、预加载脚本和渲染进程的构建配置。

启动与构建

  • npm run dev:开发模式。
  • npm run build:构建应用。
  • npm run build:win / mac / linux:打包特定平台安装包(通常需集成 electron-builder)。

方案对比与选择建议

对比项 Electron Forge Electron Vite
官方支持 官方维护 社区维护
开箱即用 完整工具链 需额外配置打包
构建速度 较慢 (Webpack) 极快 (Vite)
热更新 中等 毫秒级
打包发布 内置完整支持 需集成其他工具
适用场景 企业级项目、需要稳定支持 快速原型、现代前端框架、追求开发体验

选择建议

  • 需要稳定、完整工具链支持的企业级项目,选 Electron Forge
  • 追求极速开发体验、使用现代前端框架的快速原型项目,选 Electron Vite

主进程与渲染进程深度解析

Electron 采用多进程架构。主进程负责应用生命周期和系统级操作,运行在 Node.js 环境。渲染进程负责界面展示,每个窗口一个进程,运行在 Chromium 环境。预加载脚本作为桥梁,运行在独立的上下文中,通过 contextBridge 安全地暴露特定 API 给渲染进程。

理解上下文隔离(Context Isolation)

上下文隔离是 Electron 的重要安全特性(从 Electron 12 开始默认启用),它确保预加载脚本和渲染进程运行在不同的 JavaScript 上下文中。这可以防止预加载脚本污染渲染进程全局对象,有效防御 XSS 攻击,遵循最小权限原则。

需要在创建 BrowserWindow 时配置 webPreferences 来启用:

const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true, // 启用上下文隔离
    nodeIntegration: false, // 禁用 Node.js 集成(推荐)
    preload: path.join(__dirname, 'preload.js') // 指定预加载脚本
  }
});

预加载脚本应使用 contextBridge 来暴露 API:

const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  onUpdateAvailable: (callback) => {
    const subscription = (event, ...args) => callback(...args);
    ipcRenderer.on('update-available', subscription);
    return () => ipcRenderer.removeListener('update-available', subscription);
  }
});

进程间通信(IPC)模式

IPC 是 Electron 应用的核心机制,通过 ipcMainipcRenderer 进行异步、安全的数据交换。

模式一:渲染进程到主进程(单向)
适用于发送通知、记录日志等无需返回值的场景。

  • 主进程:使用 ipcMain.on('event-name', handler) 监听。
  • 预加载脚本:暴露封装好的 ipcRenderer.send 方法。
  • 渲染进程:调用暴露的 API,如 window.electronAPI.logUserAction(...)

模式二:渲染进程到主进程(双向)
最常用,适用于文件操作、数据库查询等需要返回结果的场景。

  • 主进程:使用 ipcMain.handle('channel', async handler) 处理请求并返回 Promise。
  • 预加载脚本:暴露封装好的 ipcRenderer.invoke 方法。
  • 渲染进程:使用 await window.electronAPI.someMethod(...) 调用并等待结果。

模式三:主进程到渲染进程
适用于主动通知渲染进程,如系统事件、后台任务进度更新。

  • 主进程:通过 win.webContents.send('event', data) 向特定窗口发送消息,或遍历 BrowserWindow.getAllWindows() 广播。
  • 预加载脚本:暴露封装好的 ipcRenderer.on 监听器,并返回取消订阅的函数。
  • 渲染进程:调用监听函数,并在组件卸载时取消订阅以防止内存泄漏。

常用 API 详解

1. app 模块

控制应用生命周期,仅用于主进程。

  • 核心事件whenReady(应用就绪)、window-all-closed(所有窗口关闭)、before-quit(退出前,可阻止)、activate(macOS 激活)。
  • 常用方法
    • app.getPath('userData'):获取用户数据目录等重要路径。
    • app.setLoginItemSettings():设置开机自启。
    • app.quit() / app.exit():退出应用。
    • app.requestSingleInstanceLock():实现单实例应用(防止多开)。

2. BrowserWindow 模块

用于创建和管理应用窗口。

  • 创建窗口new BrowserWindow(options),关键配置包括尺寸、是否显示边框 (frame)、标题栏样式 (titleBarStyle),以及重要的安全配置 webPreferences(如 contextIsolation, preload 脚本路径)。
  • 窗口事件ready-to-show(可显示)、close(关闭前,可阻止)、closed(已关闭)、maximize/unmaximize 等。
  • 窗口操作show()/hide()minimize()/maximize()setBounds()/setPosition()setAlwaysOnTop()setProgressBar()(设置任务栏进度条)、close()
  • 无边框窗口:设置 frame: false 可创建自定义标题栏的窗口,需自行实现窗口控制逻辑。
contextBridge.exposeInMainWorld('windowControls', {
  minimize: () => ipcRenderer.send('window:minimize'),
  maximize: () => ipcRenderer.send('window:maximize'),
  close: () => ipcRenderer.send('window:close')
});

// 主进程处理窗口控制
ipcMain.on('window:minimize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win.minimize();
});

ipcMain.on('window:maximize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  if (win.isMaximized()) {
    win.unmaximize();
  } else {
    win.maximize();
  }
});

ipcMain.on('window:close', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win.close();
});

3. ipcMain 和 ipcRenderer 模块高级用法

除了基础的进程间通信,Electron 的 IPC 模块还支持更高级的通信模式,如消息端口和流式数据传输。

消息端口通信提供了更灵活的双向通信能力。在主进程中,可以创建 MessageChannelMain,将端口发送给渲染进程:

// 主进程 - 使用 MessagePort 进行更灵活的通信
const { MessageChannelMain } = require('electron');

ipcMain.on('request-port', (event) => {
  const { port1, port2 } = new MessageChannelMain();

  // 监听端口 1 的消息
  port1.on('message', (event) => {
    console.log('收到消息:', event.data);
  });

  // 发送消息到端口 1
  port1.postMessage({ type: 'greeting', data: 'Hello' });

  // 将端口 2 发送到渲染进程
  event.sender.postMessage('provide-port', null, [port2]);

  // 启动端口
  port1.start();
});

// 渲染进程(通过预加载脚本)
ipcRenderer.on('provide-port', (event) => {
  const port = event.ports[0];

  port.onmessage = (event) => {
    console.log('收到主进程消息:', event.data);
  };

  port.postMessage({ type: 'response', data: 'Hi' });

  port.start();
});

流式数据传输适用于处理大文件等场景,可以分块传输数据并报告进度:

// 主进程 - 处理大文件读取,流式传输
ipcMain.handle('file:readStream', async (event, filePath) => {
  const fs = require('fs');
  const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 });

  let totalSize = 0;
  const fileStats = await fs.promises.stat(filePath);

  stream.on('data', (chunk) => {
    totalSize += chunk.length;
    const progress = (totalSize / fileStats.size) * 100;

    // 发送进度更新
    event.sender.send('file:readProgress', {
      progress: progress.toFixed(2),
      chunk: chunk.toString('base64') // 传输二进制数据
    });
  });

  return new Promise((resolve, reject) => {
    stream.on('end', () => resolve({ success: true }));
    stream.on('error', (error) => reject(error));
  });
});

4. Menu 模块

Menu 模块用于创建应用菜单和上下文菜单,是桌面应用的重要组成部分。

应用菜单定义了应用顶部的菜单栏。以下是一个包含文件、编辑、视图、窗口和帮助菜单的完整示例,并针对 macOS 平台做了特殊处理:

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

// 定义菜单模板
const template = [
  {
    label: '文件',
    submenu: [
      {
        label: '新建',
        accelerator: 'CmdOrCtrl+N',
        click: (menuItem, browserWindow, event) => {
          console.log('新建文件');
        }
      },
      {
        label: '打开',
        accelerator: 'CmdOrCtrl+O',
        click: async () => {
          const result = await dialog.showOpenDialog({
            properties: ['openFile']
          });
          if (!result.canceled) {
            console.log('打开文件:', result.filePaths[0]);
          }
        }
      },
      { type: 'separator' },
      {
        label: '保存',
        accelerator: 'CmdOrCtrl+S',
        enabled: true,
        click: () => {
          // 保存文件逻辑
        }
      },
      {
        label: '另存为...',
        accelerator: 'CmdOrCtrl+Shift+S'
      },
      { type: 'separator' },
      {
        label: '退出',
        accelerator: 'CmdOrCtrl+Q',
        role: 'quit'
      }
    ]
  },
  {
    label: '编辑',
    submenu: [
      { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
      { label: '重做', accelerator: 'CmdOrCtrl+Y', role: 'redo' },
      { type: 'separator' },
      { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
      { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
      { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' },
      { label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }
    ]
  },
  {
    label: '视图',
    submenu: [
      { label: '重新加载', accelerator: 'CmdOrCtrl+R', role: 'reload' },
      { label: '强制重新加载', accelerator: 'CmdOrCtrl+Shift+R', role: 'forceReload' },
      { label: '切换开发者工具', accelerator: 'Alt+CmdOrCtrl+I', role: 'toggleDevTools' },
      { type: 'separator' },
      { label: '实际大小', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' },
      { label: '放大', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' },
      { label: '缩小', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' },
      { type: 'separator' },
      { label: '全屏', accelerator: 'F11', role: 'togglefullscreen' }
    ]
  },
  {
    label: '窗口',
    role: 'window',
    submenu: [
      { label: '最小化', accelerator: 'CmdOrCtrl+M', role: 'minimize' },
      { label: '关闭', accelerator: 'CmdOrCtrl+W', role: 'close' }
    ]
  },
  {
    label: '帮助',
    role: 'help',
    submenu: [
      {
        label: '了解更多',
        click: async () => {
          const { shell } = require('electron');
          await shell.openExternal('https://electronjs.org');
        }
      },
      { type: 'separator' },
      {
        label: '关于',
        click: () => {
          dialog.showMessageBox({
            type: 'info',
            title: '关于',
            message: '我的应用',
            detail: `版本: ${app.getVersion()}\nElectron: ${process.versions.electron}\nChrome: ${process.versions.chrome}\nNode.js: ${process.versions.node}`
          });
        }
      }
    ]
  }
];

// macOS 特殊处理
if (process.platform === 'darwin') {
  template.unshift({
    label: app.name,
    submenu: [
      { label: `关于 ${app.name}`, role: 'about' },
      { type: 'separator' },
      { label: '服务', role: 'services' },
      { type: 'separator' },
      { label: `隐藏 ${app.name}`, accelerator: 'Command+H', role: 'hide' },
      { label: '隐藏其他', accelerator: 'Command+Alt+H', role: 'hideOthers' },
      { label: '显示全部', role: 'unhide' },
      { type: 'separator' },
      { label: '退出', accelerator: 'Command+Q', role: 'quit' }
    ]
  });
}

// 构建并设置菜单
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

右键菜单(上下文菜单) 可以通过 IPC 在渲染进程中触发,并在主进程中创建显示。下面的例子实现了一个包含复制、粘贴和打开链接功能的右键菜单:

// 主进程
const { Menu, MenuItem } = require('electron');

ipcMain.on('show-context-menu', (event, params) => {
  const template = [
    {
      label: '复制',
      click: () => {
        event.sender.send('context-menu-command', 'copy');
      }
    },
    {
      label: '粘贴',
      click: () => {
        event.sender.send('context-menu-command', 'paste');
      }
    },
    { type: 'separator' },
    {
      label: '在浏览器中打开',
      visible: params.linkURL !== undefined,
      click: () => {
        require('electron').shell.openExternal(params.linkURL);
      }
    }
  ];

  const menu = Menu.buildFromTemplate(template);
  menu.popup(BrowserWindow.fromWebContents(event.sender));
});

// 渲染进程(通过预加载脚本)
window.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  ipcRenderer.send('show-context-menu', {
    linkURL: e.target.href || undefined
  });
});

ipcRenderer.on('context-menu-command', (event, command) => {
  if (command === 'copy') {
    document.execCommand('copy');
  } else if (command === 'paste') {
    document.execCommand('paste');
  }
});

动态菜单 允许根据应用状态更新菜单项。你可以更新现有菜单项的状态,或者完全重新构建菜单:

// 根据应用状态动态更新菜单
function updateMenu(isPlaying) {
  const menu = Menu.getApplicationMenu();
  const playbackMenu = menu.items.find(item => item.label === '播放');

  if (playbackMenu) {
    playbackMenu.submenu.items[0].enabled = !isPlaying; // 播放按钮
    playbackMenu.submenu.items[1].enabled = isPlaying;  // 暂停按钮
  }
}

// 或者完全重建菜单
function rebuildMenu(userData) {
  const template = [
    {
      label: '用户',
      submenu: [
        {
          label: userData.name,
          enabled: false
        },
        { type: 'separator' },
        {
          label: '登出',
          click: () => {
            // 登出逻辑
          }
        }
      ]
    },
    // ... 其他菜单项
  ];

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

5. dialog 模块

dialog 模块用于显示系统原生的对话框,如文件选择、保存和消息框。

文件/目录选择对话框 可以配置多种选项。下面的例子展示了如何打开文件和保存文件:

const { dialog } = require('electron');

// 打开文件对话框
ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog({
    title: '选择文件',
    defaultPath: app.getPath('documents'),
    buttonLabel: '打开',
    filters: [
      { name: '图片', extensions: ['jpg', 'png', 'gif'] },
      { name: '文档', extensions: ['pdf', 'doc', 'docx'] },
      { name: '所有文件', extensions: ['*'] }
    ],
    properties: [
      'openFile',
      'openDirectory',
      'multiSelections',
      'showHiddenFiles',
      'createDirectory',
      'promptToCreate'
    ]
  });

  if (result.canceled) {
    return { canceled: true };
  }

  return {
    canceled: false,
    filePaths: result.filePaths
  };
});

// 保存文件对话框
ipcMain.handle('dialog:saveFile', async (event, content) => {
  const result = await dialog.showSaveDialog({
    title: '保存文件',
    defaultPath: path.join(app.getPath('documents'), 'untitled.txt'),
    buttonLabel: '保存',
    filters: [
      { name: '文本文件', extensions: ['txt'] },
      { name: '所有文件', extensions: ['*'] }
    ],
    properties: [
      'createDirectory',
      'showOverwriteConfirmation'
    ]
  });

  if (result.canceled) {
    return { canceled: true };
  }

  // 写入文件
  try {
    await fs.promises.writeFile(result.filePath, content, 'utf-8');
    return { success: true, filePath: result.filePath };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

消息框 用于向用户显示信息或获取确认。推荐使用异步版本以避免阻塞主进程:

// 异步消息框(推荐)
ipcMain.handle('dialog:showMessage', async (event, options) => {
  const result = await dialog.showMessageBox({
    type: 'info',
    title: '提示',
    message: '主要消息',
    detail: '详细说明文本',
    buttons: ['确定', '取消', '更多'],
    defaultId: 0,
    cancelId: 1,
    noLink: false,
    checkboxLabel: '不再提示',
    checkboxChecked: false
  });

  return {
    response: result.response,
    checkboxChecked: result.checkboxChecked
  };
});

// 同步消息框(阻塞主进程,慎用)
const choice = dialog.showMessageBoxSync(mainWindow, {
  type: 'question',
  buttons: ['是', '否'],
  title: '确认',
  message: '确定要删除吗?'
});

if (choice === 0) {
  // 用户点击"是"
}

错误对话框 可以快速显示错误信息:

// 显示错误框(简单版本)
dialog.showErrorBox('错误', '发生了一个错误,请重试。');

// 或使用消息框显示更详细的错误
dialog.showMessageBox({
  type: 'error',
  title: '操作失败',
  message: '无法保存文件',
  detail: error.stack,
  buttons: ['确定']
});

6. shell 模块

shell 模块提供与桌面集成的功能,如打开外部链接、在文件管理器中显示文件等。

const { shell } = require('electron');

// 在默认浏览器中打开 URL
ipcMain.handle('shell:openExternal', async (event, url) => {
  try {
    await shell.openExternal(url);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// 在文件管理器中显示文件
ipcMain.handle('shell:showItemInFolder', async (event, fullPath) => {
  shell.showItemInFolder(fullPath);
});

// 用默认应用打开文件
ipcMain.handle('shell:openPath', async (event, path) => {
  const error = await shell.openPath(path);
  if (error) {
    return { success: false, error };
  }
  return { success: true };
});

// 将文件移到回收站
ipcMain.handle('shell:trashItem', async (event, path) => {
  try {
    await shell.trashItem(path);
    return { success: true };
  } catch (error) {
    return { success: false, error: error.message };
  }
});

// 播放提示音
shell.beep();

// 写入剪贴板书签(macOS)
shell.writeShortcutLink(shortcutPath, operation, options);

为了安全地在渲染进程中使用这些功能,需要通过预加载脚本暴露 API:

// preload.js
contextBridge.exposeInMainWorld('shell', {
  openExternal: (url) => ipcRenderer.invoke('shell:openExternal', url),
  showItemInFolder: (path) => ipcRenderer.invoke('shell:showItemInFolder', path),
  openPath: (path) => ipcRenderer.invoke('shell:openPath', path),
  trashItem: (path) => ipcRenderer.invoke('shell:trashItem', path)
});

// renderer.js
document.getElementById('openBrowser').addEventListener('click', async () => {
  await window.shell.openExternal('https://electronjs.org');
});

document.getElementById('showFile').addEventListener('click', async () => {
  await window.shell.showItemInFolder('/path/to/file.txt');
});

7. webContents 模块

webContents 负责渲染和控制网页,是 BrowserWindow 的核心属性,提供了丰富的页面控制功能。

const { BrowserWindow } = require('electron');

const win = new BrowserWindow({ width: 800, height: 600 });
const contents = win.webContents;

// 加载 URL
contents.loadURL('https://example.com');

// 执行 JavaScript
contents.executeJavaScript(`
  document.body.style.backgroundColor = 'red';
  return document.title;
`).then(title => {
  console.log('页面标题:', title);
});

// 插入 CSS
contents.insertCSS('body { background-color: blue; }');

// 打印页面
contents.print({
  silent: false,
  printBackground: true,
  deviceName: '',
  color: true,
  margins: {
    marginType: 'printableArea'
  },
  landscape: false,
  scaleFactor: 100,
  pagesPerSheet: 1,
  collate: false,
  copies: 1
});

// 打印为 PDF
contents.printToPDF({
  marginsType: 0,
  pageSize: 'A4',
  printBackground: true,
  printSelectionOnly: false,
  landscape: false
}).then(data => {
  fs.writeFile('/path/to/output.pdf', data, (error) => {
    if (error) throw error;
    console.log('PDF 生成成功');
  });
});

// 截图
contents.capturePage().then(image => {
  fs.writeFile('/path/to/screenshot.png', image.toPNG(), (error) => {
    if (error) throw error;
    console.log('截图保存成功');
  });
});

// 缩放
contents.setZoomFactor(1.5);
contents.setZoomLevel(1);

// 查找文本
contents.findInPage('search term', {
  forward: true,
  matchCase: false,
});

// 停止查找
contents.stopFindInPage('clearSelection');

// 开始/停止加载
contents.reload();
contents.reloadIgnoringCache();
contents.stop();

// 历史导航
contents.goBack();
contents.goForward();
contents.goToIndex(1);
contents.goToOffset(-1);

// 判断状态
const canGoBack = contents.canGoBack();
const canGoForward = contents.canGoForward();
const isLoading = contents.isLoading();
const isWaitingForResponse = contents.isWaitingForResponse();

// 发送消息到渲染进程
contents.send('channel-name', arg1, arg2);

// 事件监听
contents.on('did-finish-load', () => {
  console.log('页面加载完成');
});

contents.on('did-fail-load', (event, errorCode, errorDescription) => {
  console.error('页面加载失败:', errorDescription);
});

contents.on('did-navigate', (event, url) => {
  console.log('导航到:', url);
});

contents.on('new-window', (event, url) => {
  event.preventDefault();
  // 自定义处理
});

// 控制台消息
contents.on('console-message', (event, level, message, line, sourceId) => {
  console.log(`[${level}] ${message} (${sourceId}:${line})`);
});

8. nativeImage 模块

nativeImage 用于创建和操作图片,支持从多种来源创建图片,并进行调整大小、裁剪等操作。

const { nativeImage } = require('electron');

// 从文件创建图片
const image = nativeImage.createFromPath('/path/to/icon.png');

// 从 Buffer 创建
const buffer = fs.readFileSync('/path/to/icon.png');
const image = nativeImage.createFromBuffer(buffer);

// 从 Data URL 创建
const dataURL = 'data:image/png;base64,iVBORw0KG...';
const image = nativeImage.createFromDataURL(dataURL);

// 创建空图片
const image = nativeImage.createEmpty();

// 获取图片信息
const size = image.getSize();
const aspectRatio = image.getAspectRatio();
const isEmpty = image.isEmpty();

// 转换格式
const pngBuffer = image.toPNG();
const jpegBuffer = image.toJPEG(80);
const dataURL = image.toDataURL();
const bitmap = image.toBitmap();

// 调整大小
const resized = image.resize({
  width: 64,
  height: 64,
  quality: 'best'
});

// 裁剪
const cropped = image.crop({
  x: 0,
  y: 0,
  width: 100,
  height: 100
});

// 设置模板图片(macOS 菜单栏图标)
image.setTemplateImage(true);

// 应用场景示例
const tray = new Tray(nativeImage.createFromPath('icon.png'));
const icon = nativeImage.createFromPath('icon.png').resize({ width: 16, height: 16 });

9. session 模块

session 管理浏览器会话、cookie、缓存、代理和请求拦截等,是实现网络功能控制的关键。

const { session } = require('electron');

// 默认会话
const ses = session.defaultSession;

// 或创建自定义会话
const customSession = session.fromPartition('persist:mypartition');

// Cookie 管理
ipcMain.handle('session:getCookies', async (event, url) => {
  const cookies = await ses.cookies.get({ url });
  return cookies;
});

ipcMain.handle('session:setCookie', async (event, cookie) => {
  await ses.cookies.set(cookie);
});

ipcMain.handle('session:removeCookie', async (event, url, name) => {
  await ses.cookies.remove(url, name);
});

// 清除缓存
ipcMain.handle('session:clearCache', async () => {
  await ses.clearCache();
});

// 清除存储数据
ipcMain.handle('session:clearStorageData', async (options) => {
  await ses.clearStorageData({
    storages: ['appcache', 'cookies', 'filesystem', 'indexdb', 'localstorage', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
  });
});

// 设置下载路径
ses.on('will-download', (event, item, webContents) => {
  // 设置保存路径
  item.setSavePath(path.join(app.getPath('downloads'), item.getFilename()));

  // 下载进度
  item.on('updated', (event, state) => {
    if (state === 'interrupted') {
      console.log('下载中断');
    } else if (state === 'progressing') {
      if (item.isPaused()) {
        console.log('下载暂停');
      } else {
        console.log(`已下载 ${item.getReceivedBytes()} / ${item.getTotalBytes()} 字节`);
        webContents.send('download-progress', {
          progress: (item.getReceivedBytes() / item.getTotalBytes()) * 100
        });
      }
    }
  });

  // 下载完成
  item.once('done', (event, state) => {
    if (state === 'completed') {
      console.log('下载成功:', item.getSavePath());
      webContents.send('download-complete', {
        filePath: item.getSavePath()
      });
    } else {
      console.log(`下载失败: ${state}`);
    }
  });
});

// 设置代理
ses.setProxy({
  proxyRules: 'http://proxy.example.com:8080',
  proxyBypassRules: 'localhost,127.0.0.1'
});

// 拦截请求
ses.webRequest.onBeforeRequest((details, callback) => {
  if (details.url.includes('tracking')) {
    callback({ cancel: true });
  } else {
    callback({ cancel: false });
  }
});

// 修改请求头
ses.webRequest.onBeforeSendHeaders((details, callback) => {
  details.requestHeaders['User-Agent'] = 'MyElectronApp/1.0';
  callback({ requestHeaders: details.requestHeaders });
});

// 修改响应头
ses.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ['default-src \'self\'']
    }
  });
});

10. clipboard 模块

clipboard 模块提供系统剪贴板的读写功能,支持文本、HTML、图片和 RTF 等多种格式。

const { clipboard } = require('electron');

// 读取文本
const text = clipboard.readText();
console.log('剪贴板文本:', text);

// 写入文本
clipboard.writeText('Hello Electron');

// 读取 HTML
const html = clipboard.readHTML();

// 写入 HTML
clipboard.writeHTML('<b>Hello</b> <i>Electron</i>');

// 读取图片
const image = clipboard.readImage();
if (!image.isEmpty()) {
  fs.writeFileSync('/path/to/clipboard.png', image.toPNG());
}

// 写入图片
const image = nativeImage.createFromPath('/path/to/image.png');
clipboard.writeImage(image);

// 读取 RTF
const rtf = clipboard.readRTF();

// 写入 RTF
clipboard.writeRTF('{\\rtf1\\ansi\\b Hello\\b0}');

// 读取书签(macOS)
const bookmark = clipboard.readBookmark();

// 写入书签
clipboard.writeBookmark('标题', 'https://electronjs.org');

// 清空剪贴板
clipboard.clear();

// 多格式写入
clipboard.write({
  text: 'Plain text',
  html: '<b>Bold text</b>',
  rtf: '{\\rtf1\\ansi\\b RTF text\\b0}',
  image: nativeImage.createFromPath('/path/to/image.png')
});

// 获取支持的格式
const formats = clipboard.availableFormats();
console.log('剪贴板格式:', formats);

// IPC 示例
ipcMain.handle('clipboard:readText', () => {
  return clipboard.readText();
});

ipcMain.handle('clipboard:writeText', (event, text) => {
  clipboard.writeText(text);
  return { success: true };
});

ipcMain.handle('clipboard:readImage', () => {
  const image = clipboard.readImage();
  if (image.isEmpty()) {
    return null;
  }
  return image.toDataURL();
});

// preload.js
contextBridge.exposeInMainWorld('clipboard', {
  readText: () => ipcRenderer.invoke('clipboard:readText'),
  writeText: (text) => ipcRenderer.invoke('clipboard:writeText', text),
  readImage: () => ipcRenderer.invoke('clipboard:readImage')
});

// renderer.js
document.getElementById('copyBtn').addEventListener('click', async () => {
  const text = document.getElementById('textInput').value;
  await window.clipboard.writeText(text);
  console.log('已复制到剪贴板');
});

document.getElementById('pasteBtn').addEventListener('click', async () => {
  const text = await window.clipboard.readText();
  document.getElementById('textInput').value = text;
});

应用构建与发布

应用构建与发布是 Electron 开发的最后阶段,涉及打包、签名、制作安装程序和发布到各平台。Electron Forge 提供了完整的工具链支持。

构建与发布概述

Electron Forge 构建流程主要分为三步:

  1. Package(打包):将源代码和 Electron 运行时打包成可执行应用。
  2. Make(制作):基于打包结果创建平台特定的安装程序。
  3. Publish(发布):将安装程序上传到发布平台。

配置文件通常是 forge.config.jsforge.config.ts

module.exports = {
  packagerConfig: {
    asar: true,
    icon: './assets/icon',
    appBundleId: 'com.company.app',
    appCategoryType: 'public.app-category.productivity',
    win32metadata: {
      CompanyName: 'My Company',
      FileDescription: 'My Electron App',
      ProductName: 'My App'
    },
    osxSign: {
      identity: 'Developer ID Application: Company Name (TEAM_ID)',
      hardenedRuntime: true,
      entitlements: 'entitlements.plist',
      'entitlements-inherit': 'entitlements.plist',
      'signature-flags': 'library'
    },
    osxNotarize: {
      appleId: process.env.APPLE_ID,
      appleIdPassword: process.env.APPLE_ID_PASSWORD,
      teamId: process.env.APPLE_TEAM_ID
    }
  },
  rebuildConfig: {},
  makers: [
    // 不同平台的安装包制作器
  ],
  publishers: [
    // 发布配置
  ],
  plugins: [
    // 插件配置
  ]
};

执行命令

# 打包应用(不创建安装程序)
npm run package
# 制作安装包
npm run make
# 发布应用
npm run publish
# 指定平台和架构
npm run make -- --platform=darwin --arch=x64
npm run make -- --platform=win32 --arch=ia32,x64
npm run make -- --platform=linux --arch=x64

图标配置

不同平台对图标格式和尺寸有不同的要求:

  • Windows:需要 .ico 格式,包含多个尺寸(如 16x16, 32x32, 48x48, 256x256)。
  • macOS:需要 .icns 格式,包含从 16x16 到 1024x1024 的多个尺寸(含 Retina @2x 版本)。
  • Linux:通常使用 .png 格式,推荐 512x512 或 1024x1024。

在 Forge 配置中,只需指定不含扩展名的图标路径,它会自动选择对应平台的文件:

packagerConfig: {
  icon: './assets/icons/icon', // 自动选择 icon.ico、icon.icns 或 icon.png
}

不同 Maker 的作用

Maker 用于创建平台特定的安装程序。常见的 Maker 包括:

  • Squirrel.Windows:为 Windows 创建 Setup.exe 安装程序,支持自动更新。
  • WiX MSI:使用 WiX 工具集创建企业级的 MSI 安装包。
  • DMG:为 macOS 创建磁盘镜像安装包,提供拖拽安装体验。
  • deb/RPM:分别为 Debian/Ubuntu 和 Red Hat/Fedora Linux 创建安装包。
  • ZIP:创建简单的绿色版压缩包,适合所有平台便携分发。

完整 Maker 配置示例

makers: [
  // Windows
  {
    name: '@electron-forge/maker-squirrel',
    platforms: ['win32'],
    config: {
      name: 'myapp',
      authors: 'My Company',
      exe: 'myapp.exe'
    }
  },
  // macOS
  {
    name: '@electron-forge/maker-dmg',
    platforms: ['darwin'],
    config: {
      format: 'ULFO'
    }
  },
  // Linux
  {
    name: '@electron-forge/maker-deb',
    platforms: ['linux'],
    config: {}
  },
  {
    name: '@electron-forge/maker-rpm',
    platforms: ['linux'],
    config: {}
  },
  // 所有平台 - ZIP 备份
  {
    name: '@electron-forge/maker-zip',
    platforms: ['darwin', 'linux', 'win32']
  }
]

Publisher

Publisher 负责将构建好的安装包发布到各种平台和服务。

GitHub Publisher 是最常用的方式,它可以自动创建 GitHub Release 并上传文件:

publishers: [
  {
    name: '@electron-forge/publisher-github',
    config: {
      repository: {
        owner: 'your-username',
        name: 'your-repo'
      },
      prerelease: false,
      draft: true,
      authToken: process.env.GITHUB_TOKEN
    }
  }
]

需要在环境中设置 GITHUB_TOKEN(具有 repo 权限)。可以通过在推送标签时触发 GitHub Actions 工作流来实现自动发布。

其他 Publisher 包括:

  • S3:上传到 AWS S3 存储桶。
  • Electron Release Server:发布到自托管的更新服务器。
  • Snapcraft:发布到 Ubuntu Snap Store。

发布最佳实践

  1. 遵循语义化版本控制。
  2. 为 macOS 和 Windows 应用进行代码签名和公证。
  3. 配置 electron-updater 以实现应用内自动更新。
  4. 采用分阶段发布(如先 Beta 后 Stable)。
  5. 编写清晰的 Release Notes。
  6. 考虑使用多个 Publisher 进行备份。
0 Answers