参考链接: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 模块 | events 、timers 、url |
Polyfilled 的全局模块 | Buffer 、process 、clearImmediate 、setImmediate |
有关详细信息,请阅读 沙盒进程 教程。
:::
预加载脚本像 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概念:
现在渲染器能够全局访问 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>
做完这几步之后,你的应用应该长这样:
你的代码应该长这样:
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) 接口以在两种进程之间传输任意信息。
在下一部分的教程中,我们将向你展示如何向你的应用中添加更多的功能,之后将向你传授如何向用户分发你的应用。