跳到主要内容

标记为“Electron 内部”的 6 篇文章

“对 Electron 源代码进行技术深度探索”

查看所有标签

WebView2 与 Electron

·阅读 6 分钟

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

两个团队都明确表示目标是让 Web 技术在桌面上达到最佳状态,并且正在讨论一份共享的全面比较。

Electron 和 WebView2 都是快速发展且不断演进的项目。我们整理了 Electron 和 WebView2 目前存在的相似点和不同点的一个简要快照。


架构概述

Electron 和 WebView2 都基于 Chromium 源码构建,用于渲染 Web 内容。严格来说,WebView2 基于 Edge 源码构建,但 Edge 是使用 Chromium 源码的一个分支构建的。Electron 不与 Chrome 共享任何 DLL。WebView2 二进制文件硬链接到 Edge(从 Edge 90 开始为稳定版频道),因此它们共享磁盘空间和部分工作集。更多信息请参阅常青分发模式

Electron 应用程序总是捆绑并分发它们开发时使用的确切 Electron 版本。WebView2 有两种分发选项。你可以捆绑应用程序开发时使用的确切 WebView2 库,或者可以使用系统上可能已有的共享运行时版本。WebView2 为每种方法都提供了工具,包括在共享运行时缺失时使用的引导安装程序。从 Windows 11 开始,WebView2 是随系统预装的(*inbox*)。

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

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 构建的应用程序的脚手架

在渲染 Web 内容*之外*,还有一些差异会发挥作用,Electron、WebView2、Edge 和其他项目的人员已表示有兴趣进行详细比较,包括 PWAs。

进程间通信 (IPC)

有一个我们想立即强调的不同点,因为我们认为它通常是 Electron 应用中的性能考虑因素。

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

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

Electron 通过MessagePorts API 支持任意两个进程之间的直接 IPC,该 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.bindingprocess.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)

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

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 构建为供 Electron 使用的库,以及构建系统多年来的演变过程。


使用 CEF

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

为了维护稳定的 API,CEF 隐藏了 Chromium 的所有细节,并用自己的接口封装了 Chromium 的 API。因此,当我们像将 Node.js 集成到网页中那样需要访问底层的 Chromium API 时,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 中链接它。通过这种方式,开发者在为 Electron 贡献时不再需要构建整个 Chromium。

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 中的每个模块构建为单独的共享库,从而使得最终链接步骤花费的时间变得微不足道。

gn 的改进之一是引入了 source_set,它代表一组目标文件。在 gyp 中,每个模块由 static_libraryshared_library 表示,对于 Chromium 的常规构建,每个模块生成一个静态库,并在最终的可执行文件中链接在一起。使用 gn 后,每个模块现在只生成一堆目标文件,最终的可执行文件只需将所有目标文件链接在一起,因此不再生成中间的静态库文件。

发布原始二进制文件

随着 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_libraryshared_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 来测试弱引用,它会给传入的对象添加一个弱引用,并在对象被垃圾回收时调用回调函数 (callback)

// 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 会将该对象存储在一个 map 中并为其分配一个 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];
});

包含弱值的 Map

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

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

例如,以下代码

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

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

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

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

使用 JavaScript 核心 API 无法实现这一点。使用普通 map 缓存对象会阻止 V8 垃圾回收这些对象,而 WeakMap 类只能使用对象作为弱键。

为了解决这个问题,添加了一种以弱引用作为值的 map 类型,这非常适合使用 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 用作库

·阅读 4 分钟

这是解释 Electron 内部原理的系列文章中的第二篇。如果您还没有阅读过关于事件循环集成的第一篇文章,请查看。

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


构建系统

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

GYP 新手?在深入阅读本文之前,请先阅读本指南

Node 的标志

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

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

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

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

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

共享库还是静态库

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

在很长一段时间里,Node 在 Electron 中被构建为静态库。这使得构建变得简单,启用了最佳编译器优化,并允许 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.dll 的名称无需更改。

支持原生模块

Node 中的原生模块通过定义一个供 Node 加载的入口函数,然后从 Node 中搜索 V8 和 libuv 的符号来工作。对于嵌入者来说,这有点麻烦,因为默认情况下,当 Node 作为库构建时,V8 和 libuv 的符号是隐藏的,原生模块会因为找不到符号而加载失败。

因此,为了使原生模块能够工作,Electron 中暴露了 V8 和 libuv 的符号。对于 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 内部:消息循环集成

·3 分钟阅读

这是解释 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 为其事件循环轮询的文件描述符(或句柄)。因此,通过轮询后端文件描述符,可以在 libuv 中有新事件时收到通知。

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

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

代码

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

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