跳至主要内容

带有“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 为每种方法提供了工具,包括在共享运行时缺失的情况下使用的引导安装程序。WebView2 从 Windows 11 开始包含在系统中。

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

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、模块或节点原生插件。WebView2 应用程序不会假设应用程序的其余部分是用哪种语言或框架编写的。您的 JavaScript 代码必须通过应用程序主机进程代理任何操作系统访问。

Electron 努力保持与 Web API 的兼容性,包括从 Fugu 项目 开发的 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 和其他团队都表达了对开展详细比较(包括 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 利用结构化克隆算法。利用此功能的应用程序可以在进程之间发送对象时避免支付 JSON 序列化费用。

摘要

Electron 和 WebView2 有许多不同之处,但不要期望它们在渲染网页内容方面有太大差异。最终,应用程序的架构和 JavaScript 库/框架对内存和性能的影响比其他任何因素都大,因为Chromium 始终是 Chromium,无论它运行在哪里。

特别感谢 WebView2 团队审阅本文,并确保我们对 WebView2 架构有最新的了解。他们欢迎任何关于该项目的反馈

从原生到 JavaScript 在 Electron 中

·阅读时长 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)

在上面的代码行中,.SetMethod 被调用到mate::ObjectTemplateBuilder 上。.SetMethod 可以被调用到ObjectTemplateBuilder 类的任何实例上,以在 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 是一个并非一定为其他项目设计的项目。本文介绍了 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 构建一个最小浏览器。提供浏览器界面的核心模块称为内容模块。

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

NW.js 和 Electron 的早期版本都使用这种方式进行构建。

缺点是,Chromium 的代码库非常庞大,需要功能非常强大的机器才能构建。对于普通笔记本电脑来说,这可能需要 5 个多小时。因此,这极大地影响了可以为该项目做出贡献的开发人员数量,也使开发速度变慢。

将 Chromium 构建为单个共享库

作为内容模块的用户,Electron 在大多数情况下不需要修改 Chromium 的代码,因此,提高 Electron 构建速度的明显方法是将 Chromium 构建为共享库,然后在 Electron 中与它进行链接。这样,开发人员在为 Electron 做出贡献时就不再需要构建整个 Chromium。

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

brightray 项目也是作为 libchromiumcontent 的一部分诞生的,它提供了内容模块周围的一层薄薄的包装。

通过使用 libchromiumcontent 和 brightray 结合在一起,开发人员可以快速构建浏览器,而无需了解 Chromium 构建的细节。而且,它消除了构建项目时需要高速网络和功能强大的机器的要求。

除了 Electron 之外,还有其他使用这种方式构建的基于 Chromium 的项目,例如Breach 浏览器

过滤导出的符号

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

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

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

组件构建

在讨论 libchromiumcontent 中采取的下一步措施之前,首先介绍一下 Chromium 中的组件构建概念很重要。

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

发布原始二进制文件

随着 Chromium 的不断发展,Chromium 中的导出符号数量急剧增加,即使是 Content 模块和 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 来测试弱引用,它向传递的对象添加一个弱引用,并在对象被垃圾回收时调用回调。

// 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 作为库使用

·阅读时长 4 分钟

这是关于解释 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 中,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 构建为静态库并将其包含在最终的可执行文件中,或者可以将其构建为共享库并将其与最终可执行文件一起发布。

在 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 内部:消息循环集成

·阅读时长:3 分钟

这是解释 Electron 内部机制的一系列文章的第一篇。这篇文章介绍了 Node 的事件循环如何在 Electron 中与 Chromium 集成。


曾经有过许多尝试使用 Node 进行 GUI 编程,例如 node-gui 用于 GTK+ 绑定,以及 node-qt 用于 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 的成熟,现在可以使用另一种方法。

后端 fd 的概念被引入到 libuv 中,它是一个文件描述符(或句柄),libuv 会轮询其事件循环。因此,通过轮询后端 fd,可以获悉 libuv 中是否有新的事件。

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

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

代码

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

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