使用预加载脚本
这是 Electron 教程的第三部分。
在这部分的教程中,你将会了解什么是预加载脚本,并且学会如何使用预加载脚本来安全地将特权 API 暴露至渲染进程中。 不仅如此,你还会学到如何使用 Electron 的进程间通信 (IPC) 模组来让主进程与渲染进程间进行通信。
Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,你也可以使用 和所有通过 npm 安装的软件包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。
为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。
BrowserWindow 的预加载脚本运行在具有 HTML DOM 和 Node.js、Electron API 的有限子集访问权限的环境中。
::: info 预加载脚本沙盒化
从 Electron 20 开始,预加载脚本默认 沙盒化 ,不再拥有完整 Node.js 环境的访问权。 实际上,这意味着你只拥有一个 polyfilled 的 函数,这个函数只能访问一组有限的 API。
有关详细信息,请阅读 教程。
:::
预加载脚本像 Chrome 扩展的 内容脚本(Content Script)一样,会在渲染器的网页加载之前注入。 如果你想向渲染器加入需要特殊权限的功能,你可以通过 接口定义 全局对象。
为了演示这一概念,你将会创建一个将应用中的 Chrome、Node、Electron 版本号暴露至渲染器的预加载脚本
新建一个 preload.js
文件。该脚本通过 versions
这一全局变量,将 Electron 的 process.versions
对象暴露给渲染器。
preload.js
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概念:
- API 将多个路径联结在一起,创建一个跨平台的路径字符串。
现在渲染器能够全局访问 versions
了,让我们快快将里边的信息显示在窗口中。 这个变量不仅可以通过 window.versions
访问,也可以很简单地使用 versions
来访问。 新建一个 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
的全新元素,并且记得加上你的 脚本:
index.html
做完这几步之后,你的应用应该长这样:
你的代码应该长这样:
- main.js
- preload.js
- index.html
- renderer.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();
}
});
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('versions', {
chrome: () => process.versions.chrome,
electron: () => process.versions.electron,
});
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)。
首先,在预处理脚本中设置 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 信息,会使得其成为恶意代码最强有力的攻击媒介。
然后,在主进程中设置你的 handle
监听器。 我们在 HTML 文件加载之前完成了这些,所以才能保证在你从渲染器发送 invoke
调用之前处理程序能够准备就绪。
main.js
将发送器与接收器设置完成之后,现在你可以将信息通过刚刚定义的 'ping'
通道从渲染器发送至主进程当中。
renderer.js
const func = async () => {
const response = await window.versions.ping()
console.log(response) // 打印 'pong'
}
func()
info
如欲了解使用 ipcRenderer
模块和 模块的详细说明,请访问完整的 进程间通信 指南。
预加载脚本包含在浏览器窗口加载网页之前运行的代码。 其可访问 DOM 接口和 Node.js 环境,并且经常在其中使用 contextBridge
接口将特权接口暴露给渲染器。
由于主进程和渲染进程有着完全不同的分工,Electron 应用通常使用预加载脚本来设置进程间通信 (IPC) 接口以在两种进程之间传输任意信息。
在下一部分的教程中,我们将向你展示如何向你的应用中添加更多的功能,之后将向你传授如何向用户分发你的应用。