跳转到主要内容

6 篇关于“Electron 内部机制”的博文

“通过 Electron 的源代码进行的深入技术探讨”

查看所有标签

WebView2 与 Electron

·6 分钟阅读

在过去的几周里,我们收到了几个关于新的 WebView2 和 Electron 之间区别的问题。

两个团队都有一个明确的目标,那就是让 Web 技术在桌面上发挥出最佳水平,并且正在讨论进行一次全面的共享比较。

Electron 和 WebView2 都是快速发展和不断演进的项目。我们整理了 Electron 和 WebView2 截至目前的异同点的简要快照。


架构概述

Electron 和 WebView2 都基于 Chromium 源代码构建,用于渲染网页内容。严格来说,WebView2 基于 Edge 源代码构建,而 Edge 是基于 Chromium 源代码的一个分支构建的。Electron 不与 Chrome 共享任何 DLL。WebView2 二进制文件会硬链接到 Edge(截至 Edge 90 为稳定版),因此它们会共享磁盘空间和部分工作集。有关更多信息,请参阅 Evergreen distribution mode

Electron 应用始终会捆绑和分发开发时使用的确切 Electron 版本。WebView2 在分发方面有两种选择。您可以捆绑应用程序开发时使用的确切 WebView2 库,也可以使用可能已安装在系统上的共享运行时版本。WebView2 为每种方法都提供了工具,包括在共享运行时缺失时使用的引导安装程序。WebView2 自 Windows 11 起已作为“内置”组件提供。

捆绑框架的应用程序负责更新这些框架,包括次要安全更新。对于使用共享 WebView2 运行时的应用程序,WebView2 有自己的更新程序,类似于 Chrome 或 Edge,它独立于您的应用程序运行。与 Electron 一样,更新应用程序代码或任何其他依赖项仍然是开发者的责任。Windows Update 不管理 Electron 或 WebView2。

Electron 和 WebView2 都继承了 Chromium 的多进程架构——即一个主进程与一个或多个渲染进程通信。这些进程与其他系统上运行的应用程序完全分开。每个 Electron 应用程序都是一个独立的进程树,包含一个根浏览器进程、一些实用进程以及零个或多个渲染进程。使用相同 用户数据文件夹(如一套应用程序)的 WebView2 应用会共享非渲染进程。使用不同数据文件夹的 WebView2 应用则不共享进程。

  • ElectronJS 进程模型

    ElectronJS Process Model Diagram

  • 基于 WebView2 的应用程序进程模型

    WebView2 Process Model Diagram

在此处阅读有关 WebView2 进程模型Electron 进程模型 的更多信息。

Electron 提供了满足常见桌面应用程序需求的应用编程接口(API),例如菜单、文件系统访问、通知等。WebView2 是一个旨在集成到应用程序框架(如 WinForms、WPF、WinUI 或 Win32)中的组件。WebView2 不通过 JavaScript 提供 Web 标准以外的操作系统 API。

Node.js 已集成到 Electron 中。Electron 应用程序可以在渲染进程和主进程中使用任何 Node.js API、模块或原生 Node 插件。WebView2 应用程序不假定您的应用程序的其他部分是用何种语言或框架编写的。您的 JavaScript 代码必须通过应用程序主机进程代理任何操作系统访问。

Electron 致力于保持与 Web API 的兼容性,包括从 Fugu Project 开发的 API。我们有一个 Electron Fugu API 兼容性快照。WebView2 维护着一个类似的 与 Edge 的 API 差异 列表。

Electron 为 Web 内容提供了一个可配置的安全模型,从完全访问到完全沙箱。WebView2 内容始终处于沙箱中。Electron 提供了 全面的安全文档,指导您选择安全模型。WebView2 也有 安全最佳实践

Electron 的源代码在 GitHub 上维护并可用。应用程序可以修改并构建自己的 Electron 品牌。WebView2 的源代码在 GitHub 上不可用。

快速总结

ElectronWebView2
构建依赖ChromiumEdge
GitHub 上源码是否可用
是否共享 Edge/Chrome DLL是(自 Edge 90 起)
应用程序间共享运行时可选
应用程序 API
Node.js
沙盒可选总是
需要应用程序框架
支持的平台Mac, Win, LinuxWin (计划支持 Mac/Linux)
应用间进程共享从不可选
框架更新由谁管理应用程序WebView2

性能讨论

就渲染您的 Web 内容而言,我们预计 Electron、WebView2 以及其他任何基于 Chromium 的渲染器在性能上几乎没有差异。我们为使用 Electron、C++ + WebView2 和 C# + WebView2 构建的应用程序创建了 脚手架,有兴趣的人可以研究潜在的性能差异。

在渲染网页内容之外,存在一些差异,来自 Electron、WebView2、Edge 和其他团队的人员都表示有兴趣就包括 PWA 在内的详细比较进行合作。

进程间通信 (IPC)

我们想立即强调一个差异,因为我们认为这在 Electron 应用中通常是一个性能考虑因素。

在 Chromium 中,浏览器进程充当沙箱化渲染器与系统其余部分之间的 IPC 代理。虽然 Electron 允许非沙箱化渲染进程,但许多应用程序选择启用沙箱以增强安全性。WebView2 始终启用了沙箱,因此对于大多数 Electron 和 WebView2 应用程序而言,IPC 可能会影响整体性能。

尽管 Electron 和 WebView2 具有相似的进程模型,但底层的 IPC 存在差异。在 JavaScript 和 C++ 或 C# 之间通信需要 封送,最常见的是封送为 JSON 字符串。JSON 序列化/反序列化是一个昂贵的操作,IPC 瓶颈可能会对性能产生负面影响。从 Edge 93 开始,WV2 将使用 CBOR 处理网络事件。

Electron 通过 MessagePorts API 支持任何两个进程之间的直接 IPC,该 API 使用 结构化克隆算法。利用此 API 的应用程序可以避免在进程之间发送对象时支付 JSON 序列化的费用。

总结

Electron 和 WebView2 之间存在许多差异,但在渲染 Web 内容的性能方面,您不必期望有太大区别。归根结底,应用程序的架构和 JavaScript 库/框架对内存和性能的影响比其他任何因素都大,因为“Chromium 就是 Chromium”,无论它在哪里运行。

特别感谢 WebView2 团队审阅了这篇博文,并确保我们对 WebView2 架构有最新的认识。他们欢迎就该项目提出任何 反馈

Electron 中从原生到 JavaScript

·阅读时长 4 分钟

Electron 中用 C++ 或 Objective-C 编写的功能是如何被暴露给 JavaScript,从而可供最终用户使用的?


背景

Electron 是一个 JavaScript 平台,其主要目的是降低开发人员构建健壮的桌面应用程序的门槛,而无需担心平台特定实现。然而,Electron 本身仍然需要用给定系统语言编写平台特定功能。

实际上,Electron 会为你处理原生代码,这样你就可以专注于单一的 JavaScript API。

但这又是如何工作的呢?Electron 中用 C++ 或 Objective-C 编写的功能是如何被暴露给 JavaScript,从而可供最终用户使用的?

要追踪这个路径,让我们从 app 模块开始。

通过打开我们 `lib/` 目录中的 app.ts 文件,您会在顶部附近找到以下代码行:

const binding = process.electronBinding('app');

这行代码直接指向 Electron 将其 C++/Objective-C 模块绑定到 JavaScript 的机制,以便开发者使用。此函数由 `ElectronBindings` 类的头文件和 实现文件 创建。

process.electronBinding

这些文件添加了 `process.electronBinding` 函数,其功能类似于 Node.js 的 `process.binding`。`process.binding` 是 Node.js require() 方法的一个较低级别实现,但它允许用户 `require` 原生代码而不是其他用 JS 编写的代码。这个自定义的 `process.electronBinding` 函数赋予了加载 Electron 中原生代码的能力。

当一个顶层 JavaScript 模块(如 app)需要这个原生代码时,该原生代码的状态是如何确定的和设置的?方法是如何暴露给 JavaScript 的?属性呢?

native_mate

目前,对这个问题的回答可以在 `native_mate` 中找到:它是 Chromium 的 gin 的一个分支,使得在 C++ 和 JavaScript 之间封送类型更加容易。

native_mate/native_mate 内部有一个 object_template_builder 的头文件和实现文件。这允许我们以原生代码的形式构建模块,其结构符合 JavaScript 开发者的预期。

mate::ObjectTemplateBuilder

如果我们把每个 Electron 模块看作一个 `object`,就更容易理解为什么我们要使用 `object_template_builder` 来构建它们。这个类建立在 V8 的一个类之上,V8 是 Google 开源的高性能 JavaScript 和 WebAssembly 引擎,用 C++ 编写。V8 实现 JavaScript (ECMAScript) 规范,因此其原生功能实现可以直接对应到 JavaScript 中的实现。例如,v8::ObjectTemplate 使我们能够创建没有专用构造函数和原型的 JavaScript 对象。它使用 `Object[.prototype]`,在 JavaScript 中相当于 Object.create()

要在此示例中看到这一点,请查看 app 模块的实现文件 atom_api_app.cc。底部是以下内容:

mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("getGPUInfo", &App::GetGPUInfo)

在上面的行中,在 `mate::ObjectTemplateBuilder` 上调用了 `.SetMethod`。可以对 `ObjectTemplateBuilder` 类的任何实例调用 `.SetMethod`,以在 JavaScript 的 Object 原型 上设置方法,语法如下:

.SetMethod("method_name", &function_to_bind)

这是 JavaScript 中等价于

function App{}
App.prototype.getGPUInfo = function () {
// implementation here
}

这个类还包含用于在模块上设置属性的函数

.SetProperty("property_name", &getter_function_to_bind)

.SetProperty("property_name", &getter_function_to_bind, &setter_function_to_bind)

这些将反过来是 Object.defineProperty 的 JavaScript 实现

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
})

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
set(newPropertyValue) {
_myProperty = newPropertyValue
}
})

可以创建由原型和属性组成的 JavaScript 对象,就像开发者期望的那样,并且可以更清晰地推理出在这个较低系统级别实现的功能和属性!

关于在哪里实现任何给定模块方法的决定本身是一个复杂且常常是非确定性的问题,我们将在未来的帖子中介绍。

Electron 内部原理:将 Chromium 构建为库

·7分钟阅读

Electron 基于 Google 开源的 Chromium 项目,而 Chromium 项目本身并非专门为其他项目设计。这篇文章将介绍 Chromium 如何被构建成一个库供 Electron 使用,以及构建系统在多年间的演变。


使用 CEF

Chromium Embedded Framework (CEF) 是一个将 Chromium 转换为库的项目,并基于 Chromium 代码库提供稳定的 API。Atom 编辑器和 NW.js 的早期版本都使用了 CEF。

为了维护稳定的 API,CEF 隐藏了 Chromium 的所有细节,并用自己的接口封装了 Chromium 的 API。因此,当我们想访问底层的 Chromium API,比如将 Node.js 集成到网页中时,CEF 的优势反而成了障碍。

最终,Electron 和 NW.js 都转而直接使用 Chromium 的 API。

作为 Chromium 的一部分进行构建

尽管 Chromium 官方并不支持外部项目,但其代码库是模块化的,很容易基于 Chromium 构建一个最小化的浏览器。提供浏览器界面的核心模块称为 Content Module。

要开发使用 Content Module 的项目,最简单的方法是将项目作为 Chromium 的一部分进行构建。这可以通过先检出 Chromium 的源代码,然后将项目添加到 Chromium 的 DEPS 文件中来完成。

NW.js 和 Electron 的早期版本都采用了这种构建方式。

缺点是,Chromium 是一个非常庞大的代码库,构建它需要非常强大的硬件。对于普通的笔记本电脑,这可能需要超过 5 个小时。因此,这极大地限制了可以为项目贡献的开发人员数量,并且也使开发速度变慢。

将 Chromium 构建为单个共享库

作为 Content Module 的用户,Electron 在大多数情况下不需要修改 Chromium 的代码,因此提高 Electron 构建速度的一个显而易见的方法是将 Chromium 构建为共享库,然后与 Electron 进行链接。这样,开发人员就不再需要构建整个 Chromium 来为 Electron 做贡献。

libchromiumcontent 项目由 @aroben 创建,用于此目的。它将 Chromium 的 Content Module 构建为共享库,然后提供 Chromium 的头文件和预编译的二进制文件供下载。libchromiumcontent 的初始版本的代码可以在 此链接 中找到。

brightray 项目也是作为 libchromiumcontent 的一部分诞生的,它在 Content Module 周围提供了一个精简的封装。

通过结合使用 libchromiumcontent 和 brightray,开发者无需深入了解 Chromium 的构建细节即可快速构建浏览器。并且消除了构建项目对快速网络和强大机器的要求。

除了 Electron 之外,还有其他 Chromium 驱动的项目也采用了这种方式构建,例如 Breach 浏览器

过滤导出的符号

在 Windows 上,一个共享库可以导出的符号数量是有限制的。随着 Chromium 代码库的增长,libchromiumcontent 中导出的符号数量很快就超过了限制。

解决方案是在生成 DLL 文件时过滤掉不需要的符号。它通过 为链接器提供一个 `.def` 文件,然后使用脚本 判断一个命名空间下的符号是否应该导出 来实现。

通过这种方法,即使 Chromium 不断添加新的导出符号,libchromiumcontent 仍然可以通过剥离更多符号来生成共享库文件。

组件构建

在介绍 libchromiumcontent 的下一步之前,首先介绍 Chromium 中的组件构建概念非常重要。

作为一个庞大的项目,链接步骤在 Chromium 的构建过程中非常耗时。通常,当开发人员进行微小更改时,可能需要 10 分钟才能看到最终输出。为了解决这个问题,Chromium 引入了组件构建,它将 Chromium 中的每个模块构建为独立的共享库,这样最终链接步骤花费的时间就变得可以忽略不计。

分发原始二进制文件

随着 Chromium 的不断发展,Chromium 中导出的符号越来越多,即使是 Content Module 和 Webkit 的符号也超过了限制。仅仅通过剥离符号已经不可能生成可用的共享库了。

最终,我们不得不 分发 Chromium 的原始二进制文件,而不是生成单个共享库。

如前所述,Chromium 中有两种构建模式。由于分发原始二进制文件,我们在 libchromiumcontent 中必须分发两种不同的二进制文件。一种称为 `static_library` 构建,它包含了 Chromium 正常构建生成的每个模块的静态库。另一种是 `shared_library`,它包含了组件构建生成的每个模块的共享库。

在 Electron 中,Debug 版本链接的是 libchromiumcontent 的 `shared_library` 版本,因为它下载量小,在链接最终可执行文件时耗时短。而 Electron 的 Release 版本链接的是 libchromiumcontent 的 `static_library` 版本,这样编译器就可以生成对调试至关重要的完整符号,并且链接器可以进行更好的优化,因为它知道哪些对象文件是必需的,哪些不是。

因此,对于正常开发,开发者只需要构建 Debug 版本,这不需要好的网络或强大的机器。虽然 Release 版本构建需要更好的硬件,但它可以生成更好的优化后的二进制文件。

gn 更新

作为世界上最大项目之一,大多数普通系统都不适合构建 Chromium,而 Chromium 团队开发了自己的构建工具。

Chromium 的早期版本使用 `gyp` 作为构建系统,但它速度慢,而且对于复杂项目,其配置文件难以理解。经过多年的开发,Chromium 已切换到 `gn` 作为构建系统,它速度更快,架构清晰。

`gn` 的改进之一是引入了 `source_set`,它代表了一组对象文件。在 `gyp` 中,每个模块由 `static_library` 或 `shared_library` 表示,对于 Chromium 的正常构建,每个模块会生成一个静态库,并在最终可执行文件中链接在一起。通过使用 `gn`,每个模块现在只生成一组对象文件,最终的可执行文件只是将所有对象文件链接在一起,因此不再生成中间的静态库文件。

然而,这个改进给 libchromiumcontent 带来了很大的麻烦,因为 libchromiumcontent 实际上需要中间静态库文件。

解决这个问题的第一次尝试是 修补 `gn` 以生成静态库文件,这解决了问题,但远非一个理想的解决方案。

第二次尝试由 @alespergl 进行,通过 从对象文件列表中生成自定义静态库。它利用了一个技巧,首先运行一个虚拟构建来收集生成对象文件的列表,然后通过将该列表提供给 `gn` 来实际构建静态库。它只对 Chromium 的源代码进行了最小的修改,并保持了 Electron 的构建架构。

总结

如您所见,与将 Electron 作为 Chromium 的一部分构建相比,将 Chromium 构建为库需要付出更大的努力并需要持续维护。然而,后者消除了构建 Electron 所需的强大硬件要求,从而使更多开发者能够构建和贡献 Electron。这项努力是完全值得的。

Electron 内部原理:弱引用

·6 分钟阅读

JavaScript 是一种带有垃圾回收机制的语言,它让用户无需手动管理资源。但由于 Electron 托管了这种环境,因此必须非常小心地避免内存和资源泄露。

本文将介绍弱引用的概念以及它们如何在 Electron 中用于管理资源。


弱引用

在 JavaScript 中,每当您将一个对象赋值给一个变量时,您就是添加了一个对该对象的引用。只要对该对象存在引用,它就会一直保留在内存中。一旦对该对象的所有引用都消失了,即不再有存储该对象的变量,JavaScript 引擎将在下次垃圾回收时回收内存。

弱引用是对对象的引用,它允许您获取对象,而不会影响它是否会被垃圾回收。当对象被垃圾回收时,您还会收到通知。然后,就可以使用 JavaScript 来管理资源了。

以 Electron 中的 `NativeImage` 类为例,每次调用 `nativeImage.create()` API 时,都会返回一个 `NativeImage` 实例,并在 C++ 中存储图像数据。一旦您完成了该实例的使用,并且 JavaScript 引擎 (V8) 已经垃圾回收了该对象,C++ 中的代码将被调用以释放内存中的图像数据,因此用户无需手动管理。

另一个例子是 窗口消失问题,它直观地展示了当窗口的所有引用都消失时,窗口是如何被垃圾回收的。

在 Electron 中测试弱引用

原始 JavaScript 中没有直接测试弱引用的方法,因为该语言没有分配弱引用的方式。JavaScript 中唯一与弱引用相关的 API 是 WeakMap,但由于它只能创建弱引用键,因此无法知道对象何时已被垃圾回收。

在 v0.37.8 之前的 Electron 版本中,您可以使用内部的 v8Util.setDestructor API 来测试弱引用,该 API 会为传入的对象添加一个弱引用,并在对象被垃圾回收时调用回调函数。

// Code below can only run on Electron < v0.37.8.
var v8Util = process.atomBinding('v8_util');

var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});

// Remove all references to the object.
object = undefined;
// Manually starts a GC.
gc();
// Console prints "The object is garbage collected".

请注意,您必须使用 --js-flags="--expose_gc" 命令行开关启动 Electron,以暴露内部的 gc 函数。

该 API 在后续版本中被移除,因为 V8 实际上不允许在析构函数中运行 JavaScript 代码,并且在后续版本中这样做会导致随机崩溃。

remote 模块中的弱引用

除了通过 C++ 管理原生资源之外,Electron 还需要弱引用来管理 JavaScript 资源。一个例子是 Electron 的 `remote` 模块,它是一个 远程过程调用 (RPC) 模块,允许在渲染进程中使用主进程中的对象。

`remote` 模块的一个关键挑战是避免内存泄漏。当用户在渲染进程中获取远程对象时,`remote` 模块必须确保该对象在主进程中持续存在,直到渲染进程中的引用消失。此外,它还必须确保当渲染进程中不再有任何引用时,该对象可以被垃圾回收。

例如,如果没有正确的实现,以下代码将很快导致内存泄露

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}

`remote` 模块中的资源管理很简单。每当请求一个对象时,就会向主进程发送一条消息,Electron 会将对象存储在一个映射表中并为其分配一个 ID,然后将该 ID 发送回渲染进程。在渲染进程中,`remote` 模块会接收该 ID,并用一个代理对象来包装它。当代理对象被垃圾回收时,会向主进程发送一条消息来释放该对象。

remote.require API 为例,简化的实现如下所示

remote.require = function (name) {
// Tell the main process to return the metadata of the module.
const meta = ipcRenderer.sendSync('REQUIRE', name);
// Create a proxy object.
const object = metaToValue(meta);
// Tell the main process to free the object when the proxy object is garbage
// collected.
v8Util.setDestructor(object, function () {
ipcRenderer.send('FREE', meta.id);
});
return object;
};

在主进程中

const map = {};
const id = 0;

ipcMain.on('REQUIRE', function (event, name) {
const object = require(name);
// Add a reference to the object.
map[++id] = object;
// Convert the object to metadata.
event.returnValue = valueToMeta(id, object);
});

ipcMain.on('FREE', function (event, id) {
delete map[id];
});

具有弱值的映射

通过之前的简单实现,remote 模块中的每个调用都会从主进程返回一个新的远程对象,每个远程对象都代表对主进程中对象的引用。

设计本身是好的,但问题在于,当多次调用以获取同一对象时,会创建多个代理对象,对于复杂对象来说,这会给内存使用和垃圾回收带来巨大的压力。

例如,以下代码

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}

它首先会消耗大量内存来创建代理对象,然后占用 CPU (中央处理器) 进行垃圾回收和发送 IPC 消息。

一个显而易见的优化是缓存远程对象:当已经有一个具有相同 ID 的远程对象时,将返回之前的远程对象,而不是创建一个新的。

使用 JavaScript 核心中的 API 无法做到这一点。使用普通映射表缓存对象会阻止 V8 垃圾回收对象,而 WeakMap 类只能使用对象作为弱键。

为了解决这个问题,添加了一种具有弱值作为引用的映射类型,这非常适合缓存带有 ID 的对象。现在 remote.require 看起来像这样

const remoteObjectCache = v8Util.createIDWeakMap()

remote.require = function (name) {
// Tell the main process to return the meta data of the module.
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
// Create a proxy object.
...
remoteObjectCache.set(meta.id, object)
return object
}

请注意,remoteObjectCache 将对象存储为弱引用,因此在对象被垃圾回收时无需删除键。

原生代码

对于对 Electron 中弱引用的 C++ 代码感兴趣的人,可以在以下文件中找到

setDestructor API

createIDWeakMap API

Electron 内部原理:将 Node 作为库使用

·阅读时长 5 分钟

这是解释 Electron 内部机制的系列博文中的第二篇。如果您还没有看过关于事件循环集成的 第一篇博文,请务必查看。

大多数人使用 Node 进行服务器端应用程序开发,但由于 Node 丰富的 API 集和蓬勃发展的社区,它也非常适合作为嵌入式库。这篇博文将介绍 Node 在 Electron 中是如何被用作库的。


构建系统

Node 和 Electron 都使用 GYP 作为它们的构建系统。如果您想将 Node 嵌入到您的应用程序中,您也必须使用它作为您的构建系统。

GYP 不熟悉?请在继续阅读本文之前,先阅读本指南

Node 的标志

Node 源代码目录中的 node.gyp 文件描述了 Node 如何构建,以及许多 GYP 变量,这些变量控制着 Node 的哪些部分被启用以及是否开启某些配置。

要更改构建标志,您需要设置项目中 `.gypi` 文件中的变量。Node 中的 `configure` 脚本可以为您生成一些通用配置,例如运行 `./configure --shared` 将生成一个 `config.gypi` 文件,其中包含指示 Node 作为共享库构建的变量。

Electron 不使用 `configure` 脚本,因为它有自己的构建脚本。Node 的配置定义在 Electron 根源代码目录的 common.gypi 文件中。

在 Electron 中,通过将 `GYP` 变量 `node_shared` 设置为 `true`,Node 被链接为共享库,这样 Node 的构建类型将从 `executable` 更改为 `shared_library`,并且包含 Node `main` 入点的源代码将不会被编译。

由于 Electron 使用 Chromium 附带的 V8 库,因此 Node 源代码中包含的 V8 库不会被使用。这是通过将 node_use_v8_platformnode_use_bundled_v8 都设置为 false 来实现的。

共享库或静态库

链接 Node 时,有两种选择:您可以将 Node 构建为静态库并将其包含在最终的可执行文件中,也可以将其构建为共享库并与最终的可执行文件一起分发。

在很长一段时间里,Electron 中 Node 都是作为静态库构建的。这使得构建过程简单,实现了最佳的编译器优化,并允许 Electron 在不额外包含 node.dll 文件的情况下分发。

然而,自从 Chrome 切换到使用 BoringSSL 后,情况发生了变化。BoringSSL 是 OpenSSL 的一个分支,它移除了几个未使用的 API 并更改了许多现有接口。由于 Node 仍在使用 OpenSSL,如果它们被链接在一起,编译器将产生大量链接错误,因为存在冲突的符号。

Electron 不能在 Node 中使用 BoringSSL,也不能在 Chromium 中使用 OpenSSL,因此唯一的选择是切换到将 Node 构建为共享库,并 隐藏每个组件中的 BoringSSL 和 OpenSSL 符号

这个改变给 Electron 带来了一些积极的副作用。在此更改之前,如果您使用了原生模块,则无法重命名 Windows 上的 Electron 可执行文件,因为可执行文件的名称已硬编码在导入库中。在 Node 构建为共享库后,这个限制就消失了,因为所有原生模块都链接到 `node.dll`,其名称无需更改。

支持原生模块

原生模块 在 Node 中工作的方式是定义一个入口函数供 Node 加载,然后搜索 Node 中的 V8 和 libuv 符号。这对于嵌入者来说有点麻烦,因为默认情况下,在将 Node 构建为库时,V8 和 libuv 的符号是隐藏的,原生模块将无法加载,因为它们找不到这些符号。

因此,为了使原生模块能够工作,V8 和 libuv 的符号在 Electron 中被暴露出来。对于 V8,这是通过 强制暴露 Chromium 配置文件中的所有符号 来实现的。对于 libuv,它通过 设置 `BUILDING_UV_SHARED=1` 定义 来实现。

在您的应用程序中启动 Node

经过构建和链接 Node 的所有工作后,最后一步是在您的应用程序中运行 Node。

Node 没有提供太多用于将其嵌入其他应用程序的公共 API。通常,您只需要调用 node::Startnode::Init 来启动一个新的 Node 实例。然而,如果您正在构建一个基于 Node 的复杂应用程序,您必须使用 `node::CreateEnvironment` 等 API 来精确地控制每一步。

在 Electron 中,Node 以两种模式启动:一种是在主进程中运行的独立模式,类似于官方 Node 二进制文件;另一种是嵌入模式,将 Node API 插入到网页中。这些细节将在未来的文章中进行解释。

Electron 内部原理:消息循环集成

·阅读时长 4 分钟

这是关于 Electron 内部机制系列文章的第一篇。本文将介绍 Node 的事件循环是如何集成到 Electron 的 Chromium 中的。


曾有许多尝试将 Node 用于 GUI 编程的方案,例如用于 GTK+ 绑定的 node-gui,以及用于 QT 绑定的 node-qt。但它们都无法在生产环境中使用,因为 GUI 工具包有自己的消息循环,而 Node 使用 libuv 来处理自己的事件循环,主线程一次只能运行一个循环。因此,在 Node 中运行 GUI 消息循环的常用技巧是在具有非常小间隔的计时器中泵送消息循环,这使得 GUI 界面响应缓慢并占用大量 CPU 资源。

在 Electron 的开发过程中,我们遇到了同样的问题,只是方向相反:我们需要将 Node 的事件循环集成到 Chromium 的消息循环中。

主进程和渲染器进程

在深入了解消息循环集成细节之前,我将首先解释 Chromium 的多进程架构。

在 Electron 中有两种类型的进程:主进程和渲染进程(这实际上是极度简化的,完整的视图请参阅 多进程架构)。主进程负责 GUI 工作,如创建窗口,而渲染进程只负责运行和渲染网页。

Electron 允许使用 JavaScript 控制主进程和渲染器进程,这意味着我们必须将 Node 集成到这两个进程中。

用 libuv 替换 Chromium 的消息循环

我的第一个尝试是使用 libuv 重写 Chromium 的消息循环。

对于渲染器进程来说这很容易,因为它的消息循环只监听文件描述符和定时器,我只需要实现与 libuv 的接口。

然而,对于主进程来说,这要困难得多。每个平台都有自己的 GUI 消息循环。macOS Chromium 使用 `NSRunLoop`,而 Linux 使用 glib。我尝试了许多技巧来从原生 GUI 消息循环中提取底层文件描述符,然后将它们提供给 libuv 进行迭代,但我仍然遇到了一些不起作用的边缘情况。

因此,我最终添加了一个定时器,以很小的间隔轮询 GUI 消息循环。结果是进程占用了恒定的 CPU 使用率,并且某些操作出现了长时间延迟。

在单独的线程中轮询 Node 的事件循环

随着 libuv 的成熟,可以采用另一种方法。

libuv 中引入了 backend fd 的概念,它是一个文件描述符(或句柄),libuv 会轮询它来处理事件循环。因此,通过轮询 backend fd,可以在 libuv 有新事件时收到通知。

所以在 Electron 中,我创建了一个单独的线程来轮询后端 fd,并且由于我使用的是系统调用进行轮询而不是 libuv API,因此它是线程安全的。每当 libuv 的事件循环中出现新事件时,都会向 Chromium 的消息循环发送一条消息,然后 libuv 的事件将在主线程中得到处理。

通过这种方式,我避免了修改 Chromium 和 Node,并且主进程和渲染器进程使用了相同的代码。

代码

您可以在 `electron/atom/common/` 下的 `node_bindings` 文件中找到消息循环集成的实现。对于希望集成 Node 的项目,可以轻松地重用它。

更新:实现已移至 electron/shell/common/node_bindings.cc