MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js图形界面开发Electron入门

2023-11-105.8k 阅读

一、Electron 简介

1.1 什么是 Electron

Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台桌面应用程序的开源框架。它基于 Chromium 和 Node.js,使得前端开发者可以利用他们现有的 web 开发技能来创建桌面应用。简单来说,Electron 允许你像开发网页一样开发桌面应用,将网页技术与桌面应用的能力相结合。

传统桌面应用开发往往需要使用特定于操作系统的语言和工具,例如 Windows 平台的 C++ 和 MFC,Mac 平台的 Objective - C 和 Cocoa。这意味着开发者需要针对不同操作系统编写不同代码,开发成本较高。而 Electron 则提供了一种统一的开发方式,一套代码可以在 Windows、Mac 和 Linux 上运行。

1.2 Electron 的工作原理

Electron 应用由一个主进程和多个渲染进程组成。主进程使用 Node.js 运行,负责创建和管理浏览器窗口,以及与操作系统进行交互。渲染进程运行在 Chromium 环境中,用于显示应用的用户界面,本质上就是一个网页。主进程和渲染进程之间通过 IPC(Inter - Process Communication)机制进行通信,实现数据传递和功能调用。

例如,主进程可以调用 Node.js 的文件系统模块读取本地文件,然后通过 IPC 将文件内容传递给渲染进程,渲染进程将其显示在页面上。

二、环境搭建

2.1 安装 Node.js

首先,确保你已经安装了 Node.js。你可以从 Node.js 官方网站(https://nodejs.org/)下载适合你操作系统的安装包进行安装。安装完成后,打开终端(Windows 下为命令提示符或 PowerShell),输入以下命令检查 Node.js 是否安装成功:

node -v

如果输出版本号,说明 Node.js 安装成功。

2.2 安装 Electron

全局安装 Electron 可以使用 npm(Node Package Manager,Node.js 自带的包管理工具):

npm install -g electron

如果你使用的是 yarn,也可以通过以下命令安装:

yarn global add electron

安装完成后,你可以通过以下命令检查 Electron 是否安装成功:

electron -v

2.3 创建第一个 Electron 项目

  1. 创建一个新目录作为项目根目录,例如 my - electron - app
mkdir my - electron - app
cd my - electron - app
  1. 初始化项目,这会生成一个 package.json 文件,用于管理项目的依赖和配置:
npm init -y
  1. 安装 Electron 作为项目的本地依赖(虽然已经全局安装,但项目仍需本地依赖):
npm install electron --save - dev
  1. 在项目根目录下创建一个 main.js 文件,这将是 Electron 应用的主进程文件。在 main.js 中输入以下代码:
const electron = require('electron');
const { app, BrowserWindow } = electron;

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

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

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

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

app.on('window - all - closed', () => {
    if (process.platform!== 'darwin') {
        app.quit();
    }
});
  1. 在项目根目录下创建一个 index.html 文件,作为应用的初始页面:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>My Electron App</title>
</head>

<body>
    <h1>Hello, Electron!</h1>
</body>

</html>
  1. 打开 package.json 文件,在 scripts 字段中添加启动脚本:
{
    "scripts": {
        "start": "electron. "
    }
}

现在,在项目根目录下运行以下命令启动应用:

npm start

你应该能看到一个显示 “Hello, Electron!” 的窗口,这就是你的第一个 Electron 应用。

三、主进程与渲染进程

3.1 主进程

主进程在 Electron 应用中起着核心作用。它负责创建和管理浏览器窗口,控制应用的生命周期,以及与操作系统进行交互。在我们之前创建的 main.js 文件中,主进程通过 electron.app 模块管理应用的生命周期事件,例如 app.whenReady() 事件表示应用已经准备好创建窗口,app.on('window - all - closed') 事件用于处理所有窗口关闭的情况。

主进程创建浏览器窗口使用 electron.BrowserWindow 类。在创建窗口时,可以设置窗口的各种属性,如宽度、高度、是否可最大化、是否透明等。例如,我们可以设置窗口为无边框且透明:

const win = new BrowserWindow({
    width: 400,
    height: 300,
    frame: false,
    transparent: true,
    webPreferences: {
        nodeIntegration: true
    }
});

3.2 渲染进程

渲染进程负责显示应用的用户界面,它本质上就是一个运行在 Chromium 环境中的网页。渲染进程中可以使用标准的 HTML、CSS 和 JavaScript 技术来构建界面。在渲染进程中,你可以使用 DOM 操作来动态更新页面,使用 CSS 样式来美化界面,以及使用 JavaScript 来添加交互功能。

例如,在 index.html 中添加一个按钮,并在 script 标签中添加点击事件处理:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>My Electron App</title>
</head>

<body>
    <button id="myButton">Click Me</button>
    <script>
        const button = document.getElementById('myButton');
        button.addEventListener('click', () => {
            alert('You clicked the button!');
        });
    </script>
</body>

</html>

渲染进程与主进程之间通过 IPC 进行通信,这使得渲染进程可以调用主进程的功能,如访问本地文件系统,而主进程也可以向渲染进程发送数据,如通知渲染进程更新界面。

四、进程间通信(IPC)

4.1 IPC 概述

在 Electron 中,主进程和渲染进程运行在不同的上下文环境中,它们之间需要一种机制来进行通信,这就是 IPC。IPC 可以让主进程和渲染进程相互发送消息和传递数据,实现功能的协同。

4.2 主进程向渲染进程发送消息

在主进程中,可以使用 BrowserWindow.webContents.send 方法向渲染进程发送消息。首先,在 main.js 中添加如下代码:

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

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

    win.loadFile('index.html');

    // 发送消息给渲染进程
    setTimeout(() => {
        win.webContents.send('message - from - main', 'Hello from main process!');
    }, 2000);
}

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

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

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

在渲染进程(index.html)中,使用 ipcRenderer 来接收消息:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>My Electron App</title>
</head>

<body>
    <h1>IPC Example</h1>
    <script>
        const { ipcRenderer } = require('electron');
        ipcRenderer.on('message - from - main', (event, message) => {
            alert(message);
        });
    </script>
</body>

</html>

4.3 渲染进程向主进程发送消息

渲染进程可以使用 ipcRenderer.send 方法向主进程发送消息,主进程使用 ipcMain.on 方法来接收。在 index.html 中添加一个按钮,并在点击时向主进程发送消息:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>My Electron App</title>
</head>

<body>
    <button id="sendButton">Send to Main</button>
    <script>
        const { ipcRenderer } = require('electron');
        const button = document.getElementById('sendButton');
        button.addEventListener('click', () => {
            ipcRenderer.send('message - from - renderer', 'Hello from renderer process!');
        });
    </script>
</body>

</html>

main.js 中接收消息:

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

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

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

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

    ipcMain.on('message - from - renderer', (event, message) => {
        console.log(message);
    });

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

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

这样,渲染进程和主进程之间就可以双向通信了。

五、使用 Node.js 模块

5.1 在主进程中使用 Node.js 模块

主进程基于 Node.js 运行,因此可以直接使用 Node.js 的各种内置模块和第三方模块。例如,使用 fs(文件系统)模块读取本地文件:

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

function createWindow() {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

    win.loadFile('index.html');

    // 读取文件内容
    const content = fs.readFileSync('test.txt', 'utf8');
    console.log(content);
}

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

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

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

在这个例子中,我们使用 fs.readFileSync 同步读取了 test.txt 文件的内容并输出到控制台。

5.2 在渲染进程中使用 Node.js 模块

默认情况下,渲染进程中是不能直接使用 Node.js 模块的,因为这可能会带来安全风险。但是,Electron 提供了 nodeIntegration 选项来允许在渲染进程中使用 Node.js 模块。在创建窗口时,设置 webPreferences.nodeIntegration: true 即可。

例如,在渲染进程中使用 os 模块获取操作系统信息:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>My Electron App</title>
</head>

<body>
    <h1>Using Node.js Module in Renderer</h1>
    <script>
        const os = require('os');
        const osInfo = os.type() + " " + os.release();
        document.write(`Operating System: ${osInfo}`);
    </script>
</body>

</html>

不过,需要注意的是,开启 nodeIntegration 可能会引入安全漏洞,如恶意脚本可能会访问本地文件系统。在生产环境中,建议谨慎使用,或者使用更安全的替代方案,如通过主进程代理访问 Node.js 功能。

六、打包与分发

6.1 打包 Electron 应用

为了将 Electron 应用分发给用户,需要将其打包成可执行文件。常用的打包工具是 electron - builder。首先,安装 electron - builder 作为项目依赖:

npm install electron - builder --save - dev

然后,在 package.json 文件中添加打包脚本:

{
    "scripts": {
        "start": "electron. ",
        "package": "electron - builder"
    }
}

运行以下命令进行打包:

npm run package

electron - builder 会根据你的操作系统生成对应的安装包,例如在 Windows 上生成 .exe 文件,在 Mac 上生成 .dmg 文件。

6.2 分发应用

  1. Windows 平台:将生成的 .exe 文件以及相关依赖(通常在打包后的目录中)提供给用户,用户可以直接双击运行。如果需要制作安装向导,可以使用工具如 Inno Setup 对 .exe 文件进行进一步封装。
  2. Mac 平台:将 .dmg 文件提供给用户,用户将应用图标拖到 Applications 文件夹即可安装。
  3. Linux 平台electron - builder 可以生成适用于不同 Linux 发行版的安装包,如 .deb(适用于 Debian、Ubuntu 等)和 .rpm(适用于 Fedora、CentOS 等)。将对应的安装包提供给用户,用户可以使用系统自带的包管理工具进行安装。

在分发应用时,还需要考虑应用的更新机制。可以使用 electron - builder 提供的自动更新功能,或者自己搭建更新服务器,实现应用的版本检查和更新下载。

七、实战案例:简易文本编辑器

7.1 功能需求

我们来创建一个简易的文本编辑器,具备以下功能:

  1. 打开本地文本文件并显示内容。
  2. 编辑文本内容并保存文件。
  3. 提供基本的文件操作菜单,如打开、保存、退出。

7.2 项目结构

text - editor/
├── main.js
├── index.html
├── package.json
├── styles.css

7.3 主进程代码(main.js)

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

let mainWindow;

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        }
    });

    mainWindow.loadFile('index.html');

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

    const template = [
        {
            label: 'File',
            submenu: [
                {
                    label: 'Open',
                    accelerator: 'CmdOrCtrl+O',
                    click: () => {
                        mainWindow.webContents.send('open - file');
                    }
                },
                {
                    label: 'Save',
                    accelerator: 'CmdOrCtrl+S',
                    click: () => {
                        mainWindow.webContents.send('save - file');
                    }
                },
                {
                    label: 'Exit',
                    accelerator: 'CmdOrCtrl+Q',
                    click: () => {
                        app.quit();
                    }
                }
            ]
        }
    ];

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

ipcMain.on('open - file - request', (event) => {
    const { dialog } = electron;
    const filePath = dialog.showOpenDialogSync(mainWindow, {
        properties: ['openFile'],
        filters: [
            { name: 'Text Files', extensions: ['txt'] }
        ]
    });
    if (filePath) {
        const content = fs.readFileSync(filePath[0], 'utf8');
        event.sender.send('open - file - response', content);
    }
});

ipcMain.on('save - file - request', (event, content) => {
    const { dialog } = electron;
    const filePath = dialog.showSaveDialogSync(mainWindow, {
        properties: ['showOverwriteConfirmation'],
        filters: [
            { name: 'Text Files', extensions: ['txt'] }
        ]
    });
    if (filePath) {
        fs.writeFileSync(filePath, content);
    }
});

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

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

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

7.4 渲染进程代码(index.html)

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>Simple Text Editor</title>
    <link rel="stylesheet" href="styles.css">
</head>

<body>
    <textarea id="textArea"></textarea>
    <script>
        const { ipcRenderer } = require('electron');

        ipcRenderer.on('open - file - response', (event, content) => {
            document.getElementById('textArea').value = content;
        });

        document.getElementById('textArea').addEventListener('input', () => {
            const content = document.getElementById('textArea').value;
            ipcRenderer.send('text - changed', content);
        });

        ipcRenderer.on('save - file - response', () => {
            alert('File saved successfully!');
        });

        ipcRenderer.on('open - file', () => {
            ipcRenderer.send('open - file - request');
        });

        ipcRenderer.on('save - file', () => {
            const content = document.getElementById('textArea').value;
            ipcRenderer.send('save - file - request', content);
        });
    </script>
</body>

</html>

7.5 样式文件(styles.css)

#textArea {
    width: 100%;
    height: 100%;
    border: none;
    padding: 10px;
    font - size: 16px;
}

通过以上代码,我们实现了一个简易的文本编辑器。用户可以通过菜单打开和保存文本文件,对文件内容进行编辑。这个案例展示了如何在 Electron 应用中结合主进程和渲染进程的功能,实现一个实用的桌面应用。

八、常见问题与解决方法

8.1 性能问题

随着应用功能的增加,Electron 应用可能会出现性能问题。例如,渲染进程中大量的 DOM 操作或复杂的 JavaScript 计算可能导致卡顿。

解决方法:

  1. 优化 DOM 操作:尽量减少不必要的 DOM 操作,批量更新 DOM。例如,使用文档片段(DocumentFragment)来缓存 DOM 更改,然后一次性添加到页面中。
  2. Web Workers:对于复杂的 JavaScript 计算,可以使用 Web Workers 在后台线程中执行,避免阻塞主线程。在 Electron 渲染进程中,Web Workers 同样适用。
  3. 图片优化:使用合适的图片格式(如 WebP),压缩图片以减少加载时间。

8.2 安全问题

由于 Electron 应用允许在渲染进程中执行 JavaScript 代码,存在一定的安全风险,如 XSS(跨站脚本攻击)和恶意脚本访问本地文件系统。

解决方法:

  1. 禁用 nodeIntegration:如果应用不需要在渲染进程中直接访问 Node.js 模块,应禁用 nodeIntegration。可以通过在创建窗口时设置 webPreferences.nodeIntegration: false 来实现。
  2. 内容安全策略(CSP):使用 CSP 来限制渲染进程中可以加载的资源来源。可以在 index.html<meta> 标签中设置 CSP 规则,例如:
<meta http - equiv="Content - Security - Policy" content="default - src'self'">

这将只允许加载来自应用本身的资源,防止加载外部恶意脚本。 3. 输入验证:对渲染进程接收的来自用户的输入进行严格验证,防止 XSS 攻击。例如,使用正则表达式验证输入是否符合预期格式。

8.3 跨平台兼容性问题

虽然 Electron 旨在实现跨平台开发,但不同操作系统之间仍可能存在兼容性问题,如样式显示差异、快捷键冲突等。

解决方法:

  1. 样式测试:在不同操作系统上进行样式测试,针对特定操作系统进行样式调整。可以使用 CSS 媒体查询来检测操作系统,并应用相应的样式。例如:
/* 针对 Mac 系统 */
@media screen and (-webkit - mac - catalyst) {
    body {
        font - family: system - ui;
    }
}
  1. 快捷键处理:为不同操作系统设置不同的快捷键。在主进程中,可以通过 process.platform 判断当前操作系统,然后动态设置菜单快捷键。例如:
const template = [
    {
        label: 'File',
        submenu: [
            {
                label: 'Open',
                accelerator: process.platform === 'darwin'? 'Cmd+O' : 'Ctrl+O',
                click: () => {
                    mainWindow.webContents.send('open - file');
                }
            }
        ]
    }
];

通过以上方法,可以有效解决 Electron 应用开发过程中的常见问题,提高应用的性能、安全性和跨平台兼容性。