从架构出发探究Electron运行原理

早期桌面应用的开发主要借助原生 C/C++ API 进行,由于需要反复经历编译过程,且无法分离界面 UI 与业务代码,开发调试极为不便。后期出现的 QT 和 WPF 在一定程度上解决了界面代码分离和跨平台的问题,却依然无法避免较长时间的编译过程。近几年伴随互联网行业的迅猛发展,尤其是 NodeJS、Chromium 这类基于 W3C 标准开源应用的不断涌现,原生代码与 Web 浏览器开发逐步走向融合,Electron 在这种背景下诞生。 

从架构出发探究Electron运行原理_第1张图片

 基于Electron实现的产品

从架构出发探究Electron运行原理_第2张图片

Electron提供了丰富的本地(操作系统)API,使你能够使用纯JavaScript来创建桌面应用程序。与其它各种的Node.js运行时不同的是Electron专注于桌面应用程序而不是Web服务器。

Electron通过集成浏览器内核,使用Web技术来实现不同平台下的渲染,并结合了 Chromium 、Node.js 和用于调用系统本地功能的 API 三大板块。

  1. Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用;
  2. Chromium 为 Electron 提供强大的 UI 渲染能力,由于 Chromium 本身跨平台,因此无需考虑代码的兼容性。
  3. Chromium 并不具备原生 GUI 的操作能力,因此 Electron 内部集成 Node.js,编写 UI 的同时也能够调用操作系统的底层 API,例如 path、fs、crypto 等模块。
  4. Native API 为 Electron 提供原生系统的 GUI 支持,借此 Electron 可以调用原生应用程序接口。

总结起来,Chromium 负责页面 UI 渲染,Node.js 负责业务逻辑,Native API 则提供原生能力和跨平台。

上面粗略讲解了 Electron 的跨端原理,下面我们来深究一下。

Chromium 架构

Chromium 是 Chrome 的开源版,也是一个浏览器,Google Chrome 浏览器正是基于它。

Electron底层基于Chromium,Chromium的设计理念是基于多进程的,每个Tab都是一个独立的进程,称之为Renderer Process,有多少个Tab就有多少个Renderer Process。(图源: Chromium 官网)

另外还有一个,有且只有一个的主进程,称之为Main Process(浏览器整体的Window),它负责其他众多Renderer Process的创建、分配,还有其他众多整体上的控制。

因此如果有一个Tab崩溃的话,不会影响到其他的Tab,浏览器可以继续运行。

从架构出发探究Electron运行原理_第3张图片

Chromium 的多进程模式主要由三部分组成: 浏览器端(Browser)、渲染器端(Render)、浏览器与渲染器的通信方式(IPC)

1.浏览器进程

浏览器进程 Browser 只有一个,当 Chrome 打开时,进程启动。浏览器为每个渲染进程维护对应的 RenderProcessHost,负责浏览器与渲染器的交互。RenderViewHost 则是与 RenderView 对象进行交互,渲染网页的内容。浏览器与渲染器通过 IPC 进行通信。

2.渲染进程管理

每个渲染进程都有一个全局 RenderProcess 对象,可以管理其与父浏览器进程之间的通信,并维护其全局状态。

3.view 管理

每个渲染器可以维护多个 RenderView 对象,当新开标签页或弹出窗口后,渲染进程就会创建一个 RenderView,RenderView 对象与它在浏览器进程中对应的 RenderViewHost 和 Webkit 嵌入层通信,渲染出网页内容(这里是我们日常主要关注的地方)。

Electron 架构解析

Electron 架构参考了 Chromium 的多进程架构模式,即将主进程和渲染进程隔离,并且在 Chromium 多进程架构基础上做一定扩展。

将上面复杂的 Chromium 架构简化:

从架构出发探究Electron运行原理_第4张图片

Chromium 运行时由一个 Browser Process,以及一个或者多个 Renderer Process 构成。Renderer Process 负责渲染页面 Web ,Browser Process 负责管理各个 Renderer Process 以及其他功能(菜单栏、收藏夹等)

下面我们看一下 Electron 架构有那些变化?

从架构出发探究Electron运行原理_第5张图片

 Electron 架构中仍然使用了 Chromium 的 Renderer Process 渲染界面,Renderer Process 可以有多个,互相独立不干扰。由于 Electron 为其集成了 Node 运行时,Renderer Process 还可以调用 Node API。

相较于 Chromium 架构,Electron 对 Browser 进程做了很多改动,将其更改名 Main Process,每个应用程序只能有一个主进程,主进程位于 Node.js 下运行,因此其可以调用系统底层功能,其主要负责:渲染进程的创建;系统底层功能及原生资源的调用;应用生命周期的控制(包裹启动、推出以及一些事件监听),可以把它看做页面和计算机沟通的桥梁。

经过上面的分析,Electron 多进程的系统架构可以总结为下图:

从架构出发探究Electron运行原理_第6张图片

可以发现,主进程和渲染进程都集成了 Native API 和 Node.js,渲染进程还集成 Chromium 内核,成功实现跨端开发。

在Electron中,GUI组件仅在主进程可用,在渲染进程中不可用。那如果想要在渲染进程中使用GUI组件,势必需要和主进程进行通信。ipc模块就是用来实现主进程和渲染进程之间的通信。在主进程中使用ipcMain模块进行对渲染进程的通信进行控制和处理。而在渲染进程中,则使用ipcRenderer模块,来向主进程发送消息或者接受主进程的回应。

Node 与 Chromium

没有Chromium就没有V8(Chromium内置的高性能JavaScript执行引擎),没有V8就没有Node.js。Chromium的高性能并不单单是多进程架构的功劳,V8引擎也居功甚伟,V8引擎以超高性能执行JavaScript脚本著称,Node.js的作者也是因为这一点才决定封装V8,把JavaScript程序员的战场引向客户端和服务端。

Node 的事件循环与浏览器的事件循环有明显不同,Chromium 既然是 Chrome 的实验版,自然与浏览器实现相同。

Node 的事件循环基于 libuv 实现,而 Chromium 基于 message bump 实现。主线程只能同时运行一个事件循环,因此需要将两个完全不同的事件循环整合起来。

有两种解决方案:

  • 使用 libuv 实现 message bump 将 Chromium 集成到 Node.js
  • 将 Node.js 集成到 Chromium

Electron 最初的方案是第一种,使用 libuv 实现 message bump,但不同的 OS 系统 GUI 事件循环差异很大,例如 mac 为 NSRunLoop,Linux 为 glib,实现过程特别复杂,资源消耗和延迟问题也无法得到有效解决,最终放弃了第一种方案。

Electron 第二次尝试使用小间隔的定时器来轮询 GUI 事件循环,但此方案 CPU 占用高,并且 GUI 响应速度慢。

后来 libuv 引入了 backend_fd 概念,backend_fd 轮询事件循环的文件描述符,因此 Electron 通过轮询 backend_fd 来得到 libuv 的新事件实现 Node.js 与 Chromium 事件循环的融合(第二种方案)。

下面这张 PPT 完美的描述了上述过程(图源:Electron: The Event Loop Tightrope - Shelley Vohr | JSHeroes 2019)

从架构出发探究Electron运行原理_第7张图片

如何开发?

开发Electron应用的方式有很多,下面以React为例做个简单的说明:

1. 热调试

在React项目目录下安装Electron

npm install electron

修改package.json文件,增加或将已有的main属性值修改为main.js,在scriptes中添加"electron-start": "electron .",最终配置文件如下:

{
  "name": "electron-react",
  "version": "0.1.0",
  "main": "main.js",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^13.5.0",
    "electron": "^20.0.3",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "electron-start": "electron ."
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

打开main.js,将

const { app, BrowserWindow, globalShortcut } = require("electron");
const path = require("path");
 
function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webContents: {
      openDevTools: true, //不想要控制台直接把这段删除
    }
  });
 
  win.loadFile("index.html");
}
 
app.whenReady().then(() => {
  createWindow();
 
  app.on("activate", () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});
 
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
})

中的

win.loadFile("index.html");

修改为

win.loadURL("http://localhost:3000/")

打开两个终端,一个运行React

npm start

另一个执行

npm run electron-start

程序运行正常

从架构出发探究Electron运行原理_第8张图片

你可能感兴趣的:(electron,electron)