云计算百科
云计算领域专业知识百科平台

Electron 小白入门:从零开始构建跨平台桌面应用

一、Electron 概述与环境搭建

1.1 什么是 Electron

Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用的开源框架。它通过将 Chromium 浏览器引擎和 Node.js 运行时嵌入到二进制文件中,允许开发者使用单一的 JavaScript 代码库创建能在 Windows、macOS 和 Linux 上运行的应用程序,无需原生开发经验。简单来说,Electron 让 Web 开发者能够利用他们已有的前端技能来开发桌面应用。

Electron 由 GitHub 开发,最初名为 Atom Shell,现在是 OpenJS 基金会的一个项目,由活跃的社区贡献者维护。它的核心优势在于一次编写,处处运行的跨平台特性,这使得开发者可以用熟悉的技术栈创建桌面应用,而无需为每个操作系统单独编写代码。

1.2 为什么选择 Electron

作为面向小白的 Electron 教程,我们需要明确为什么 Electron 是一个值得学习的框架:

  • 技术栈友好:如果你已经掌握了 JavaScript、HTML 和 CSS,那么学习 Electron 几乎没有额外的学习成本。
  • 跨平台支持:Electron 应用可以在 Windows、macOS 和 Linux 三大主流操作系统上运行,大大提高了开发效率。
  • 强大的生态系统:Electron 拥有丰富的第三方库和工具,可以轻松扩展应用功能,如文件操作、网络请求等。
  • 开源且活跃的社区:作为开源项目,Electron 有一个庞大且活跃的社区支持,这意味着你可以轻松找到资源、教程和解决问题的方法。
  • 企业级应用案例:许多知名的桌面应用,如 VS Code、Atom 编辑器、Discord 等,都是使用 Electron 构建的,证明了其在生产环境中的可靠性。
  • 1.3 环境搭建

    在开始编写第一个 Electron 应用之前,我们需要搭建必要的开发环境。

    1.3.1 安装 Node.js

    Electron 基于 Node.js,因此首先需要安装 Node.js。Node.js 的安装程序会自动安装 npm(Node.js 包管理器),这是我们安装 Electron 和其他依赖的主要工具。

    安装步骤:

  • 访问Node.js 官方网站下载适合你操作系统的安装程序。
  • 运行安装程序,按照提示完成安装。
  • 验证安装是否成功:打开终端(命令提示符或 PowerShell),输入以下命令:
  • node -v

    npm -v

    如果安装成功,这两个命令会分别输出版本号。本书假设你已经安装了 Node.js 20 或更高版本,因为 Electron 在 2025 年初已经将其 npm 生态系统迁移到 Node.js 22 作为最低支持版本。

    1.3.2 安装 Electron

    安装 Electron 有两种主要方式:全局安装和本地安装。对于开发应用,建议本地安装,这样可以更好地控制版本并避免权限问题。

    本地安装步骤:

  • 创建一个新的项目目录:
  • mkdir my-electron-app

    cd my-electron-app

  • 初始化 npm 项目:
  • npm init -y

    这会在项目目录中生成一个package.json文件,用于管理项目依赖和配置。

  • 安装 Electron 作为开发依赖:
  • npm install electron –save-dev

    全局安装(可选):

    如果你想在命令行中直接使用 Electron 命令,可以全局安装 Electron:

    npm install electron -g

    1.3.3 验证 Electron 安装

    安装完成后,可以通过以下步骤验证 Electron 是否正确安装:

  • 在项目目录中创建一个名为main.js的文件,这是 Electron 应用的主进程文件。
  • 在main.js中输入以下代码:
  • const { app, BrowserWindow } = require('electron');

    function createWindow() {

    const win = new BrowserWindow({

    width: 800,

    height: 600,

    webPreferences: {

    nodeIntegration: true

    }

    });

    win.loadFile('index.html');

    }

    app.whenReady().then(createWindow);

    app.on('window-all-closed', () => {

    if (process.platform !== 'darwin') {

    app.quit();

    }

    });

    app.on('activate', () => {

    if (BrowserWindow.getAllWindows().length === 0) {

    createWindow();

    }

    });

  • 创建一个index.html文件,作为应用的主界面:
  • <!DOCTYPE html>

    <html>

    <head>

    <meta charset="UTF-8">

    <title>Hello Electron!</title>

    </head>

    <body>

    <h1>Hello Electron!</h1>

    <p>这是你的第一个Electron应用。</p>

    </body>

    </html>

  • 在package.json的scripts部分添加启动命令:
  • "scripts": {

    "start": "electron ."

    }

  • 启动应用:
  • npm start

    如果一切顺利,你应该会看到一个显示 "Hello Electron!" 的窗口,这表示你的 Electron 环境已经搭建成功。

    1.4 Electron 应用的基本结构

    在深入学习 Electron 的 API 之前,了解 Electron 应用的基本结构是非常重要的。Electron 应用主要由两部分组成:主进程和渲染进程。

    1.4.1 主进程

    主进程是 Electron 应用的入口点,它负责创建和管理应用窗口。在主进程中,你可以使用 Electron 提供的各种模块来控制应用的生命周期、创建窗口、处理系统事件等。

    主进程的特点:

    • 主进程文件(通常是main.js)在应用启动时被执行。
    • 使用app模块管理应用的生命周期。
    • 使用BrowserWindow模块创建和管理应用窗口。
    • 可以访问 Node.js 的所有 API。
    • 主进程中的每个BrowserWindow实例都会在渲染进程中创建一个对应的网页。
    1.4.2 渲染进程

    渲染进程负责显示用户界面,每个BrowserWindow实例都会运行在自己的渲染进程中。渲染进程本质上是一个运行在 Chromium 浏览器中的网页,可以使用标准的 Web 技术(HTML、CSS、JavaScript)来构建用户界面。

    渲染进程的特点:

    • 每个渲染进程都是独立的,它们之间不能直接通信。
    • 默认情况下,渲染进程中的 JavaScript 无法直接访问 Node.js 的 API。
    • 可以通过ipcRenderer和ipcMain模块与主进程进行通信。
    • 可以使用标准的 Web APIs,如 DOM 操作、fetch 等。
    1.4.3 进程间通信

    由于主进程和渲染进程是相互独立的,它们之间的通信需要通过 Electron 提供的 IPC(Inter-Process Communication)机制。

    主要的 IPC 模块有:

    • ipcMain:用于在主进程中监听和发送消息。
    • ipcRenderer:用于在渲染进程中监听和发送消息。
    • remote:提供了一种在渲染进程中直接调用主进程模块的方法,但不推荐在生产环境中使用,因为它可能导致安全问题。

    二、主进程 API 详解

    2.1 app 模块:应用生命周期管理

    app模块是 Electron 中最重要的模块之一,它负责管理应用的生命周期和全局设置。在主进程中,你可以通过const { app } = require('electron')来获取app模块的实例。

    2.1.1 应用生命周期事件

    app模块提供了一系列事件,用于监听应用的不同生命周期阶段:

    app.whenReady()方法

    app.whenReady()返回一个 Promise,当 Electron 完成初始化并准备好创建浏览器窗口时,这个 Promise 会被 resolve。这是在主进程中创建窗口的最佳时机。

    示例:

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

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

    createWindow();

    });

    app.on('window-all-closed', callback)事件

    当所有窗口都被关闭时触发。在这个事件中,你可以决定是否退出应用。

    示例:

    app.on('window-all-closed', () => {

    if (process.platform !== 'darwin') { // 在macOS上,用户通常通过Cmd+Q显式退出

    app.quit();

    }

    });

    app.on('activate', callback)事件

    当应用被激活时触发,通常发生在点击应用图标或从 dock 中选择应用时。在 macOS 上,即使所有窗口都关闭了,应用仍然处于激活状态,这时需要重新创建窗口。

    示例:

    app.on('activate', () => {

    if (BrowserWindow.getAllWindows().length === 0) {

    createWindow();

    }

    });

    app.on('before-quit', callback)事件

    在应用即将退出前触发。你可以在这个事件中执行一些清理工作,如保存数据或关闭文件句柄。

    示例:

    app.on('before-quit', (event) => {

    // 执行清理工作

    console.log('应用即将退出');

    });

    2.1.2 应用信息相关方法

    app模块还提供了一些获取应用信息的方法:

    app.getVersion()方法

    返回应用的版本号。如果在package.json中找不到版本号,就返回当前包或可执行文件的版本。

    示例:

    console.log(`应用版本:${app.getVersion()}`);

    app.getName()方法

    返回应用的名称,默认为package.json中的productName字段的值,如果没有设置,则为package.json中的name字段的值。

    示例:

    console.log(`应用名称:${app.getName()}`);

    app.getPath(name)方法

    返回应用相关的特殊路径,如用户数据目录、缓存目录等。name参数可以是以下值之一:'userData', 'appData', 'cache', 'temp', 'exe', 'home', 'desktop', 'documents', 'downloads', 'music', 'pictures', 'videos'。

    示例:

    console.log(`用户数据目录:${app.getPath('userData')}`);

    console.log(`缓存目录:${app.getPath('cache')}`);

    2.1.3 应用设置方法

    app模块还提供了一些设置应用行为的方法:

    app.setAppUserModelId(id)方法

    在 Windows 上设置应用的用户模型 ID,这会影响任务栏图标和用户体验。这个 ID 应该是一个唯一的字符串,通常采用反向域名格式。

    示例:

    app.setAppUserModelId('com.example.myapp');

    app.setLoginItemSettings(settings)方法

    设置应用是否在系统启动时自动运行。settings是一个对象,可以包含以下属性:openAtLogin(是否在登录时打开)、openAsHidden(是否以隐藏方式打开)、path(要运行的路径,默认为当前应用)、args(要传递的参数数组)。

    示例:

    app.setLoginItemSettings({

    openAtLogin: true,

    openAsHidden: false

    });

    2.2 BrowserWindow 模块:创建和管理窗口

    BrowserWindow模块用于创建和管理应用窗口,是主进程中另一个核心模块。你可以通过const { BrowserWindow } = require('electron')来获取BrowserWindow类。

    2.2.1 创建窗口

    要创建一个窗口,首先需要实例化BrowserWindow类:

    new BrowserWindow([options])构造函数

    options参数是一个可选的对象,用于设置窗口的各种属性。以下是一些常用的窗口属性:

    • width:窗口宽度,默认值为 800 像素。
    • height:窗口高度,默认值为 600 像素。
    • minWidth:窗口最小宽度。
    • minHeight:窗口最小高度。
    • maxWidth:窗口最大宽度。
    • maxHeight:窗口最大高度。
    • x:窗口初始 x 坐标。
    • y:窗口初始 y 坐标。
    • title:窗口标题。
    • icon:窗口图标路径。
    • webPreferences:网页偏好设置,如是否启用 Node.js 集成等。

    示例:创建一个基本窗口

    const win = new BrowserWindow({

    width: 1024,

    height: 768,

    minWidth: 800,

    minHeight: 600,

    title: 'My Electron App',

    webPreferences: {

    nodeIntegration: true // 是否在渲染进程中启用Node.js集成

    }

    });

    2.2.2 窗口加载内容

    创建窗口后,需要为其加载内容。可以通过以下方法加载不同类型的内容:

    win.loadFile(filePath)方法

    加载本地 HTML 文件。filePath是相对于应用根目录的路径。

    示例:

    win.loadFile('index.html');

    win.loadURL(url)方法

    加载远程 URL 或本地文件。对于本地文件,需要使用file://协议。

    示例:

    // 加载远程URL

    win.loadURL('https://www.electronjs.org');

    // 加载本地文件

    win.loadURL('file://' + __dirname + '/index.html');

    win.loadURL(dataUrl)方法

    直接加载 Data URL 格式的内容。

    示例:

    win.loadURL('data:text/html,<h1>Hello World!</h1>');

    2.2.3 窗口操作方法

    BrowserWindow类提供了一系列方法来操作窗口:

    窗口状态控制

    • win.show():显示窗口。
    • win.hide():隐藏窗口。
    • win.toggleVisibility():切换窗口可见性。
    • win.focus():聚焦窗口。
    • win.blur():使窗口失去焦点。
    • win.minimize():最小化窗口。
    • win.maximize():最大化窗口。
    • win.unmaximize():取消最大化窗口。
    • win.restore():恢复窗口到正常大小。
    • win.isMinimized():检查窗口是否最小化。
    • win.isMaximized():检查窗口是否最大化。
    • win.isFocused():检查窗口是否获得焦点。

    窗口尺寸和位置

    • win.setPosition(x, y[, animate]):设置窗口位置。animate参数表示是否平滑过渡。
    • win.getPosition():获取窗口位置,返回一个数组[x, y]。
    • win.setSize(width, height[, animate]):设置窗口大小。
    • win.getSize():获取窗口大小,返回一个数组[width, height]。
    • win.setMinimumSize(width, height):设置窗口最小尺寸。
    • win.setMaximumSize(width, height):设置窗口最大尺寸。
    • win.setResizable(resizable):设置窗口是否可调整大小。
    • win.isResizable():检查窗口是否可调整大小。

    示例:窗口操作综合示例

    // 窗口加载完成后执行一些操作

    win.once('ready-to-show', () => {

    // 显示窗口并聚焦

    win.show();

    win.focus();

    // 设置窗口位置和大小

    win.setPosition(100, 100);

    win.setSize(1200, 800);

    // 设置最小尺寸

    win.setMinimumSize(800, 600);

    // 设置不可调整大小

    win.setResizable(false);

    });

    2.2.4 窗口事件

    BrowserWindow类触发多个事件,可以通过on和once方法来监听这些事件:

    窗口生命周期事件

    • 'ready-to-show':当窗口内容准备好显示时触发。
    • 'closed':当窗口被关闭时触发。
    • 'close':当窗口即将关闭时触发。可以通过event.preventDefault()阻止窗口关闭。
    • 'destroy':当窗口对象被销毁时触发。

    窗口状态变化事件

    • 'focus':当窗口获得焦点时触发。
    • 'blur':当窗口失去焦点时触发。
    • 'maximize':当窗口最大化时触发。
    • 'unmaximize':当窗口取消最大化时触发。
    • 'minimize':当窗口最小化时触发。
    • 'restore':当窗口从最小化或最大化状态恢复时触发。
    • 'move':当窗口位置改变时触发。
    • 'resize':当窗口大小改变时触发。

    示例:窗口事件监听示例

    // 监听窗口关闭事件

    win.on('close', (event) => {

    // 可以在这里询问用户是否保存数据

    const answer = confirm('是否要关闭应用?');

    if (!answer) {

    event.preventDefault(); // 阻止窗口关闭

    }

    });

    // 监听窗口最大化事件

    win.on('maximize', () => {

    console.log('窗口已最大化');

    });

    // 监听窗口恢复事件

    win.on('restore', () => {

    console.log('窗口已恢复');

    });

    // 监听窗口移动事件

    win.on('move', () => {

    console.log(`窗口新位置:${win.getPosition()}`);

    });

    2.2.5 多窗口管理

    在 Electron 应用中,通常需要管理多个窗口。BrowserWindow类提供了一些静态方法来管理所有窗口:

    BrowserWindow.getAllWindows()方法

    返回所有BrowserWindow实例的数组。

    示例:

    const windows = BrowserWindow.getAllWindows();

    console.log(`当前打开的窗口数量:${windows.length}`);

    BrowserWindow.fromId(id)方法

    根据窗口 ID 获取BrowserWindow实例。窗口 ID 是一个整数,每个窗口都有唯一的 ID。

    示例:

    const win = BrowserWindow.fromId(1);

    if (win) {

    win.focus();

    }

    BrowserWindow.getFocusedWindow()方法

    获取当前获得焦点的窗口实例,如果没有则返回null。

    示例:

    const focusedWindow = BrowserWindow.getFocusedWindow();

    if (focusedWindow) {

    console.log(`当前聚焦的窗口标题:${focusedWindow.getTitle()}`);

    }

    2.3 Menu 模块:创建应用菜单

    在桌面应用中,菜单是用户交互的重要组成部分。Electron 提供了Menu模块来创建和管理应用菜单。

    2.3.1 创建菜单

    要创建菜单,首先需要定义菜单模板。菜单模板是一个数组,其中每个元素代表一个菜单项或子菜单。

    菜单项属性

    每个菜单项可以包含以下属性:

    • label:菜单项的文本标签。
    • accelerator:快捷键,如CmdOrCtrl+O。
    • icon:菜单项的图标路径。
    • type:菜单项的类型,可以是'normal', 'separator', 'submenu', 'checkbox', 'radio'。
    • submenu:子菜单,是一个菜单项数组。
    • click:点击菜单项时执行的回调函数。
    • enabled:是否启用菜单项,默认值为true。
    • visible:是否显示菜单项,默认值为true。
    • checked:对于复选框和单选按钮类型的菜单项,是否被选中。

    示例:基本菜单模板

    const template = [

    {

    label: '文件',

    submenu: [

    {

    label: '新建',

    accelerator: 'CmdOrCtrl+N',

    click: () => { /* 处理新建文件 */ }

    },

    {

    label: '打开…',

    accelerator: 'CmdOrCtrl+O',

    click: () => { /* 处理打开文件 */ }

    },

    {

    type: 'separator'

    },

    {

    label: '退出',

    accelerator: 'CmdOrCtrl+Q',

    click: () => { app.quit(); }

    }

    ]

    },

    {

    label: '编辑',

    submenu: [

    {

    label: '剪切',

    accelerator: 'CmdOrCtrl+X',

    click: () => { /* 处理剪切 */ }

    },

    {

    label: '复制',

    accelerator: 'CmdOrCtrl+C',

    click: () => { /* 处理复制 */ }

    },

    {

    label: '粘贴',

    accelerator: 'CmdOrCtrl+V',

    click: () => { /* 处理粘贴 */ }

    }

    ]

    }

    ];

    创建并应用菜单

    定义好菜单模板后,可以使用Menu.buildFromTemplate(template)方法创建菜单对象,然后使用Menu.setApplicationMenu(menu)方法将其设置为应用菜单。

    示例:

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

    const menu = Menu.buildFromTemplate(template);

    Menu.setApplicationMenu(menu);

    2.3.2 上下文菜单

    除了应用菜单外,Electron 还支持上下文菜单(右键菜单)。创建上下文菜单的方法与应用菜单类似,但通常是在需要时动态创建和显示。

    示例:创建并显示上下文菜单

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

    // 定义上下文菜单模板

    const contextMenuTemplate = [

    {

    label: '复制',

    accelerator: 'CmdOrCtrl+C',

    click: () => { /* 处理复制 */ }

    },

    {

    label: '粘贴',

    accelerator: 'CmdOrCtrl+V',

    click: () => { /* 处理粘贴 */ }

    }

    ];

    // 在渲染进程中监听右键点击事件

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

    document.addEventListener('contextmenu', (event) => {

    event.preventDefault(); // 阻止默认的上下文菜单

    ipcRenderer.send('show-context-menu'); // 发送消息给主进程

    });

    // 在主进程中监听消息并显示上下文菜单

    ipcMain.on('show-context-menu', (event) => {

    const menu = Menu.buildFromTemplate(contextMenuTemplate);

    menu.popup({ window: BrowserWindow.getFocusedWindow() }); // 在当前聚焦窗口中显示菜单

    });

    2.3.3 系统默认菜单

    Electron 提供了一些系统默认的菜单项和行为,特别是在 macOS 上,应用菜单通常遵循系统标准。

    macOS 特殊处理

    在 macOS 上,应用菜单通常包含应用名称、编辑菜单、窗口管理菜单等。Electron 会自动为这些标准菜单添加一些默认行为,但你也可以自定义它们。

    示例:macOS 专用菜单项

    if (process.platform === 'darwin') {

    template.unshift({

    label: app.getName(),

    submenu: [

    {

    label: '关于 ' + app.getName(),

    role: 'about'

    },

    {

    type: 'separator'

    },

    {

    label: '退出',

    accelerator: 'Cmd+Q',

    click: () => { app.quit(); }

    }

    ]

    });

    // 编辑菜单

    const editMenu = template.find(item => item.label === '编辑');

    editMenu.submenu.push(

    { type: 'separator' },

    {

    label: '全选',

    accelerator: 'Cmd+A',

    click: () => { /* 处理全选 */ }

    }

    );

    // 窗口菜单

    template.push({

    label: '窗口',

    role: 'window',

    submenu: [

    {

    label: '最小化',

    accelerator: 'Cmd+M',

    role: 'minimize'

    },

    {

    label: '关闭',

    accelerator: 'Cmd+W',

    role: 'close'

    },

    { type: 'separator' },

    {

    label: '全部显示',

    role: 'front'

    }

    ]

    });

    }

    2.4 Tray 模块:系统托盘图标

    在许多桌面应用中,系统托盘(也称为任务栏或 dock)图标是一个常见的功能。Electron 提供了Tray模块来创建和管理系统托盘图标。

    2.4.1 创建系统托盘

    要创建系统托盘,首先需要实例化Tray类,并传入图标路径。

    new Tray(imagePath)构造函数

    imagePath参数是图标的路径,可以是相对路径或绝对路径。在 macOS 上,图标应该是一个.icns 文件;在 Windows 和 Linux 上,可以是.ico 或.png 文件。

    示例:

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

    const tray = new Tray(path.join(__dirname, 'icon.png'));

    2.4.2 设置工具提示和菜单

    创建系统托盘后,可以设置工具提示和上下文菜单。

    tray.setToolTip(tooltip)方法

    设置鼠标悬停时显示的工具提示。

    示例:

    tray.setToolTip('我的Electron应用');

    tray.setContextMenu(menu)方法

    设置右键点击托盘图标时显示的上下文菜单。

    示例:

    const contextMenu = Menu.buildFromTemplate([

    {

    label: '打开应用',

    click: () => { mainWindow.show(); }

    },

    {

    label: '退出',

    click: () => { app.quit(); }

    }

    ]);

    tray.setContextMenu(contextMenu);

    2.4.3 托盘事件

    Tray类触发多个事件,可以通过on和once方法来监听这些事件。

    'click'事件

    当托盘图标被点击时触发,回调函数接收event和bounds参数。bounds是一个对象,包含托盘图标的位置和大小。

    示例:

    tray.on('click', (event, bounds) => {

    // 点击托盘图标时切换窗口显示状态

    if (mainWindow.isVisible()) {

    mainWindow.hide();

    } else {

    mainWindow.show();

    mainWindow.focus();

    }

    });

    'double-click'事件

    当托盘图标被双击时触发,回调参数与'click'事件相同。

    示例:

    tray.on('double-click', () => {

    mainWindow.maximize();

    });

    'right-click'事件

    当托盘图标被右键点击时触发,回调参数与'click'事件相同。

    示例:

    tray.on('right-click', () => {

    // 显示上下文菜单

    contextMenu.popup({ window: mainWindow });

    });

    2.5 Dialog 模块:系统对话框

    系统对话框是桌面应用中与用户交互的重要方式。Electron 提供了Dialog模块来创建各种类型的系统对话框。

    2.5.1 消息对话框

    消息对话框用于向用户显示信息、警告或错误消息,并可以获取用户的反馈。

    dialog.showMessageBox([browserWindow, ]options)方法

    显示一个消息对话框。browserWindow参数是可选的,指定对话框所属的窗口;options参数是一个对象,包含以下属性:

    • type:对话框类型,可以是'none', 'info', 'error', 'question', 'warning'。
    • title:对话框标题。
    • message:对话框的主要消息。
    • detail:对话框的详细内容。
    • buttons:按钮文本数组,默认为['确定']。
    • defaultId:默认选中的按钮索引。
    • cancelId:取消按钮的索引。
    • icon:对话框图标路径。
    • noLink:是否隐藏链接,仅在 macOS 上有效。

    该方法返回一个 Promise,resolve 的值是用户点击的按钮索引。

    示例:基本消息对话框

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

    dialog.showMessageBox({

    type: 'question',

    title: '确认退出',

    message: '你确定要退出应用吗?',

    buttons: ['取消', '确定']

    }).then(result => {

    if (result.response === 1) {

    app.quit();

    }

    });

    2.5.2 文件选择对话框

    文件选择对话框允许用户选择文件或目录。

    dialog.showOpenDialog([browserWindow, ]options)方法

    显示一个打开文件对话框。options参数是一个对象,包含以下属性:

    • title:对话框标题。
    • defaultPath:默认路径。
    • buttonLabel:按钮标签。
    • filters:文件过滤器,是一个数组,每个元素包含name和extensions属性。
    • properties:对话框属性数组,可以包含'openFile', 'openDirectory', 'multiSelections', 'showHiddenFiles', 'createDirectory', 'promptToCreate', 'noResolveAliases', 'dontAddToRecent'。

    该方法返回一个 Promise,resolve 的值是一个对象,包含filePaths属性(选中的文件路径数组)。

    示例:打开文件对话框

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

    dialog.showOpenDialog({

    title: '打开文件',

    defaultPath: app.getPath('documents'),

    filters: [

    { name: '文本文件', extensions: ['txt', 'md'] },

    { name: '所有文件', extensions: ['*'] }

    ],

    properties: ['openFile', 'multiSelections']

    }).then(result => {

    if (!result.canceled) {

    console.log('选中的文件:', result.filePaths);

    }

    });

    dialog.showSaveDialog([browserWindow, ]options)方法

    显示一个保存文件对话框。options参数与showOpenDialog类似,还可以包含defaultPath属性指定默认文件名。

    示例:保存文件对话框

    dialog.showSaveDialog({

    title: '保存文件',

    defaultPath: path.join(app.getPath('documents'), 'new-file.txt'),

    filters: [

    { name: '文本文件', extensions: ['txt'] }

    ]

    }).then(result => {

    if (!result.canceled) {

    console.log('保存路径:', result.filePath);

    }

    });

    2.5.3 其他类型对话框

    除了消息对话框和文件选择对话框外,Dialog模块还提供了其他类型的对话框:

    dialog.showErrorBox(title, content)方法

    显示一个错误消息框,是showMessageBox的简化版,专门用于显示错误信息。

    示例:

    dialog.showErrorBox('错误', '无法打开文件,请检查文件路径是否正确。');

    dialog.showCertificateTrustDialog([browserWindow, ]options)方法

    显示证书信任对话框,用于处理 SSL 证书问题。

    dialog.showMessageBoxSync([browserWindow, ]options)方法

    同步版本的showMessageBox,会阻塞当前进程直到对话框关闭。不建议在主进程中使用,因为会阻塞事件循环。

    三、渲染进程 API 详解

    3.1 ipcRenderer 模块:渲染进程通信

    在 Electron 中,渲染进程和主进程之间需要进行通信,ipcRenderer模块提供了在渲染进程中与主进程通信的能力。你可以通过const { ipcRenderer } = require('electron')来获取ipcRenderer模块的实例。

    3.1.1 发送消息到主进程

    渲染进程可以使用ipcRenderer.send(channel[, …args])方法向主进程发送消息。

    示例:

    // 发送简单消息

    ipcRenderer.send('message-from-renderer', '你好,主进程!');

    // 发送包含数据的消息

    ipcRenderer.send('data-from-renderer', { key: 'value', array: [1, 2, 3] });

    3.1.2 接收主进程消息

    渲染进程可以使用ipcRenderer.on(channel, listener)方法监听来自主进程的消息。

    示例:

    ipcRenderer.on('message-from-main', (event, message) => {

    console.log('收到主进程消息:', message);

    });

    ipcRenderer.on('data-from-main', (event, data) => {

    console.log('收到主进程数据:', data);

    });

    3.1.3 发送同步消息

    在某些情况下,渲染进程需要发送同步消息并立即获取响应。可以使用ipcRenderer.sendSync(channel[, …args])方法。

    示例:

    // 发送同步消息并获取响应

    const response = ipcRenderer.sendSync('sync-message', '请求数据');

    console.log('主进程响应:', response);

    ** 注意:** 同步消息会阻塞渲染进程,可能导致应用卡顿,应谨慎使用。

    3.1.4 一次性监听

    如果只需要监听一次来自主进程的消息,可以使用ipcRenderer.once(channel, listener)方法。

    示例:

    ipcRenderer.once('one-time-message', (event, message) => {

    console.log('一次性消息:', message);

    });

    3.2 remote 模块:直接访问主进程对象

    remote模块允许渲染进程直接访问主进程中的对象,而无需通过ipcRenderer和ipcMain进行显式通信。不过,由于安全和性能考虑,不建议在生产环境中使用remote模块。

    3.2.1 使用 remote 模块

    要使用remote模块,可以通过const { remote } = require('electron')来获取。

    示例:获取主进程中的 BrowserWindow 对象

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

    const BrowserWindow = remote.BrowserWindow;

    // 获取当前窗口的引用

    const currentWindow = BrowserWindow.getFocusedWindow();

    console.log(`当前窗口标题:${currentWindow.getTitle()}`);

    3.2.2 remote 模块的替代方案

    由于remote模块存在安全风险,推荐使用ipcRenderer和ipcMain进行进程间通信。以下是一个替代方案的示例:

    渲染进程代码:

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

    // 发送消息请求窗口标题

    ipcRenderer.send('request-window-title');

    // 监听响应

    ipcRenderer.on('window-title', (event, title) => {

    console.log('窗口标题:', title);

    });

    主进程代码:

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

    ipcMain.on('request-window-title', (event) => {

    const window = BrowserWindow.getFocusedWindow();

    event.sender.send('window-title', window.getTitle());

    });

    3.3 webFrame 模块:操作网页内容

    webFrame模块允许在渲染进程中操作当前网页的框架(frame),包括缩放、执行 JavaScript 代码等。

    3.3.1 网页缩放

    webFrame模块提供了一些方法来控制网页的缩放级别。

    webFrame.setZoomFactor(factor)方法

    设置网页的缩放因子。factor是一个数值,1.0 表示正常大小,2.0 表示双倍大小,0.5 表示一半大小。

    示例:

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

    // 放大50%

    webFrame.setZoomFactor(1.5);

    // 缩小25%

    webFrame.setZoomFactor(0.75);

    webFrame.getZoomFactor()方法

    获取当前的缩放因子。

    示例:

    console.log(`当前缩放因子:${webFrame.getZoomFactor()}`);

    webFrame.setZoomLevel(level)方法

    设置网页的缩放级别。level是一个整数,0 表示正常大小,正数表示放大,负数表示缩小。

    示例:

    // 放大一级

    webFrame.setZoomLevel(1);

    // 缩小两级

    webFrame.setZoomLevel(-2);

    3.3.2 执行 JavaScript 代码

    webFrame模块允许在当前网页的上下文中执行 JavaScript 代码。

    webFrame.executeJavaScript(code[, userGesture, callback])方法

    在当前网页的上下文中执行 JavaScript 代码。userGesture参数表示是否由用户手势触发(如点击事件),callback是执行完成后的回调函数。

    示例:

    webFrame.executeJavaScript(`

    document.body.style.backgroundColor = 'lightblue';

    console.log('背景颜色已改变');

    `, true, (result) => {

    console.log('JavaScript执行结果:', result);

    });

    webFrame.insertCSS(css[, options])方法

    向当前网页插入 CSS 样式。

    示例:

    webFrame.insertCSS(`

    h1 {

    color: red;

    text-decoration: underline;

    }

    `);

    3.3.3 资源加载控制

    webFrame模块还提供了一些方法来控制资源加载行为。

    webFrame.registerURLSchemeAsSecure(scheme)方法

    注册一个 URL 协议为安全协议,使其可以使用https:相关的 API,如XMLHttpRequest和fetch。

    示例:

    webFrame.registerURLSchemeAsSecure('myapp');

    webFrame.registerURLSchemeAsPrivileged(scheme, options)方法

    注册一个 URL 协议为特权协议,使其具有更多权限。

    示例:

    webFrame.registerURLSchemeAsPrivileged('myapp', {

    secure: true,

    standard: true,

    supportFetchAPI: true

    });

    3.4 clipboard 模块:剪贴板操作

    clipboard模块允许在渲染进程中访问系统剪贴板,实现复制、剪切和粘贴功能。

    3.4.1 写入剪贴板

    clipboard模块提供了多种方法向剪贴板写入不同类型的数据。

    clipboard.writeText(text)方法

    向剪贴板写入纯文本数据。

    示例:

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

    clipboard.writeText('选中的文本');

    clipboard.writeHTML(html)方法

    向剪贴板写入 HTML 格式的数据。

    示例:

    clipboard.writeHTML('<strong>加粗文本</strong>');

    clipboard.writeImage(image)方法

    向剪贴板写入图片数据。image参数是一个NativeImage对象。

    示例:

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

    const image = NativeImage.createFromPath('image.png');

    clipboard.writeImage(image);

    clipboard.write(data)方法

    向剪贴板写入多种格式的数据。data参数是一个对象,键是 MIME 类型,值是对应的数据。

    示例:

    clipboard.write({

    'text/plain': '纯文本',

    'text/html': '<p>HTML内容</p>'

    });

    3.4.2 读取剪贴板

    clipboard模块还提供了方法从剪贴板读取不同类型的数据。

    clipboard.readText()方法

    从剪贴板读取纯文本数据。

    示例:

    const text = clipboard.readText();

    console.log('剪贴板中的文本:', text);

    clipboard.readHTML()方法

    从剪贴板读取 HTML 格式的数据。

    示例:

    const html = clipboard.readHTML();

    console.log('剪贴板中的HTML:', html);

    clipboard.readImage()方法

    从剪贴板读取图片数据,返回一个NativeImage对象。

    示例:

    const image = clipboard.readImage();

    if (!image.isEmpty()) {

    image.saveToPath('clipboard-image.png');

    }

    clipboard.read(dataTypes)方法

    从剪贴板读取指定 MIME 类型的数据。dataTypes参数是一个数组,包含要读取的 MIME 类型。

    示例:

    const data = clipboard.read(['text/plain', 'text/html']);

    console.log('剪贴板中的数据:', data);

    3.4.3 剪贴板变化监听

    clipboard模块提供了on方法来监听剪贴板内容的变化。

    clipboard.on('change', (event, type) => { … })事件

    当剪贴板内容发生变化时触发。type参数表示变化的类型,可以是'copy', 'cut', 'paste'或'unknown'。

    示例:

    clipboard.on('change', (event, type) => {

    console.log(`剪贴板内容已改变(类型:${type})`);

    console.log('当前剪贴板文本:', clipboard.readText());

    });

    3.5 screen 模块:屏幕信息和尺寸

    screen模块提供了关于屏幕的信息,如显示器的尺寸、工作区域、缩放因子等。

    3.5.1 获取屏幕信息

    screen模块提供了多种方法获取屏幕相关信息。

    screen.getAllDisplays()方法

    返回所有显示器的信息数组。每个显示器对象包含以下属性:

    • bounds:显示器的边界矩形,包含x, y, width, height。
    • workArea:显示器的工作区域矩形(不包含任务栏等系统元素)。
    • scaleFactor:显示器的缩放因子。
    • id:显示器的唯一标识符。
    • rotation:显示器的旋转角度。

    示例:

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

    const displays = screen.getAllDisplays();

    displays.forEach((display, index) => {

    console.log(`显示器 ${index + 1} 信息:`);

    console.log(` 边界:${display.bounds.x}, ${display.bounds.y}, ${display.bounds.width}x${display.bounds.height}`);

    console.log(` 工作区域:${display.workArea.x}, ${display.workArea.y}, ${display.workArea.width}x${display.workArea.height}`);

    console.log(` 缩放因子:${display.scaleFactor}`);

    });

    screen.getPrimaryDisplay()方法

    返回主显示器的信息对象。

    示例:

    const primaryDisplay = screen.getPrimaryDisplay();

    console.log('主显示器工作区域:', primaryDisplay.workArea);

    3.5.2 窗口位置计算

    screen模块提供了一些方法来帮助计算窗口的位置,使其适应不同的显示器配置。

    screen.getCursorScreenPoint()方法

    返回当前鼠标指针在屏幕坐标系中的位置。

    示例:

    const cursorPosition = screen.getCursorScreenPoint();

    console.log(`鼠标位置:${cursorPosition.x}, ${cursorPosition.y}`);

    screen.getDisplayNearestPoint(point)方法

    返回离指定点最近的显示器信息对象。

    示例:

    const point = { x: 100, y: 100 };

    const display = screen.getDisplayNearestPoint(point);

    console.log(`离点 (100, 100) 最近的显示器工作区域:`, display.workArea);

    screen.getWorkingAreaOfCursor()方法

    返回当前鼠标指针所在显示器的工作区域。

    示例:

    const workingArea = screen.getWorkingAreaOfCursor();

    console.log(`鼠标所在显示器的工作区域:${workingArea.x}, ${workingArea.y}, ${workingArea.width}x${workingArea.height}`);

    四、进程间通信与高级主题

    4.1 ipcMain 模块:主进程通信

    ipcMain模块用于在主进程中监听和处理来自渲染进程的消息,是实现进程间通信的关键模块之一。

    4.1.1 监听消息

    主进程可以使用ipcMain.on(channel, listener)方法监听来自渲染进程的消息。

    示例:

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

    ipcMain.on('message-from-renderer', (event, message) => {

    console.log('收到渲染进程消息:', message);

    // 发送响应消息

    event.sender.send('reply-to-renderer', '你好,渲染进程!');

    });

    event参数说明

    listener回调函数的第一个参数是event对象,它包含以下属性和方法:

    • event.sender:发送消息的渲染进程对象(WebContents实例)。
    • event.preventDefault():阻止默认行为(如果有的话)。
    • event.returnValue:设置同步消息的返回值。
    • event.send(channel[, …args]):向发送者发送消息。
    4.1.2 发送消息到渲染进程

    主进程可以通过event.sender.send(channel[, …args])方法向发送消息的渲染进程发送响应。

    示例:

    ipcMain.on('request-data', (event) => {

    const data = fetchData(); // 获取数据的函数

    event.sender.send('data-reply', data);

    });

    如果需要向所有渲染进程发送消息,可以使用BrowserWindow.getAllWindows()方法获取所有窗口,然后遍历发送。

    示例:

    ipcMain.on('broadcast-message', (event, message) => {

    BrowserWindow.getAllWindows().forEach(window => {

    window.webContents.send('global-message', message);

    });

    });

    4.1.3 处理同步消息

    主进程可以使用ipcMain.handle(channel, listener)方法处理同步消息请求。

    示例:

    ipcMain.handle('sync-message', (event, arg) => {

    console.log('收到同步消息:', arg);

    return '同步响应';

    });

    在渲染进程中,可以使用ipcRenderer.invoke(channel[, …args])方法发送同步消息并获取响应。

    示例:

    const response = await ipcRenderer.invoke('sync-message', '同步请求');

    console.log('同步响应:', response);

    4.2 跨进程数据传递

    在 Electron 应用中,经常需要在主进程和渲染进程之间传递数据。本节将介绍几种常见的数据传递方式及其注意事项。

    4.2.1 基本数据类型传递

    基本数据类型(如字符串、数字、布尔值)可以直接在进程间传递,Electron 会自动进行序列化和反序列化。

    示例:

    // 主进程

    ipcMain.on('send-number', (event, number) => {

    console.log('收到数字:', number);

    event.sender.send('reply-number', number * 2);

    });

    // 渲染进程

    ipcRenderer.send('send-number', 42);

    ipcRenderer.on('reply-number', (event, result) => {

    console.log('计算结果:', result); // 输出 84

    });

    4.2.2 对象和数组传递

    复杂数据类型(如对象和数组)也可以在进程间传递,但需要注意以下几点:

  • 数据会被深拷贝,因此修改接收方的数据不会影响发送方的数据。
  • 原型链和函数不会被保留。
  • 循环引用会被自动处理。
  • 示例:

    // 主进程

    ipcMain.on('send-object', (event, obj) => {

    console.log('收到对象:', obj);

    obj.answer = 42; // 修改对象不会影响发送方

    event.sender.send('reply-object', obj);

    });

    // 渲染进程

    const data = { question: '生命、宇宙以及一切的终极答案' };

    ipcRenderer.send('send-object', data);

    ipcRenderer.on('reply-object', (event, result) => {

    console.log('修改后的对象:', result); // 输出 { question: '…', answer: 42 }

    console.log('原始对象:', data); // 输出 { question: '…' }

    });

    4.2.3 缓冲区传递

    对于二进制数据,如文件内容或图像数据,可以使用Buffer对象进行传递。Buffer对象在进程间传递时不会被拷贝,而是通过引用传递,这提高了性能并减少了内存使用。

    示例:

    // 主进程

    const fs = require('fs');

    ipcMain.on('read-file', (event, filePath) => {

    try {

    const buffer = fs.readFileSync(filePath);

    event.sender.send('file-content', buffer);

    } catch (error) {

    event.sender.send('file-error', error.message);

    }

    });

    // 渲染进程

    ipcRenderer.send('read-file', 'data.txt');

    ipcRenderer.on('file-content', (event, buffer) => {

    const content = buffer.toString();

    console.log('文件内容:', content);

    });

    4.2.4 大型数据传递注意事项

    传递大型数据时需要注意以下几点:

  • 避免频繁传递大型数据,这可能导致性能问题。
  • 对于非常大的文件,考虑使用流式传输而不是一次性传递整个Buffer。
  • 在渲染进程中处理大型数据时,注意不要阻塞主线程。
  • 4.3 远程模块的替代方案

    remote模块允许渲染进程直接访问主进程中的对象,但它存在一些安全和性能问题。本节将介绍如何使用ipcRenderer和ipcMain实现相同的功能。

    4.3.1 简单方法调用

    假设在主进程中有一个createWindow()函数,在渲染进程中需要调用它。使用remote模块可以直接调用:

    使用 remote 模块:

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

    remote.createWindow();

    替代方案:

    在主进程中:

    ipcMain.on('create-window', () => {

    createWindow();

    });

    在渲染进程中:

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

    ipcRenderer.send('create-window');

    4.3.2 带参数的方法调用

    如果方法需要参数,可以通过消息传递参数:

    使用 remote 模块:

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

    remote.createWindow({ width: 1024, height: 768 });

    替代方案:

    在主进程中:

    ipcMain.on('create-window', (event, options) => {

    createWindow(options);

    });

    在渲染进程中:

    const options = { width: 1024, height: 768 };

    ipcRenderer.send('create-window', options);

    4.3.3 获取返回值

    对于有返回值的方法,需要使用异步消息传递:

    使用 remote 模块:

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

    const count = remote.getWindowCount();

    console.log(`窗口数量:${count}`);

    替代方案:

    在主进程中:

    ipcMain.handle('get-window-count', () => {

    return BrowserWindow.getAllWindows().length;

    });

    在渲染进程中:

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

    ipcRenderer.invoke('get-window-count').then(count => {

    console.log(`窗口数量:${count}`);

    });

    4.4 应用打包与分发

    开发完成后,需要将 Electron 应用打包成可执行文件,以便用户安装和使用。Electron 提供了多种工具和方法来实现这一点。

    4.4.1 使用 Electron Forge 打包

    Electron Forge 是 Electron 官方推荐的打包工具,它提供了一站式的解决方案,包括打包、签名、发布等功能。

    安装 Electron Forge:

    npm install –save-dev electron-forge

    初始化 Electron Forge 项目:

    npx electron-forge import

    这会自动检测项目类型并进行配置。

    配置打包选项:

    在package.json中,可以配置 Electron Forge 的打包选项:

    "config": {

    "forge": {

    "packagerConfig": {

    "asar": true,

    "icon": "./build/icon"

    },

    "makers": [

    {

    "name": "@electron-forge/maker-squirrel",

    "config": {

    "name": "my-electron-app"

    }

    },

    {

    "name": "@electron-forge/maker-zip",

    "platforms": [

    "darwin"

    ]

    },

    {

    "name": "@electron-forge/maker-deb",

    "config": {}

    },

    {

    "name": "@electron-forge/maker-rpm",

    "config": {}

    }

    ]

    }

    }

    打包应用:

    npm run make

    这会在out目录中生成适用于不同平台的安装包。

    4.4.2 使用 Electron Packager

    Electron Packager 是另一个常用的打包工具,它可以将 Electron 应用打包成适用于不同平台的可执行文件。

    安装 Electron Packager:

    npm install –save-dev electron-packager

    配置打包脚本:

    在package.json的scripts部分添加打包命令:

    "scripts": {

    "package:win32": "electron-packager . my-app –platform=win32 –arch=x64 –icon=build/icon.ico –prune=true –out=release-builds",

    "package:darwin": "electron-packager . my-app –platform=darwin –arch=x64 –icon=build/icon.icns –prune=true –out=release-builds",

    "package:linux": "electron-packager . my-app –platform=linux –arch=x64 –icon=build/icon.png –prune=true –out=release-builds"

    }

    执行打包:

    npm run package:win32

    npm run package:darwin

    npm run package:linux

    这会在release-builds目录中生成适用于不同平台的可执行文件。

    4.4.3 自动更新

    为了让用户能够方便地更新应用,可以集成自动更新功能。Electron 提供了electron-updater模块来实现这一点。

    安装 electron-updater:

    npm install –save-dev electron-updater

    主进程中添加自动更新代码:

    const { autoUpdater } = require('electron-updater');

    autoUpdater.checkForUpdatesAndNotify();

    autoUpdater.on('update-downloaded', () => {

    dialog.showMessageBox({

    type: 'info',

    title: '更新可用',

    message: '应用更新已下载,是否重启应用以安装更新?'

    }).then(result => {

    if (result.response === 0) {

    autoUpdater.quitAndInstall();

    }

    });

    });

    autoUpdater.on('error', (error) => {

    dialog.showErrorBox('更新错误', error.message);

    });

    配置更新服务器:

    需要将应用的更新文件上传到一个 HTTP 服务器,并在package.json中配置更新地址:

    "build": {

    "publish": [

    {

    "provider": "generic",

    "url": "https://example.com/update"

    }

    ]

    }

    五、实战案例:构建简单的桌面工具

    5.1 文本编辑器应用

    在本节中,我们将使用 Electron 构建一个简单的文本编辑器应用,演示如何综合运用前面学习的 API。

    5.1.1 应用功能规划

    我们的文本编辑器将具备以下基本功能:

  • 新建文件
  • 打开文件
  • 保存文件
  • 另存为
  • 基本的编辑菜单(剪切、复制、粘贴)
  • 简单的窗口管理
  • 5.1.2 主进程代码

    首先,创建主进程文件main.js:

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

    const path = require('path');

    let mainWindow;

    function createWindow() {

    mainWindow = new BrowserWindow({

    width: 1024,

    height: 768,

    webPreferences: {

    nodeIntegration: true

    }

    });

    mainWindow.loadFile('index.html');

    mainWindow.on('closed', () => {

    mainWindow = null;

    });

    // 创建菜单

    const template = [

    {

    label: '文件',

    submenu: [

    {

    label: '新建',

    accelerator: 'CmdOrCtrl+N',

    click: () => { newFile(); }

    },

    {

    label: '打开…',

    accelerator: 'CmdOrCtrl+O',

    click: () => { openFile(); }

    },

    {

    label: '保存',

    accelerator: 'CmdOrCtrl+S',

    click: () => { saveFile(); }

    },

    {

    label: '另存为…',

    accelerator: 'Shift+CmdOrCtrl+S',

    click: () => { saveFileAs(); }

    },

    { type: 'separator' },

    {

    label: '退出',

    accelerator: 'CmdOrCtrl+Q',

    click: () => { app.quit(); }

    }

    ]

    },

    {

    label: '编辑',

    submenu: [

    {

    label: '剪切',

    accelerator: 'CmdOrCtrl+X',

    click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("cut")'); }

    },

    {

    label: '复制',

    accelerator: 'CmdOrCtrl+C',

    click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("copy")'); }

    },

    {

    label: '粘贴',

    accelerator: 'CmdOrCtrl+V',

    click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("paste")'); }

    },

    { type: 'separator' },

    {

    label: '全选',

    accelerator: 'CmdOrCtrl+A',

    click: () => { mainWindow.webContents.executeJavaScript('document.execCommand("selectAll")'); }

    }

    ]

    }

    ];

    const menu = Menu.buildFromTemplate(template);

    Menu.setApplicationMenu(menu);

    }

    // 文件操作相关函数

    let currentFilePath = null;

    function newFile() {

    currentFilePath = null;

    mainWindow.webContents.send('new-file');

    }

    function openFile() {

    dialog.showOpenDialog({

    filters: [{ name: '文本文件', extensions: ['txt'] }]

    }).then(result => {

    if (!result.canceled && result.filePaths.length > 0) {

    const filePath = result.filePaths[0];

    const content = require('fs').readFileSync(filePath, 'utf8');

    mainWindow.webContents.send('open-file', filePath, content);

    currentFilePath = filePath;

    }

    });

    }

    function saveFile() {

    if (currentFilePath) {

    const content = mainWindow.webContents.executeJavaScript('document.getElementById("editor").value');

    require('fs').writeFileSync(currentFilePath, content);

    } else {

    saveFileAs();

    }

    }

    function saveFileAs() {

    dialog.showSaveDialog({

    filters: [{ name: '文本文件', extensions: ['txt'] }]

    }).then(result => {

    if (!result.canceled) {

    const filePath = result.filePath;

    const content = mainWindow.webContents.executeJavaScript('document.getElementById("editor").value');

    require('fs').writeFileSync(filePath, content);

    currentFilePath = filePath;

    }

    });

    }

    app.whenReady().then(createWindow);

    app.on('window-all-closed', () => {

    if (process.platform !== 'darwin') {

    app.quit();

    }

    });

    app.on('activate', () => {

    if (mainWindow === null) {

    createWindow();

    }

    });

    5.1.3 渲染进程代码

    创建渲染进程文件index.html和对应的 JavaScript 文件renderer.js:

    index.html:

    <!DOCTYPE html>

    <html>

    <head>

    <meta charset="UTF-8">

    <title>Electron文本编辑器</title>

    <style>

    body { margin: 0; padding: 0; }

    #editor { width: 100vw; height: 100vh; border: none; padding: 10px; box-sizing: border-box; font-size: 16px; }

    </style>

    </head>

    <body>

    <textarea id="editor"></textarea>

    <script src="renderer.js"></script>

    </body>

    </html>

    renderer.js:

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

    // 监听新建文件消息

    ipcRenderer.on('new-file', () => {

    document.getElementById('editor').value = '';

    });

    // 监听打开文件消息

    ipcRenderer.on('open-file', (event, filePath, content) => {

    document.title = `Electron文本编辑器 – ${path.basename(filePath)}`;

    document.getElementById('editor').value = content;

    });

    // 处理窗口关闭时的确认

    window.addEventListener('beforeunload', (event) => {

    const content = document.getElementById('editor').value;

    if (content.trim() !== '') {

    event.returnValue = '你有未保存的更改,确定要退出吗?';

    }

    });

    5.1.4 运行和测试

    在项目目录中执行以下命令:

    npm install electron –save-dev

    npm start

    测试应用的各项功能:

  • 新建文件(Ctrl+N)
  • 打开文件(Ctrl+O)
  • 保存文件(Ctrl+S)
  • 另存为(Shift+Ctrl+S)
  • 基本编辑操作(剪切、复制、粘贴、全选)
  • 退出应用(Ctrl+Q)
  • 5.2 文件管理器应用

    接下来,我们将构建一个简单的文件管理器应用,展示如何使用 Electron 的文件系统和对话框 API。

    5.2.1 应用功能规划

    我们的文件管理器将具备以下基本功能:

  • 显示当前目录内容
  • 导航到上级目录
  • 打开目录
  • 显示文件信息
  • 基本文件操作(新建文件、删除文件 / 目录)
  • 5.2.2 主进程代码

    创建主进程文件main.js:

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

    const path = require('path');

    const fs = require('fs');

    let mainWindow;

    function createWindow() {

    mainWindow = new BrowserWindow({

    width: 1200,

    height: 800,

    webPreferences: {

    nodeIntegration: true

    }

    });

    mainWindow.loadFile('index.html');

    mainWindow.on('closed', () => {

    mainWindow = null;

    });

    }

    app.whenReady().then(createWindow);

    app.on('window-all-closed', () => {

    if (process.platform !== 'darwin') {

    app.quit();

    }

    });

    app.on('activate', () => {

    if (mainWindow === null) {

    createWindow();

    }

    });

    5.2.3 渲染进程代码

    创建渲染进程文件index.html和对应的 JavaScript 文件renderer.js:

    index.html:

    <!DOCTYPE html>

    <html>

    <head>

    <meta charset="UTF-8">

    <title>Electron文件管理器</title>

    <style>

    body { margin: 0; padding: 0; font-family: Arial, sans-serif; }

    .header { background-color: #f0f0f0; padding: 10px; }

    .breadcrumb { margin: 10px; }

    .file-list { list-type: none; padding: 0; margin: 10px; }

    .file-item { padding: 5px; border: 1px solid #ddd; margin-bottom: 5px; cursor: pointer; }

    .file-item:hover { background-color: #f5f5f5; }

    .file-info { margin: 10px; padding: 10px; border: 1px solid #ddd; }

    </style>

    </head>

    <body>

    <div class="header">

    <button id="back-btn">返回</button>

    <button id="new-file-btn">新建文件</button>

    <button id="delete-btn">删除</button>

    </div>

    <div class="breadcrumb" id="breadcrumb"></div>

    <ul class="file-list" id="file-list"></ul>

    <div class="file-info" id="file-info"></div>

    <script src="renderer.js"></script>

    </body>

    </html>

    renderer.js:

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

    const fs = require('fs');

    const path = require('path');

    let currentPath = remote.app.getPath('documents'); // 初始路径为用户文档目录

    // 初始化文件列表

    updateFileList(currentPath);

    // 监听返回按钮点击

    document.getElementById('back-btn').addEventListener('click', () => {

    const parentPath = path.dirname(currentPath);

    if (parentPath !== currentPath) {

    currentPath = parentPath;

    updateFileList(currentPath);

    }

    });

    // 监听新建文件按钮点击

    document.getElementById('new-file-btn').addEventListener('click', () => {

    const fileName = prompt('请输入新文件名:');

    if (fileName) {

    const filePath = path.join(currentPath, fileName);

    fs.writeFileSync(filePath, ''); // 创建空文件

    updateFileList(currentPath);

    }

    });

    // 监听文件列表点击事件

    document.getElementById('file-list').addEventListener('click', (event) => {

    if (event.target.classList.contains('file-item')) {

    const fileName = event.target.dataset.filename;

    const filePath = path.join(currentPath, fileName);

    if (fs.statSync(filePath).isDirectory()) {

    currentPath = filePath;

    updateFileList(currentPath);

    } else {

    showFileInfo(filePath);

    }

    }

    });

    // 更新文件列表

    function updateFileList(path) {

    const fileList = document.getElementById('file-list');

    fileList.innerHTML = '';

    // 更新面包屑导航

    const breadcrumb = document.getElementById('breadcrumb');

    breadcrumb.innerHTML = generateBreadcrumb(path);

    try {

    const files = fs.readdirSync(path, { withFileTypes: true });

    files.forEach(file => {

    const fileItem = document.createElement('li');

    fileItem.className = 'file-item';

    fileItem.dataset.filename = file.name;

    fileItem.innerHTML = `

    <span>${file.name}</span>

    <span>${file.isDirectory() ? '目录' : '文件'}</span>

    `;

    fileList.appendChild(fileItem);

    });

    } catch (error) {

    alert('无法读取目录:' + error.message);

    }

    }

    // 生成面包屑导航

    function generateBreadcrumb(path) {

    const parts = path.split(path.sep);

    return parts.map((part, index) => {

    const href = parts.slice(0, index + 1).join(path.sep);

    return `<a href="#" onclick="navigateTo('${href}')">${part}</a>`;

    }).join(' / ');

    }

    // 导航到指定路径

    window.navigateTo = function(href) {

    currentPath = href;

    updateFileList(currentPath);

    };

    // 显示文件信息

    function showFileInfo(filePath) {

    const fileInfo = document.getElementById('file-info');

    try {

    const stats = fs.statSync(filePath);

    fileInfo.innerHTML = `

    <p><strong>文件名:</strong> ${path.basename(filePath)}</p>

    <p><strong>路径:</strong> ${filePath}</p>

    <p><strong>类型:</strong> ${stats.isDirectory() ? '目录' : '文件'}</p>

    <p><strong>大小:</strong> ${formatSize(stats.size)}</p>

    <p><strong>创建时间:</strong> ${stats.birthtime.toLocaleString()}</p>

    <p><strong>修改时间:</strong> ${stats.mtime.toLocaleString()}</p>

    `;

    } catch (error) {

    fileInfo.innerHTML = '无法获取文件信息:' + error.message;

    }

    }

    // 格式化文件大小

    function formatSize(bytes) {

    if (bytes < 1024) return `${bytes} B`;

    else if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;

    else if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;

    else return `${(bytes / 1073741824).toFixed(1)} GB`;

    }

    5.2.4 运行和测试

    在项目目录中执行以下命令:

    npm install electron –save-dev

    npm start

    测试应用的各项功能:

  • 导航目录(点击文件或目录)
  • 返回上级目录(点击 "返回" 按钮)
  • 新建文件(点击 "新建文件" 按钮)
  • 查看文件信息(点击文件)
  • 基本文件操作(后续可以添加更多功能)
  • 5.3 图像查看器应用

    最后,我们将构建一个简单的图像查看器应用,展示如何使用 Electron 的图像处理和文件操作 API。

    5.3.1 应用功能规划

    我们的图像查看器将具备以下基本功能:

  • 打开图像文件
  • 显示图像
  • 缩放图像
  • 旋转图像
  • 保存修改后的图像
  • 5.3.2 主进程代码

    创建主进程文件main.js:

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

    const path = require('path');

    let mainWindow;

    function createWindow() {

    mainWindow = new BrowserWindow({

    width: 800,

    height: 600,

    webPreferences: {

    nodeIntegration: true

    }

    });

    mainWindow.loadFile('index.html');

    mainWindow.on('closed', () => {

    mainWindow = null;

    });

    }

    app.whenReady().then(createWindow);

    app.on('window-all-closed', () => {

    if (process.platform !== 'darwin') {

    app.quit();

    }

    });

    app.on('activate', () => {

    if (mainWindow === null) {

    createWindow();

    }

    });

    5.3.3 渲染进程代码

    创建渲染进程文件index.html和对应的 JavaScript 文件renderer.js:

    index.html:

    <!DOCTYPE html>

    <html>

    <head>

    <meta charset="UTF-8">

    <title>Electron图像查看器</title>

    <style>

    body { margin: 0; padding: 0; }

    .toolbar { background-color: #f0f0f0; padding: 10px; }

    #image-container { display: flex; justify-content: center; align-items: center; height: calc(100vh – 40px); }

    #image { max-width: 100%; max-height: 100%; }

    </style>

    </head>

    <body>

    <div class="toolbar">

    <button id="open-btn">打开图像</button>

    <button id="save-btn">保存</button>

    <button id="zoom-in-btn">放大</button>

    <button id="zoom-out-btn">缩小</button>

    <button id="rotate-btn">旋转</button>

    </div>

    <div id="image-container">

    <img id="image" src="">

    </div>

    <script src="renderer.js"></script>

    </body>

    </html>

    renderer.js:

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

    const fs = require('fs');

    const path = require('path');

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

    let currentImagePath = null;

    let image = null;

    let zoomFactor = 1;

    let rotation = 0;

    // 初始化

    document.getElementById('image').style.transform = `scale(${zoomFactor}) rotate(${rotation}deg)`;

    // 监听打开图像按钮点击

    document.getElementById('open-btn').addEventListener('click', () => {

    ipcRenderer.invoke('open-image').then(filePath => {

    if (filePath) {

    currentImagePath = filePath;

    image = nativeImage.createFromPath(currentImagePath);

    document.getElementById('image').src = image.toDataURL();

    }

    });

    });

    // 监听保存按钮点击

    document.getElementById('save-btn').addEventListener('click', () => {

    if (image) {

    ipcRenderer.invoke('save-image', currentImagePath, image).then(success => {

    if (success) {

    alert('图像已保存');

    } else {

    alert('保存失败');

    }

    });

    }

    });

    // 监听放大按钮点击

    document.getElementById('zoom-in-btn').addEventListener('click', () => {

    zoomFactor *= 1.2;

    updateImageDisplay();

    });

    // 监听缩小按钮点击

    document.getElementById('zoom-out-btn').addEventListener('click', () => {

    zoomFactor /= 1.2;

    updateImageDisplay();

    });

    // 监听旋转按钮点击

    document.getElementById('rotate-btn').addEventListener('click', () => {

    rotation += 90;

    updateImageDisplay();

    });

    // 更新图像显示

    function updateImageDisplay() {

    const imageElement = document.getElementById('image');

    imageElement.style.transform = `scale(${zoomFactor}) rotate(${rotation}deg)`;

    }

    // 处理窗口关闭时的确认

    window.addEventListener('beforeunload', (event) => {

    if (image) {

    event.returnValue = '你有未保存的更改,确定要退出吗?';

    }

    });

    // 主进程通信

    ipcRenderer.on('open-image', (event, filePath) => {

    if (filePath) {

    currentImagePath = filePath;

    image = nativeImage.createFromPath(currentImagePath);

    document.getElementById('image').src = image.toDataURL();

    }

    });

    ipcRenderer.handle('open-image', () => {

    return dialog.showOpenDialog({

    filters: [{ name: '图像文件', extensions: ['png', 'jpg', 'jpeg', 'gif', 'bmp'] }]

    }).then(result => {

    if (!result.canceled && result.filePaths.length > 0) {

    return result.filePaths[0];

    } else {

    return null;

    }

    });

    });

    ipcRenderer.handle('save-image', (event, filePath, image) => {

    return new Promise((resolve) => {

    if (!filePath) {

    dialog.showSaveDialog({

    filters: [{ name: '图像文件', extensions: ['png'] }]

    }).then(result => {

    if (!result.canceled) {

    filePath = result.filePath;

    image.writePNG(filePath, () => {

    resolve(true);

    });

    } else {

    resolve(false);

    }

    });

    } else {

    image.writePNG(filePath, () => {

    resolve(true);

    });

    }

    });

    });

    5.3.4 运行和测试

    在项目目录中执行以下命令:

    npm install electron –save-dev

    npm start

    测试应用的各项功能:

  • 打开图像文件(支持 PNG、JPG 等常见格式)
  • 放大 / 缩小图像(点击相应按钮)
  • 旋转图像(点击旋转按钮)
  • 保存修改后的图像(点击保存按钮)
  • 六、总结与进阶学习

    6.1 本书内容回顾

    在本书中,我们从零开始学习了 Electron 框架的基础知识和核心 API,包括:

  • 环境搭建:安装 Node.js 和 Electron,创建第一个 Electron 应用。
  • 主进程 API:管理应用生命周期的app模块,创建和管理窗口的BrowserWindow模块,创建菜单的Menu模块,系统托盘图标的Tray模块,以及系统对话框的Dialog模块。
  • 渲染进程 API:进程间通信的ipcRenderer模块,直接访问主进程对象的remote模块,操作网页内容的webFrame模块,剪贴板操作的clipboard模块,以及屏幕信息的screen模块。
  • 进程间通信:使用ipcMain和ipcRenderer实现主进程和渲染进程之间的通信,传递各种类型的数据,以及remote模块的替代方案。
  • 应用打包与分发:使用 Electron Forge 和 Electron Packager 打包应用,实现自动更新功能。
  • 实战案例:构建了文本编辑器、文件管理器和图像查看器三个实际应用,综合运用所学的 API。
  • 6.2 最佳实践与注意事项

    在使用 Electron 开发应用时,需要遵循以下最佳实践和注意事项:

  • 安全考虑:
      • 避免在渲染进程中使用remote模块,优先使用ipcRenderer和ipcMain进行通信。
      • 谨慎启用 Node.js 集成(nodeIntegration),只在必要时使用。
      • 对来自用户的输入进行安全处理,防止代码注入攻击。
  • 性能优化:
      • 避免在主进程中执行阻塞操作,使用异步 API 或工作线程。
      • 对于大型数据传递,考虑使用Buffer对象或流式传输。
      • 合理管理窗口资源,及时销毁不再使用的窗口。
  • 跨平台兼容性:
      • 测试应用在不同操作系统上的行为和外观。
      • 使用process.platform检测当前平台,为不同平台提供适当的处理。
      • 遵循各平台的用户界面规范,如 macOS 的菜单栏和 Windows 的任务栏。
  • 代码组织:
      • 将主进程和渲染进程的代码分离,保持代码清晰。
      • 使用事件驱动架构,避免紧密耦合。
      • 为 Electron API 调用添加错误处理,提高应用的稳定性。

    6.3 进阶学习资源

    如果你希望进一步深入学习 Electron,以下是一些推荐的资源:

  • 官方文档:Electron 的官方文档是最权威的学习资源,包含了所有 API 的详细说明和示例。
  • Electron Fiddle:Electron 官方提供的实验工具,可以快速创建和测试 Electron 代码片段。
  • Electron API JSON:Electron 的每个新版本都会提供一个描述所有公共 API 的 JSON 文件,可用于开发工具和文档生成。
  • 社区资源:Electron 社区活跃,有大量的教程、博客和开源项目可供学习。
  • 高级主题:如使用 TypeScript 开发 Electron 应用,集成 React、Vue 等前端框架,以及使用 Native API 扩展 Electron 功能。
  • 6.4 结语

    Electron 为 Web 开发者提供了一个强大的工具,使他们能够利用现有的技能创建跨平台桌面应用。通过本书的学习,你应该已经掌握了 Electron 的基础知识和核心 API,能够开发简单的桌面工具应用。

    记住,学习任何框架的最佳方法是实践。尝试将本书中学到的知识应用到实际项目中,解决实际问题。随着你对 Electron 的深入理解,你会发现它的潜力远远不止于此,能够创建出功能更强大、体验更丰富的桌面应用。

    希望本书能够成为你学习 Electron 的起点,帮助你开启桌面应用开发的新旅程!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Electron 小白入门:从零开始构建跨平台桌面应用
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!