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()