跳转至

:::info 跟随教程

这是 Electron 教程的第 3 部分。

  1. 前提条件
  2. 构建你的第一个应用
  3. 使用预加载脚本
  4. 添加功能
  5. 打包你的应用
  6. 发布和更新

:::

学习目标

在本教程的这一部分中,你将了解什么是预加载脚本,以及如何安全地将特权 API 暴露到渲染进程中。你还将学习如何使用 Electron 的进程间通信(IPC)模块在主进程和渲染进程之间进行通信。

什么是预加载脚本?

Electron 的主进程是一个 Node.js 环境,具有完整的操作系统访问权限。除了 Electron 模块,你还可以访问 Node.js 内置模块,以及通过 npm 安装的任何包。另一方面,出于安全原因,渲染进程运行网页,默认不运行 Node.js。

为了将 Electron 的不同进程类型连接起来,我们需要使用一种称为 preload 的特殊脚本。

使用预加载脚本增强渲染进程

BrowserWindow 的预加载脚本运行在一个可以访问 HTML DOM 以及 Node.js 和 Electron API 有限子集的上下文中。

:::info 预加载脚本沙盒化

从 Electron 20 开始,预加载脚本默认沙盒化,并且不再能够访问完整的 Node.js 环境。实际上,这意味着你拥有一个 require 函数,它只能加载 electron 模块(渲染进程模块),此外还有一个精简的 process 对象,以及 globalThis 的 global 别名。没有可用的 Node.js 内置模块或 Node.js 全局变量。

如需了解更多信息,请查看进程沙盒化指南。

:::

预加载脚本会在渲染进程中网页加载之前注入,类似于 Chrome 扩展的内容脚本。要为渲染进程添加需要特权访问的功能,你可以通过 contextBridge API 定义 global 对象。

为了演示这个概念,你将创建一个预加载脚本,将你的应用的 Chrome、Node 和 Electron 版本暴露到渲染进程中。

添加一个新的 preload.js 脚本,将 Electron 的 process.versions 对象的选定属性通过 versions 全局变量暴露到渲染进程中。

preload.js
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron
  // we can also expose variables, not just functions
})

要将此脚本附加到你的渲染进程,请在 BrowserWindow 构造函数中将其路径传递给 webPreferences.preload 选项:

```js {2,8-10} title="main.js" const { app, BrowserWindow } = require('electron')

const path = require('node:path')

const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } })

win.loadFile('index.html') }

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

:::info

这里用到了两个 Node.js 概念:

- [`__dirname`][dirname] 字符串指向当前执行脚本的路径(在本例中,是你的项目根文件夹)。
- [`path.join`][path-join] API 将多个路径段连接在一起,创建一个在所有平台上都能工作的组合路径字符串。

:::

此时,渲染进程可以访问 `versions` 全局变量,因此让我们在窗口中显示这些信息。可以通过 `window.versions` 或简单地通过 `versions` 访问此变量。创建一个 `renderer.js` 脚本,使用 [`document.getElementById`][] DOM API 替换 `id` 属性为 `info` 的 HTML 元素的显示文本。

```js title="renderer.js" @ts-nocheck
const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), and Electron (v${versions.electron()})`

然后,修改你的 index.html,添加一个 id 属性为 info 的新元素,并附加你的 renderer.js 脚本:

```html {18,20} title="index.html"

Hello from Electron renderer!

Hello from Electron renderer!

👋

按照上述步骤操作后,你的应用应该看起来类似这样:

![Electron 应用显示:此应用使用 Chrome (v102.0.5005.63)、Node.js (v16.14.2) 和 Electron (v19.0.3)](https://raw.atomgit.com/GitHub_Trending/el/electron/raw/7880c689d542b83e9643a6a3acb6ea853d95edfc/docs/images/preload-example.png)

代码应该看起来像这样:

```fiddle docs/fiddles/tutorial-preload

进程间通信

正如上文所述,Electron 的主进程和渲染进程具有不同的职责,且不能互换。这意味着无法从渲染进程直接访问 Node.js API,也无法从主进程访问 HTML 文档对象模型(DOM)。

解决此问题的方法是使用 Electron 的 ipcMain 和 ipcRenderer 模块进行进程间通信(IPC)。要从你的网页向主进程发送消息,你可以使用 ipcMain.handle 设置一个主进程处理器,然后在你的预加载脚本中暴露一个调用 ipcRenderer.invoke 以触发该处理器的函数。

为了说明这一点,我们将向渲染进程添加一个名为 ping() 的全局函数,该函数将从主进程返回一个字符串。

首先,在你的预加载脚本中设置 invoke 调用:

```js {1,7} title="preload.js" const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('versions', { node: () => process.versions.node, chrome: () => process.versions.chrome, electron: () => process.versions.electron, ping: () => ipcRenderer.invoke('ping') // we can also expose variables, not just functions })

:::caution IPC 安全

注意,我们将 `ipcRenderer.invoke('ping')` 调用包装在一个辅助函数中,而不是通过 context bridge 直接暴露 `ipcRenderer` 模块。你**绝不**应该通过 preload 直接暴露整个 `ipcRenderer` 模块。这会让渲染进程能够向主进程发送任意 IPC 消息,从而成为恶意代码的强大攻击向量。

:::

然后,在主进程中设置你的 `handle` 监听器。我们在加载 HTML 文件_之前_这样做,以确保在你从渲染进程发出 `invoke` 调用之前,处理程序已经准备就绪。

```js {1,15} title="main.js"
const { app, BrowserWindow, ipcMain } = require('electron/main')

const path = require('node:path')

const createWindow = () => {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  win.loadFile('index.html')
}
app.whenReady().then(() => {
  ipcMain.handle('ping', () => 'pong')
  createWindow()
})

一旦你设置好了发送方和接收方,现在就可以通过你刚刚定义的 'ping' 通道从渲染进程向主进程发送消息。

```js title='renderer.js' @ts-expect-error=[2] const func = async () => { const response = await window.versions.ping() console.log(response) // prints out 'pong' }

func() ```

:::info

如需对使用 ipcRenderer 和 ipcMain 模块的更深入说明,请查看完整的 进程间通信 指南。

:::

总结

preload 脚本包含在网页加载到浏览器窗口之前运行的代码。它可以访问 DOM API 和 Node.js 环境,并且通常用于通过 contextBridge API 向渲染进程暴露特权 API。

由于主进程和渲染进程具有非常不同的职责,Electron 应用通常使用 preload 脚本来设置进程间通信(IPC)接口,以便在两种进程之间传递任意消息。

在教程的下一部分中,我们将向你展示用于为应用添加更多功能的资源,然后教你如何向用户分发你的应用。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 el/electron