Electron 架构
参考:流程模型 | Electron ( electronjs.org )
一、Electron 架构分布
提示
Electron 具有两个进程:Main 主进程和 Renderer 渲染进程。
之所以为什么这样做呢,我们可以看下面这张 Chrome 的图:
Chrome的多进程架构
在 Chrome 浏览器中,既有用户界面的绘制刷新工作,也有大量的逻辑处理工作。如果只用一个线程,在逻辑处理工作时间太长的时候,渲染界面就会帧率下降,在用户看来就是卡顿了。
通用的解法就是使用两个线程,
让凯撒的归凯撒,让上帝的归上帝
,Main 进程中处理逻辑,Render 进程中处理渲染,互不干扰,相辅相成,通过 IPC 进行通信。
笔记
IPC 的全称是 Inter-Process Communication,进程间通信。
具体在 Electron 的场景下,我们主要涉及的就是四个部分:
- Main 主进程:可以调用 Node.js 能力。
- Renderer 渲染进程:不可以调用 Node.js 能力,只能执行网页能力。
-
Preload 脚本:在 Renderer 进程之前执行,可以调用 Node.js 能力。虽然 Preload 脚本可以获取到 Renderer 进程中的 Window,但是不能直接把变量传递给 Renderer 进程,只能通过
contextBridge.exposeInMainWorld
的方式,将内容传递给 Renderer。 - HTML 前端界面:这里面可以管理 DOM 结构、样式等。
笔记: 为什么 Preload 脚本中不能和 Renderer 共享所有上下文?
这是因为安全性的原因,从 Electron 12 开始,就默认启用了 [上下文隔离](<https://www.electronjs.org/zh/docs/latest/tutorial/context-isolation>)
二、通过夜间模式切换的例子,了解 IPC 进程间通信的原理
例子来源于:Dark Mode | Electron ( electronjs.org )
提示: 目标
我们的目标就是,在页面加载的时候,先使用系统的主题配色。当然也可以切换主题配色。
1、执行过程
整体流程执行过程如下:
2、HTML / CSS 接受媒体查询,显示背景颜色
首先从 HTML 开始,页面内容如下:
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Hello World!</title>
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<link rel="stylesheet" type="text/css" href="./styles.css">
</head>
<h1>Hello World!</h1>
<p>Current theme source: <strong id="theme-source">System</strong></p>
<button id="toggle-dark-mode">Toggle Dark Mode</button>
<button id="reset-to-system">Reset to System Theme</button>
<script src="renderer.js"></script>
</body>
</body>
</html>
在界面样式部分,CSS 内容如下:
@media (prefers-color-scheme: dark) {
body { background: #333; color: white; }
@media (prefers-color-scheme: light) {
body { background: #ddd; color: black; }
}
可见是通过媒体查询,查询
perfers-color-scheme
的值,分别在 dark 和 light 两种情况下,自定义 body 的背景颜色。
3、Renderer 中绑定按钮点击函数,并与 Main 通信
另外,其中的两个 button 绑定的事件,是在在 Renderer 中进行绑定的:
document.getElementById('toggle-dark-mode').addEventListener('click', async () => {
const isDarkMode = await window.darkMode.toggle()
document.getElementById('theme-source').innerHTML = isDarkMode ? 'Dark' : 'Light'
document.getElementById('reset-to-system').addEventListener('click', async () => {
await window.darkMode.system()
document.getElementById('theme-source').innerHTML = 'System'
Renderer 中也只是常规的事件绑定,在此不再赘述。值得注意的是按钮点击函数内部的内容,执行了
window.darkMode.xxx()
,那么挂在 window 上的函数,是怎么来的呢?
提示:
是在 preload 中设置的。
我们看以下 preload 的
const { contextBridge, ipcRenderer } = require(‘electron’)
contextBridge.exposeInMainWorld(‘darkMode’, { toggle: () => ipcRenderer.invoke(‘dark-mode:toggle’), system: () => ipcRenderer.invoke(‘dark-mode:system’) })
笔记 在 Electron 中,提供了 Main 和 Renderer 的跨进程通信机制,分别是 ipcMain 和 ipcRenderer,两者分别只能在各自的进程中调用。
可见,在 prealod.js 执行的时候,预先在 Renderer 的 Window 上,挂载了两个函数:
- toggle
- system
具体的执行过程中,又执行了
ipcRenderer.invoke()
,那么这个函数又是干嘛的?
4、Main 中接受 Renderer 中的跨进程请求
具体来说,在本例中是在 Renderer 中执行了 ipcRenderer.invoke(),然后在 Main 中执行了 ipcMain.handle():
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron')
const path = require('path')
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
win.loadFile('index.html')
ipcMain.handle('dark-mode:toggle', () => {
if (nativeTheme.shouldUseDarkColors) {
nativeTheme.themeSource = 'light'
} else {
nativeTheme.themeSource = 'dark'
return nativeTheme.shouldUseDarkColors
ipcMain.handle('dark-mode:system', () => {
nativeTheme.themeSource = 'system'
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()