Electron 内部原理:将 Chromium 构建为库
Electron 基于 Google 的开源 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 中导出的符号太多,即使是内容模块和 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 做出贡献。这种努力是完全值得的。