Electron预加载脚本

578 阅读5分钟

参考链接:www.electronjs.org/zh/docs/lat…

什么是预加载脚本?

Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,你也可以使用 Node.js 内置模块 和所有通过 npm 安装的软件包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。

为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。

使用预加载脚本来增强渲染器

BrowserWindow 的预加载脚本运行在具有 HTML DOM 和 Node.js、Electron API 的有限子集访问权限的环境中。

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

从 Electron 20 开始,预加载脚本默认 沙盒化 ,不再拥有完整 Node.js 环境的访问权。 实际上,这意味着你只拥有一个 polyfilled 的 require 函数,这个函数只能访问一组有限的 API。

可用的 API详细信息
Electron 模块渲染进程模块
Node.js 模块eventstimersurl
Polyfilled 的全局模块BufferprocessclearImmediatesetImmediate

有关详细信息,请阅读 沙盒进程 教程。

:::

预加载脚本像 Chrome 扩展的 内容脚本(Content Script)一样,会在渲染器的网页加载之前注入。 如果你想向渲染器加入需要特殊权限的功能,你可以通过 contextBridge 接口定义 全局对象

为了演示这一概念,你将会创建一个将应用中的 Chrome、Node、Electron 版本号暴露至渲染器的预加载脚本

1.新建一个 preload.js 文件。该脚本通过 versions 这一全局变量,将 Electron 的 process.versions 对象暴露给渲染器。

preload.js

const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld('versions', {
  node: () => process.versions.node,
  chrome: () => process.versions.chrome,
  electron: () => process.versions.electron,
  // 能暴露的不仅仅是函数,我们还可以暴露变量
})

为了将脚本附在渲染进程上,在 BrowserWindow 构造器中使用 webPreferences.preload 传入脚本的路径。

main.js

const { app, BrowserWindow } = require('electron')
const path = require('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 字符串指向当前正在执行脚本的路径 (在本例中,它指向你的项目的根文件夹)。
  • path.join API 将多个路径联结在一起,创建一个跨平台的路径字符串。

现在渲染器能够全局访问 versions 了,让我们快快将里边的信息显示在窗口中。 这个变量不仅可以通过 window.versions 访问,也可以很简单地使用 versions 来访问。

2.新建一个 renderer.js 脚本, 这个脚本使用 document.getElementById DOM 接口来替换 id 属性为 info 的 HTML 元素显示文本。

renderer.js

const information = document.getElementById('info')
information.innerText = `本应用正在使用 Chrome (v${versions.chrome()}), Node.js (v${versions.node()}), 和 Electron (v${versions.electron()})`

然后请修改你的 index.html 文件。加上一个 id 属性为 info 的全新元素,并且记得加上你的 renderer.js 脚本:

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <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>来自 Electron 渲染器的问好!</title>
  </head>
  <body>
    <h1>来自 Electron 渲染器的问好!</h1>
    <p>👋</p>
    <p id="info"></p>
  </body>
  <script src="./renderer.js"></script>
</html>

做完这几步之后,你的应用应该长这样:

Electron 应用显示这个应用正在使用 Chrome (v102.0.5005.63)、Node.js (v16.14.2) 和 Electron (v19.0.3)。

你的代码应该长这样:

DOCS/FIDDLES/TUTORIAL-PRELOAD (23.1.4)Open in Fiddle

  • main.js
const { app, BrowserWindow } = require('electron');  
const path = require('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();  
  
app.on('activate', () => {  
if (BrowserWindow.getAllWindows().length === 0) {  
createWindow();  
}  
});  
});  
  
app.on('window-all-closed', () => {  
if (process.platform !== 'darwin') {  
app.quit();  
}  
});
  • preload.js
const { contextBridge } = require('electron');  
  
contextBridge.exposeInMainWorld('versions', {  
node: () => process.versions.node,  
chrome: () => process.versions.chrome,  
electron: () => process.versions.electron,  
});
  • index.html
<!DOCTYPE html>  
<html>  
<head>  
<meta charset="UTF-8" />  
<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>  
<p id="info"></p>  
</body>  
<script src="./renderer.js"></script>  
</html>
  • renderer.js
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()})`;

在进程之间通信

我们之前提到,Electron 的主进程和渲染进程有着清楚的分工并且不可互换。 这代表着无论是从渲染进程直接访问 Node.js 接口,亦或者是从主进程访问 HTML 文档对象模型 (DOM),都是不可能的。

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

我们将向渲染器添加一个叫做 ping() 的全局函数来演示这一点。这个函数将返回一个从主进程翻山越岭而来的字符串。

3.首先,在预处理脚本中设置 invoke 调用:

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'),
  // 能暴露的不仅仅是函数,我们还可以暴露变量
})

IPC 安全

可以注意到我们使用了一个辅助函数来包裹 ipcRenderer.invoke('ping') 调用,而并非直接通过 context bridge 暴露 ipcRenderer 模块。 你永远都不会想要通过预加载直接暴露整个 ipcRenderer 模块。 这将使得你的渲染器能够直接向主进程发送任意的 IPC 信息,会使得其成为恶意代码最强有力的攻击媒介。

4.然后,在主进程中设置你的 handle 监听器。 我们在 HTML 文件加载之前完成了这些,所以才能保证在你从渲染器发送 invoke 调用之前处理程序能够准备就绪。

main.js

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

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

将发送器与接收器设置完成之后,现在你可以将信息通过刚刚定义的 'ping' 通道从渲染器发送至主进程当中。

renderer.js

const func = async () => {
  const response = await window.versions.ping()
  console.log(response) // 打印 'pong'
}

func()

INFO

如欲了解使用 ipcRenderer 模块和 ipcMain 模块的详细说明,请访问完整的 进程间通信 指南。

摘要

预加载脚本包含在浏览器窗口加载网页之前运行的代码。 其可访问 DOM 接口和 Node.js 环境,并且经常在其中使用 contextBridge 接口将特权接口暴露给渲染器。

由于主进程和渲染进程有着完全不同的分工,Electron 应用通常使用预加载脚本来设置进程间通信 (IPC) 接口以在两种进程之间传输任意信息。

在下一部分的教程中,我们将向你展示如何向你的应用中添加更多的功能,之后将向你传授如何向用户分发你的应用。