跳转到主要内容

构建你的第一个应用

学习目标

在本教程的这一部分,你将学习如何设置你的 Electron 项目并编写一个最小的入门应用程序。在本节结束时,你应该能够从终端运行一个工作的 Electron 应用(在开发模式下)。

设置你的项目

避免使用 WSL

如果你使用的是 Windows 电脑,请不要在本教程中跟随使用 Windows Subsystem for Linux (WSL),因为在尝试执行应用程序时会遇到问题。

初始化你的 npm 项目

Electron 应用是使用 npm 脚手架构建的,其中 package.json 文件是入口点。首先创建一个文件夹,并在其中使用 npm init 初始化一个 npm 包。

mkdir my-electron-app && cd my-electron-app
npm init

此命令将提示你配置 package.json 中的一些字段。在本教程中,有几个规则需要遵循:

  • 入口点应为 main.js(你很快就会创建这个文件)。
  • 作者许可证描述可以是任何值,但对于之后的打包是必需的。
使用常规的 node_modules 文件夹安装依赖项

Electron 的打包工具链要求 node_modules 文件夹以 npm 安装 Node 依赖项的方式物理存在于磁盘上。默认情况下,Yarn Berrypnpm 都使用替代的安装策略。

因此,如果你正在使用 Yarn,则必须在 Yarn 配置中设置 nodeLinker: node-modules;如果你正在使用 pnpm,则必须设置 nodeLinker: hoisted

然后,将 Electron 安装到你应用程序的 devDependencies 中,这是不需要在生产环境中使用的外部仅开发依赖项列表。

为什么 Electron 是开发依赖项?

这可能看起来违反直觉,因为你的生产代码正在运行 Electron API。在底层,Electron 的 JavaScript API 绑定到一个包含其实现的二进制文件。Electron 的打包步骤处理了这个二进制文件的捆绑,从而无需将其指定为生产依赖项。

npm install electron --save-dev
警告

为了正确安装 Electron,你需要确保其 postinstall 生命周期脚本能够运行。这意味着在 npm 中避免使用 --ignore-scripts 标志,并在其他包管理器中允许 electron 运行构建脚本。

这很可能在 Electron 的未来版本中会发生变化。有关更多详细信息,请参阅 electron/rfcs#22

在初始化包并安装 Electron 后,你的 package.json 文件应该类似于这样。现在你应该还有一个 node_modules 文件夹,其中包含 Electron 可执行文件,以及一个 package-lock.json 锁定文件,该文件指定了要安装的确切依赖项版本。

package.json
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "Hello World!",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jane Doe",
"license": "MIT",
"devDependencies": {
"electron": "23.1.3"
}
}
高级 Electron 安装步骤

如果直接安装 Electron 失败,请参阅我们的 高级安装文档,了解有关下载镜像、代理和故障排除步骤的信息。

添加 .gitignore

.gitignore 文件指定了要避免用 Git 跟踪的文件和目录。你应该将 GitHub 的 Node.js gitignore 模板副本放在项目根文件夹中,以避免提交项目的 node_modules 文件夹。

运行 Electron 应用

进一步阅读

阅读 Electron 的进程模型文档,以更好地理解 Electron 的多个进程如何协同工作。

你在 package.json 中定义的 main 脚本是任何 Electron 应用程序的入口点。此脚本控制主进程,该进程在 Node.js 环境中运行,并负责控制应用程序的生命周期、显示原生界面、执行特权操作以及管理渲染器进程(稍后会详细介绍)。

在创建你的第一个 Electron 应用之前,你将首先使用一个简单的脚本来确保你的主进程入口点配置正确。在你的项目根文件夹中创建一个 main.js 文件,其中包含一行代码:

main.js
console.log('Hello from Electron 👋')

由于 Electron 的主进程是 Node.js 运行时,你可以使用 electron 命令执行任意 Node.js 代码(甚至可以使用它作为REPL)。要执行此脚本,请在 package.json 的 scripts 字段中将 electron . 添加到 start 命令。此命令将告诉 Electron 可执行文件在当前目录中查找主脚本并以开发模式运行它。

package.json
{
"name": "my-electron-app",
"version": "1.0.0",
"description": "Hello World!",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Jane Doe",
"license": "MIT",
"devDependencies": {
"electron": "23.1.3"
}
}
npm run start

你的终端应该会打印出 Hello from Electron 👋。恭喜你,你已经在 Electron 中执行了你的第一行代码!接下来,你将学习如何使用 HTML 创建用户界面,并将它们加载到原生窗口中。

将网页加载到 BrowserWindow 中

在 Electron 中,每个窗口都显示一个网页,该网页可以从本地 HTML 文件或远程 Web 地址加载。在本例中,你将加载一个本地文件。首先,在项目根文件夹中的 index.html 文件中创建一个基本网页:

index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- https://mdn.org.cn/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<title>Hello from Electron renderer!</title>
</head>
<body>
<h1>Hello from Electron renderer!</h1>
<p>👋</p>
</body>
</html>

现在你有一个网页了,你可以将它加载到 Electron 的 BrowserWindow 中。将你的 main.js 文件内容替换为以下代码。我们将单独解释每个突出显示的块。

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

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})

win.loadFile('index.html')
}

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

导入模块

main.js (第 1 行)
const { app, BrowserWindow } = require('electron')

在第一行,我们使用 CommonJS 模块语法导入了两个 Electron 模块:

  • app,它控制应用程序的事件生命周期。
  • BrowserWindow,它创建和管理应用程序窗口。
模块命名约定

你可能已经注意到 app 和 BrowserWindow 模块之间的命名约定差异。Electron 在这里遵循典型的 JavaScript 约定,即 PascalCase 模块是可实例化的类构造函数(例如 BrowserWindow、Tray、Notification),而 camelCase 模块是不可实例化的(例如 app、ipcRenderer、webContents)。

类型化导入别名

为了在编写 TypeScript 代码时获得更好的类型检查,你可以选择从 electron/main 导入主进程模块。

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

有关更多信息,请参阅 进程模型文档

Electron 中的 ES 模块

自 Electron 28 起,Electron 支持ECMAScript 模块(即使用 import 加载模块)。你可以在 我们的 ESM 指南中找到有关 ESM 在 Electron 中的状态以及如何使用它们的更多信息。

编写一个可重用的函数来实例化窗口

createWindow() 函数将你的网页加载到一个新的 BrowserWindow 实例中。

main.js (第 3-10 行)
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})

win.loadFile('index.html')
}

在应用准备好时调用你的函数

main.js (第 12-14 行)
app.whenReady().then(() => {
createWindow()
})

Electron 的许多核心模块是 Node.js 事件发射器,它们遵循 Node 的异步事件驱动架构。app 模块就是其中一个事件发射器。

在 Electron 中,只有在 app 模块的 ready 事件触发后才能创建 BrowserWindow。你可以使用 app.whenReady() API 来等待此事件,并在其 Promise fulfilled 后调用 createWindow()

信息

你通常通过使用事件发射器的 .on 函数来监听 Node.js 事件。

+ app.on('ready', () => {
- app.whenReady().then(() => {
createWindow()
})

然而,Electron 提供了 app.whenReady() 作为 ready 事件的特定助手,以避免直接监听该事件时出现的细微陷阱。有关详细信息,请参阅 electron/electron#21972

此时,运行你的 Electron 应用程序的 start 命令应该可以成功打开一个显示你网页的窗口!

你的应用程序在窗口中显示的每个网页都将在一个单独的进程中运行,称为渲染器进程(或简称渲染器)。渲染器进程可以访问与你用于典型前端 Web 开发相同的 JavaScript API 和工具,例如使用 webpack 来捆绑和缩小你的代码,或使用 React 来构建你的用户界面。

管理你的应用程序窗口生命周期

应用程序窗口在每个操作系统上的行为都不同。Electron 不会默认强制执行这些约定,而是让你选择在应用程序代码中实现它们(如果你希望遵循的话)。你可以通过监听 app 和 BrowserWindow 模块发出的事件来实现基本的窗口约定。

特定于进程的流程控制

检查 Node 的 process.platform 变量可以帮助你根据特定平台有条件地运行代码。请注意,Electron 可以运行的平台只有三种:“win32”(Windows)、“linux”(Linux)和“darwin”(macOS)。

关闭所有窗口时退出应用(Windows 和 Linux)

在 Windows 和 Linux 上,关闭所有窗口通常会完全退出应用程序。要在 Electron 应用中实现此模式,请监听 app 模块的 window-all-closed 事件,并在用户不在 macOS 上时调用 app.quit() 来退出应用程序。

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

如果没有打开的窗口,则打开一个窗口(macOS)

相反,macOS 应用即使在没有窗口打开时通常也会继续运行。当没有可用的窗口时激活应用程序应打开一个新窗口。

要实现此功能,请监听 app 模块的 activate 事件,并在没有 BrowserWindow 打开时调用你现有的 createWindow() 方法。

由于在 ready 事件之前无法创建窗口,因此你只能在应用程序初始化后监听 activate 事件。通过仅在你现有的 whenReady() 回调中监听 activate 事件来执行此操作。

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

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

最终的入门代码

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

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})

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()
}
})

可选:从 VS Code 进行调试

如果你想使用 VS Code 调试你的应用程序,你需要将 VS Code 连接到主进程和渲染器进程。这是一个供你运行的示例配置。在你项目的新 .vscode 文件夹中创建一个 launch.json 配置:

.vscode/launch.json
{
"version": "0.2.0",
"compounds": [
{
"name": "Main + renderer",
"configurations": ["Main", "Renderer"],
"stopAll": true
}
],
"configurations": [
{
"name": "Renderer",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}"
},
{
"name": "Main",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": [".", "--remote-debugging-port=9222"],
"outputCapture": "std",
"console": "integratedTerminal"
}
]
}

当你从侧边栏选择“运行和调试”时,“主进程+渲染器进程”选项将出现,允许你设置断点并检查所有变量等,包括主进程和渲染器进程。

我们在 launch.json 文件中所做的是创建了 3 个配置:

  • Main 用于启动主进程,并公开端口 9222 进行远程调试(--remote-debugging-port=9222)。这就是我们将用于附加调试器的端口,用于 Renderer。因为主进程是 Node.js 进程,所以类型设置为 node
  • Renderer 用于调试渲染器进程。因为主进程是创建该进程的进程,所以我们必须“附加”到它("request": "attach"),而不是创建一个新的进程。渲染器进程是 Web 进程,所以我们必须使用的调试器是 chrome
  • Main + renderer 是一个 复合任务,它同时执行前面的两个任务。
注意

由于我们正在附加到一个 Renderer 进程,因此你的代码的前几行可能会被跳过,因为在它们被执行之前调试器还没有足够的时间连接。你可以通过刷新页面或在开发模式下执行代码之前设置一个超时来解决这个问题。

延伸阅读

如果你想更深入地了解调试领域,以下指南提供了更多信息:

总结

Electron 应用程序是使用 npm 包设置的。Electron 可执行文件应安装在项目 devDependencies 中,并可以使用 package.json 文件中的脚本在开发模式下运行。

可执行文件运行 package.json 的 main 属性中找到的 JavaScript 入口点。此文件控制 Electron 的主进程,该进程运行 Node.js 的实例,并负责应用程序的生命周期、显示原生界面、执行特权操作和管理渲染器进程。

渲染器进程(或简称为渲染器)负责显示图形内容。你可以通过指向 Web 地址或本地 HTML 文件将网页加载到渲染器中。渲染器与常规网页非常相似,并可以访问相同的 Web API。

在本教程的下一节中,我们将学习如何用特权 API 增强渲染器进程以及如何在进程之间进行通信。