课堂笔记
课堂笔记
- 生命周期
- 渲染进程的权限
- 预加载脚本
- 上下文隔离
生命周期
我们在最早就接触了一个 Electron 的生命周期方法:
// whenReady 是一个生命周期方法,当 Electron 完成初始化后会调用这个方法
app.whenReady().then(() => {
createWindow();
});
该方法是在 Electron 完成初始化后会调用这个方法。
- will-finish-launching:在应用完成基本启动进程之后触发
- ready:当 Electron 完成初始化后触发
- window-all-closed:所有窗口都关闭的时候触发,特别是在 windows 和 linux 里面,所有窗口都关闭则意味着应用退出
app.on("window-all-closed", () => {
// 当操作系统不是 darwin(macOS) 的时候
if (process.platform !== "darwin") {
// 退出应用
app.quit();
}
});
- before-quit:退出应用之前触发
- will-quit:即将退出应用的时候
- quit:退出应用的时候
你可以在 https://www.electronjs.org/docs/latest/api/app 看到更多的 app 模块的生命周期方法。
除了 app 模块以外,BrowserWindow 也有很多的事件钩子:
- closed:当窗口被关闭的时候
- focus:当窗口聚焦的时候
- show:当窗口展示的时候
- hide:当窗口隐藏的时候
- maxmize:当窗口最大化的时候
- minimize:当窗口最小化的时候
你可以在 https://www.electronjs.org/docs/latest/api/browser-window 这里看到更多的关于 BrowserWindow 的事件钩子。
一个简单的使用示例:
win.on("minimize", () => {
console.log("窗口最小化了");
});
渲染进程的权限
在 Electron 中,出于安全性的考虑,实际上提供给渲染进程的可用的 API 是比较少的。

在早期的时候,Electron 团队其实提供了一个名为 remote 的模块,该模块也能够达到主进程和渲染进程互访的目的,降低两者之间通信的难度。
但是该模块本身带来了一些问题:
- 性能问题
- 通过 remote 模块倒是可以使用原本只能在主进程里面使用的对象、类型、方法,但是这些操作都是跨进程的。在操作系统中,一旦涉及到跨进程的操作,性能上的损耗可能会达到几百倍甚至上千倍。
- 假设我们在渲染进程里面通过 remote 模块使用了主进程的 BrowserWindow 来创建一个窗口实例,不仅创建该窗口实例的过程很慢,你后面使用这个窗口实例的过程也很慢,小到更新属性,大到使用方法,都是跨进程。
- 影子对象
- 在渲染进程中通过 remote 模块使用到了主进程里面的某个对象,看上去是得到了主进程里面真正的对象,但实际上不是,得到的是一个对象的代理(影子)。
- 这个影子对象和主进程里面真正的原本的对象还是有一定区别。首先,原本的对象的原型链上面的属性是不会被映射到渲染进程的影子对象上面。另外,类似于 NaN、Infinity 这样的值在渲染进程的影子对象里面得到是 undefined。这意味着假设在主进程里面有一个方法返回一个 NaN 的值,通过渲染进程的影子对象来调用该方法的话,得到的却是 undefined。
- 存在安全性问题
- 使用 remote 模块后,渲染进程可以很轻松的直接访问主进程的模块和对象,这会带来一些安全性问题,可能会导致一些渲染进程里面的恶性代码利用该特性进行攻击。
Electron 团队意识到这个问题之后,将 remote 模块标记为了“不赞成”。
从 Electron 10 版本开始,要使用 remote 模块,必须手动开启
const { app, BrowserWindow } = require("electron");
// 创建窗口方法
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
enableRemoteModule: true, // 允许使用 remote 模块
},
});
win.loadFile("window/index.html");
};
// whenReady 是一个生命周期方法,当 Electron 完成初始化后会调用这个方法
app.whenReady().then(() => {
createWindow();
});
开启之后,就可以在渲染进程中直接通过 remote 使用一些原本只能在主进程中才能使用的 API
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="btn">新建页面</button>
<script>
let { remote } = require("electron");
let newWin = null;
btn.onclick = function () {
// 创建新的渲染进程
newWin = new remote.BrowserWindow({
webPreferences: { nodeIntegration: true },
});
// 在新的渲染进程中加载 html 文件
newWin.loadFile("./index2.html");
};
</script>
</body>
</html>
之后,从 Electron14版本开始,彻底废除了 remote 模块。
不过,如果你坚持要用,也有一个替代品,就是 @electron/remote:https://www.npmjs.com/package/@electron/remote
预加载脚本
所谓预加载脚本,指的是执行于渲染进程当中,但是要先于网页内容开始加载的代码。
在预加载脚本中,可以使用 Node.js 的 API,并且由于它是在渲染进程中,也可以使用渲染进程的 API 以及 DOM API,另外还可以通过 IPC 和主进程之间进行通信,从而达到调用主进程模块的目的。
因此,预加载脚本虽然是在渲染进程中,但是却拥有了更多的权限。

下面是一个简单的示例:
// preload.js
const fs = require("fs");
window.myAPI = {
write: fs.writeSync,
};
在 preload.js 中,我们引入了 Node.js 的 API,并且由于预加载脚本和渲染进程里面的浏览器共享一个全局的 window 对象,因此我们可以将其挂载到 window 对象上面。
之后需要在 webPreferences 里面指定预加载脚本的路径,注意这是一个绝对路径,这意味着最好使用 path.join 方法去拼接路径。
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
preload: path.join(__dirname, "preload.js"),
},
但是需要注意,从 Electron12版本开始,默认是开启了上下文隔离的,这意味着预加载脚本和渲染进程里面的浏览器不再共享 window 对象,我们在 preload 里面对 window 的任何修改,不会影响渲染进程里面的 window 对象。
上下文隔离
上下文隔离(contextIsolation)是 Electron 里面的一个非常重要的安全特性,用于提高渲染进程里面的安全性。从 Electron12 版本开始默认就开启,当然目前可以在 webPreferences 里面设置关闭。
上下文隔离打开之后,主要是为了将渲染进程中的 JS 上下文环境和主进程隔离开,减少安全性风险。
举个例子:
假设有一个 Electron 程序,在没有隔离的情况,其中一个渲染进程进行文件相关的操作,例如文件删除,这就可能导致安全漏洞。
现在,在启动了上下文隔离之后,渲染进程是无法直接使用 Node.js 里面的模块的。
那么如果我在渲染进程中就是想要使用一些 Node.js 的相关模块,该怎么办呢?这里就可以通过预加载脚本来选择性的向渲染进程暴露,提高了安全性。
下面是一个简单的示例:
const fs = require("fs");
const { contextBridge } = require("electron");
// 通过 contextBridge 暴露给渲染进程的方法
contextBridge.exposeInMainWorld("myAPI", {
write: fs.writeSync,
open: fs.openSync,
});
在预加载脚本中,我们通过 contextBridge 的 exposeInMainWorld 方法来向渲染进程暴露一些 Node.js 里面的 API,这样做的一个好处在于渲染进程中只能使用到暴露出来的 API,其他没有暴露的是无法使用。
在渲染进程中,通过如下的方式来使用:
// 渲染进程 index.js
console.log(window.myAPI, "window.myAPI");
const btn = document.getElementById("btn");
btn.addEventListener("click", () => {
// 打开文件
const fd = window.myAPI.open("example.txt", "w");
// 写入内容
window.myAPI.write(fd, "This is a test");
});
当我们使用 contextBridge 向渲染进程暴露方法的时候,有两个方法可选:
- exposeInMainWorld:允许向渲染进程的主世界(MainWorld)暴露 API.
该方法接收两个参数:
- apiKey:在主世界的 window 对象下暴露的 API 名称
- api(Object):要暴露的方法,一般封装到一个对象里面
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("myAPI", {
doSomething: () => console.log("在主世界中做了些事情!"),
});
- exposeInIsolatedWorld:允许向渲染进程的隔离世界(IsolatedWorld)暴露 API.
该方法接收 4 个参数:
- worldId:隔离世界的唯一标识
- apiKey:想在隔离世界的 window 对象下暴露的 API 名称
- api(Object):要暴露的方法,一般封装到一个对象里面
- options:附加渲染
// 在预加载脚本中
const { contextBridge } = require("electron");
contextBridge.exposeInIsolatedWorld(
"isolatedWorld", // 隔离世界的标识
"myIsolatedAPI", // 在隔离世界中暴露的 API 名称
{
doSomethingElse: () => console.log("在隔离世界中做了些事情!"),
},
{}
);
// 在隔离世界的网页脚本中
window.myIsolatedAPI.doSomethingElse(); // 输出:"在隔离世界中做了些事情!"
一般来讲 exposeInMainWorld 就够用了。