三、Electron的基础知识

如同打仗,赢得了每一场战役,却输掉了整场战争,是很悲剧的事情。软件开发也是如此。有时候,我们做好了每一件事,却研发出了一个很糟糕的产品。往往是架构设计出现了问题。学习一套框架更是如此,我们不仅要了解这个框架的技术细节,正要看到它的整体面貌。细节我们可以在日常使用中一点一点积累,整体架构要一开始就夯实。不然,地基不稳,很多细节的理解很容易出现偏差。最终落个知其言而不知其所以然的混沌状态。现在,我们现在开始从Electron的架构设计入手来看看它。

Electron是基于Chromium和Node.js的,熟悉他们的都知道,二者都是多进程架构的。Electron也一样。它分为主进程和渲染进程。其中,主进程只有一个,渲染进程可以有很多个。然后,还有多个工作者进程。其基本架构为:

graph TB 主进程-->xr1(渲染进程) 主进程-->xr2(渲染进程) 主进程-->xr3(渲染进程) 主进程-->xr4(... ...)

事实上,这就是Chromium浏览器的架构。其中,每个渲染进程都是一个窗口,而主进程负责管理所有的窗口及其对应的渲染进程,同时处理网页窗口外的部分界面,如开发工具、窗口菜单等等。我们知道,正常我们在网页中是一个受限的环境,为了安全,浏览器把网页锁在了一个“笼子”里面。所以,我们浏览网页的时候不会因为恶意网页而损坏我们电脑中的数据。在浏览网页时是合理的。但是,对于桌面开发,这就成了一个问题。会导致很多功能无法实现。所以,Electron内置了很多功能模块来突破浏览器的限制。其中,大部分模块被放在了主进程里面。渲染进程要想使用这些功能,就必须跟主进程进行通信。所以,主进程与渲染进程的互访是一个很重要的话题。

3.1 主进程和渲染进程的划分

以咱们写的那个简单的demo为例,其中包含三个文件,分别是package.jsonindex.jsindex.html。其中,

  • package.json是配置文件,用于记录依赖包、编译等配置的。

  • index.js是主进程运行的脚本。

  • index.html是渲染进程运行的网页。

所以,我们可以看到在index.js中会创建一个窗口并加载index.html。实际上,如果index.html页面被关闭了,其窗口和进程也是由index.js运行的主进程回收的。渲染进程只需要负责显示界面、接收用户输入、响应用户的操作就可以了。

在demo中,我们使用的是静态网页,所以,并没有给渲染进程开启访问Node.js的能力。实际上,如果我们创建窗口时将nodeIntergration设置为true之后,渲染进程就可以使用Node.js了。

3.2 主进程与渲染进程之间的通信

基于demo2,在demo3中,加了一点点功能。就是在我们让渲染进程启动后立即发送了一个消息给主进程。然后,主进程再回复一个消息给渲染进程。接着,渲染进程收到主进程发过来的消息后,将消息内容显示到页面上面。

index.js的代码如下:

// index.js
const { app, BrowserWindow, ipcMain } = require("electron");

ipcMain.on("msg2main", (event, msg) => {
    console.log("Received a msg in main: ", msg)
    event.sender.send("msg2render", "this is a message from main");
})

app.whenReady().then(() => {
    const win = new BrowserWindow({
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false
        }
    })
    win.loadFile("./index.html");
    setTimeout(() => {
        console.log("send a msg in main");
        win.webContents.send("msg2render", "this is a message from main, delay one second");
    }, 1000.0);
})

相对于demo2

首先,咱们引入了一个名为ipcMain的模块。这个模块实现了主进程的ipc功能。我们用它可以接收渲染进程发送过来的消息。如第4~7行所演示的,我们可以接收一个名为msg2main的消息,并发送一个回复消息给渲染进程。需要注意的是,默认通信是异步的。这是因为,javascript是单线程执行的。如果大量使用同步函数,很容易造成卡顿甚至卡死。这也是Node.js提供异步I/O的核心原因。我们当然可以在主进程的监听函数中直接返回回复,但是,极度不推荐这样做。这会带来很大的性能问题。这儿就不演示同步通信了。

其中,第6行,还可以写成:

event.reply("msg2render", "this is a message from main");

这两种写法是完全一模一样的。只是不同的接口封装而已。

如果主进程想主动发送消息给渲染进程,那就需要使用BrowserWindow类中webContentssend函数了。第17~20行演示的就是这种。

为什么渲染进程发消息给主进程直接使用ipcRenderer就可以了,而主进程给渲染进程不能使用ipcMain呢?答案很简单,因为渲染进程有多个,主进程只有一个。如果设计成使用ipcMain给渲染进程发消息,那就需要先找到是哪个窗口。不如索性把发送函数放在这个窗口的类里面不好么。

index.html的代码如下:

<!DOCTYPE html>
<html>
    <head>
        <title>demo</title>
    </head>
    <body>
        <span id="msg"></span>!
        <script lang="javascript">
            let {ipcRenderer} = require("electron");

            ipcRenderer.on("msg2render", (event, msg)=>{
                console.log("Received a msg in render: ", msg)
                document.querySelector("#msg").innerText += "\n"+ msg;
            });

            ipcRenderer.send("msg2main", "this is a message from render");
        </script>
    </body>
</html>

相对于demo2,主要多了第8~17行的脚本。

首先,从electron中引入ipcRenderer模块。

然后,给ipcRenderermsg2render添加一个监听函数,在这个函数中,我们把消息内容打印到控制台并显示到页面中。

最后,通过ipcRenderer模块发送消息给主进程。这样,主进程就会发回复回来,完成整个通信交互。

image-20240602140440104

3.3 主进程和渲染进程可以做什么

我们说过,为了实现桌面应用程序的开发,Electron内置了很多功能模块。这些模块的具体接口可以在Electron官网提供的API文档中查到。简单列举如下:

模块名 归属 简介
app 主进程 用于控制应用程序的事件生命周期。
BrowserWindow 主进程 用于创建和控制浏览器窗口。
clipboard 主进程、渲染进程 在系统剪贴板上执行复制和粘贴操作。
contentTracing 主进程 从Chromium收集追踪数据以找到性能瓶颈和慢操作。
crashReporter 主进程、渲染进程 用于将崩溃日志提交给远程服务器
desktopCapturer 主进程 用来从桌面捕获音频和视频的媒体源的信息。
dialog 主进程 用于显示用于打开和保存文件、警报等的本机系统对话框。
globalShortcut 主进程 在应用程序没有键盘焦点时,监听键盘事件。
ipcMain 主进程 从主进程到渲染进程的异步通信。
Menu 主进程 创建原生应用菜单和上下文菜单。
MessageChannelMain 主进程 主进程中用于通道消息传递的通道接口。
MessagePortMain 主进程 主进程中用于通道消息传递的端口接口。
nativeImage 主进程、渲染进程 使用 PNG 或 JPG 文件创建托盘、dock和应用程序图标。
nativeTheme 主进程 读取并响应Chromium本地色彩主题中的变化。
net 主进程 使用Chromium的原生网络库发出HTTP / HTTPS请求
Notification 主进程 创建OS(操作系统)桌面通知
parentPort 通用 与父进程通信接口。
powerMonitor 主进程 监视电源状态的改变。
powerSaveBlocker 主进程 阻止系统进入低功耗 (休眠) 模式。
process 主进程、渲染进程 当前程序进程的相关信息
protocol 主进程 注册自定义协议并拦截基于现有协议的请求。
safeStorage 主进程 允许访问简单的加密和解密字符串,以便存储在本地机器上。
screen 主进程 检索有关屏幕大小、显示器、光标位置等的信息。
session 主进程 管理浏览器会话、cookie、缓存、代理设置等。
shell 主进程、渲染进程 使用默认应用程序管理文件和 url。
Tray 主进程 添加图标和上下文菜单到系统通知区
utilityProcess 主进程 用于创建子进程。与Node.js提供的child_process.fork相似,
只是它使用的是Chromium的API
webContents 主进程 渲染以及控制 web 页面
contextBridge 渲染进程 在隔离的上下文中创建一个安全的、双向的、同步的桥梁。
ipcRenderer 渲染进程 从渲染器进程到主进程的异步通信。
webFrame 渲染进程 用于调整当前页面的一些属性,如缩放倍数、拼写检查等等
webUtils 渲染进程 主要用于获取网页File对象的真实完整路径的。