欢迎来到Electron的第二期入门教程,上一期我们从零开始编写并发布了第一个简单的windows桌面应用程序,对electron的项目结构等也有了基本了解。这一期开始,会继续深入地学习Electron的其他知识点,这一节概念性的东西会比较多,虽然后面也有实操的点,但重在理解。上一节写preload.js的时候,就提到过进程相关的概念,但是并没有详细地去学习。Electron继承了Chromium的多进程架构,这使得该框架架构与现代web浏览器非常相似。
✧ 为什么不是单个进程?
Web浏览器是非常复杂的应用程序。除了显示网页内容的主要功能外,它们还有许多次要的职责,比如管理多个窗口(或标签)和加载第三方扩展。 在早期,浏览器通常使用一个进程来实现所有这些功能。虽然这种模式意味着你打开的每个标签的开销更少,但它也意味着一个网站崩溃或挂起会影响整个浏览器。
✧ 多进程模型
为了解决这个问题,Chrome团队决定每个标签将在自己的进程中渲染,以限制网页上的bug或恶意代码可能对整个应用程序造成的伤害。然后,单个浏览器流程控制这些流程以及应用程序的整个生命周期。下面这张来自Chrome漫画的图表显示了这个模型:
主进程 :每个Electron应用程序都有一个主进程,它作为应用程序的入口点。主进程运行在Node.js环境中,这意味着它有能力要求模块并使用所有Node.js的api。
主进程的主要目的是使用BrowserWindow
模块创建和管理应用程序窗口。
BrowserWindow
类的每个实例都创建了一个应用程序窗口,该窗口在单独的渲染进程中加载网页。你可以使用窗口的webContents
对象让主进程与这个web内容交互。当一个BrowserWindow实例被销毁时,它对应的渲染进程也会被终止。
如我们下面尝试把csdn的首页给渲染到我们的应用上,并且在控制台打印出webContents
对象,可以在main.js里面编写如下代码(如果你用的是上一期的代码,请把preload.js的代码都注释掉,不然会报错):
main.js
// 我们需要导入两个electron模块
const { app, BrowserWindow } = require('electron')
const path = require('path')
// 创建一个createWindow()函数,用于将index.html加载到新BrowserWindow实例中
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// let promise = win.loadFile('index.html');
win.loadURL('https://www.csdn.net/').then(r => {
const contents = win.webContents
console.log(contents)
})
}
// 调用这个createWindow()函数来打开你的窗口
app.whenReady().then(() => {
createWindow()
}
✧ 进程在Electron应用的生命周期的作用
主进程还通过Electron的应用程序模块控制应用程序的生命周期。该模块提供了大量的事件和方法,您可以使用它们来添加自定义的应用程序行为(例如,以编程退出应用程序的方式、修改应用程序上方的选项菜单等)。之后会专门开一节来详细讲解所有常用的生命周期。
为了扩展Electron的功能,使之不仅仅是一个Chromium的网页内容包装器,主进程还添加了自定义api来与用户的操作系统交互。Electron公开了控制本机桌面功能的各种模块,比如菜单、对话框和托盘图标。
✧ 渲染进程
每个Electron应用程序为每个打开的BrowserWindow
(和每个web嵌入)生成一个单独的渲染进程。顾名思义,渲染器负责渲染网页内容。对于所有的意图和目的,在渲染进程中运行的代码应该按照web标准运行(至少在Chromium中是这样的)。因此,在一个浏览器窗口中,所有的用户界面和应用程序功能都应该使用你在web上使用的相同的工具和范例来编写(HTML,CSS,JS)。此外,渲染器不能直接访问require或其他Node.js api。为了在渲染器中直接包含NPM模块,你必须使用你在web上使用的相同的捆绑工具链(例如,webpack或parcel等)。
✧ 预加载脚本
预加载脚本包含在web内容开始加载之前在渲染进程中执行的代码。这些脚本在呈现器上下文中运行,但通过访问Node.js api被授予了更多的特权。预加载脚本可以在BrowserWindow
构造函数的webPreferences
选项中附加到主进程。
main.js
代码语言:javascript复制const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js'
}
})
//...
因为预加载脚本与渲染器共享一个全局的window
对象,并且可以访问Node.js api,它通过在Window global中公开任意api来增强你的渲染器,你的web内容可以使用这些api。但这里要注意,尽管预加载脚本与它们所连接的渲染器共享一个全局窗口,但由于contextIsolation
默认值使上下文隔离的缘故,你不能直接将任何变量从预加载脚本连接到窗口。
✧上下文隔离
上下文隔离是一个特性,它可以确保你的预加载脚本和Electron的内部逻辑在一个单独的上下文中运行到你在webContents
中加载的网站。这对于安全目的来说很重要,因为它有助于防止网站访问Electron内部组件或您的预加载脚本可以访问的强大api。这意味着你的预加载脚本访问的窗口对象实际上是一个不同于网站访问的对象。例如,如果你设置window
的Hello = 'wave'
在您的预加载脚本和上下文隔离已启用如果网站试图访问Hello, Hello将是undefined
。在默认情况下,自Electron 12以来已经启用了上下文隔离,建议所有应用程序都采用此安全设置。
如下面这个例子: 我们现在preload.js里面编写自己的接口:
代码语言:javascript复制window.myAPI = {
desktop: true
}
然后再创建一个叫index.js的脚本,编写代码获取preload.js预加载脚本里的自己编写的接口,并把它通过script标签引到index.html页面中去: index.js
代码语言:javascript复制console.log(window.myAPI)
// => undefined
index.html
代码语言:javascript复制<script src="./index.js"></script>
会发现打印的确是undefined,myAPI的值并没有被取到。
上下文隔离意味着预加载脚本与渲染器的主世界隔离,以避免泄露任何特权api到你的web内容的代码中。相反地,使用contextBridge
模块来安全地完成这个任务 :
preload.js
代码语言:javascript复制const { contextBridge } = require('electron')
contextBridge.exposeInMainWorld('myAPI', {
desktop: true
})
index.js
代码语言:javascript复制console.log(window.myAPI)
// => { desktop: true }
好了,Electron的进程模型就学到这里,后面会继续更新Electron的教程!!